org.kawanfw.file.api.client.RemoteOutputStream Maven / Gradle / Ivy
Show all versions of awake-file-server Show documentation
/*
* This file is part of Awake FILE.
* Awake file: Easy file upload & download over HTTP with Java.
* Copyright (C) 2015, KawanSoft SAS
* (http://www.kawansoft.com). All rights reserved.
*
* Awake FILE is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* Awake FILE 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA
*
* Any modifications to this file must keep this entire header
* intact.
*/
package org.kawanfw.file.api.client;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.logging.Level;
import org.apache.commons.io.FileUtils;
import org.kawanfw.commons.api.client.SessionParameters;
import org.kawanfw.commons.api.client.InvalidLoginException;
import org.kawanfw.commons.api.client.RemoteException;
import org.kawanfw.commons.util.ClientLogger;
import org.kawanfw.commons.util.FrameworkDebug;
import org.kawanfw.commons.util.Tag;
import org.kawanfw.file.api.util.client.ApiOutputStreamUploader;
import org.kawanfw.file.api.util.client.ChunkUtil;
import org.kawanfw.file.api.util.client.ExceptionThrower;
import org.kawanfw.file.api.util.client.RemoteFilePartStore;
import org.kawanfw.file.api.util.client.UniqueFileCreator;
/**
*
* A remote output stream is an output stream for writing data to a remote
* File
.
*
* It allows to create a remote file by writing bytes on the
* {@code RemoteOutputStream} with standards {@code OutputStream} write methods.
*
* Large streams are split in chunks that are uploaded in sequence. The default
* chunk length is 10Mb. You can change the default value with
* {@link SessionParameters#setUploadChunkLength(long)} before passing
* {@code SessionParameters} to this {@link RemoteSession} constructor.
*
* Note that stream chunking requires all chunks to be sent to the same web
* server that will aggregate the chunks on the same file. Thus, stream chunking
* does not support true stateless architecture with multiple identical web
* servers. If you want to set a full stateless architecture with multiple
* identical web servers, you must disable file chunking. This is done by
* setting a 0 upload chunk length value using
* {@link SessionParameters#setUploadChunkLength(long)}.
*
* A recovery mechanism allows - in case of failure - to start again in the same
* JVM run the data upload from the last non-uploaded chunk. The recovery
* mechanism is enabled only if the length is known (and so is different from
* -1)
* See User Guide for more information.
*
* Note that {@code write} and {@code close} methods throw following subclasses
* of {@code IOException}:
*
* -
* {@code InvalidLoginException} the session has been closed by a
* {@code RemoteSession.logoff()}.
* - {@code UnknownHostException} if host URL (http://www.acme.org) does not
* exists or no Internet connection.
* - {@code ConnectException} if the Host is correct but the ServerFileManager
* Servlet is not reachable (http://www.acme.org/ServerFileManager) and access
* failed with a status != OK (200). (If the host is incorrect, or is impossible
* to connect to - Tomcat down - the {@code ConnectException} will be the sub
* exception {@code HttpHostConnectException}.)
* - {@code SocketException} if network failure during transmission.
* - {@link RemoteException} an exception has been thrown on the server side.
*
*
* Example:
*
* // Define URL of the path to the {@code ServerFileManager} servlet
* char[] password = { 'm', 'y', 'P', 'a', 's', 's', 'w', 'o', 'r', 'd' };
*
* // Establish a session with the remote server
* RemoteSession remoteSession = new RemoteSession(url, username, password);
*
* File file = new File("C:\\Users\\Mike\\Koala.jpg");
* String pathname = "/Koala.jpg";
*
* InputStream in = null;
* OutputStream out = null;
*
* try {
*
* // Get an InputStream from our local file
* in = new FileInputStream(file);
*
* // Create an OutputStream that maps a remote file on the host
* out = new RemoteOutputStream(remoteSession, pathname, file.length());
*
* // Create the remote file reading the InpuStream and writing
* // on the OutputStream
* byte[] buffer = new byte[1024 * 4];
* int n = 0;
* while ((n = in.read(buffer)) != -1) {
* out.write(buffer, 0, n);
* }
*
* // It is better to also close out before finally
* // because RemoteOutputStream.close() sends data to the server and
* // is thus prone to IOException
* out.close();
*
* } catch (IOException e) {
* // Treat IOException, including those thrown by out.close()
* e.printStackTrace();
* // Etc.
*
* } finally {
* if (in != null)
* in.close();
* if (out != null)
* out.close();
* }
*
*
* @see org.kawanfw.file.api.client.RemoteFile
* @see org.kawanfw.file.api.client.RemoteInputStream
*
* @author Nicolas de Pomereu
* @since 2.0
*/
public class RemoteOutputStream extends OutputStream {
/** For debug info */
private static boolean DEBUG = FrameworkDebug
.isSet(RemoteOutputStream.class);
/** the file session in use */
private RemoteSession remoteSession;
/** The remote file's pathname */
private String pathname = null;
/** The stream containing the datas to upload */
private OutputStream out = null;
/** The unique file used as container and reference for download */
private File fileUnique = null;
/** The counter to use use for file chunks */
private int cpt = 0;
/** Total length for one upload */
private long totalLength = 0;
/** Temporary length for one chunk */
private long tempLength = 0;
/** The remote file length */
private long remoteFileLength = -1;
/** Compute the total length of upload files, must match totalLength */
private long totalFileLength = 0;
/** Happens id the lase send id exactly the size of a chunk */
private boolean noSendInclose = false;
/**
* Creates an output stream to write to the remote file with the
* specified pathname.
*
* The real path of the remote file depends on the Awake FILE
* configuration on the server. See User Documentation.
*
* @param remoteSession
* the current remote session
* @param pathname
* the pathname on host with "/" as file separator. Must be absolute.
* @param length
* the final length of the remote file after creation, -1 if unknown
*
* @throws IllegalArgumentException
* if remoteFile is null or length is < -1
* @throws InvalidLoginException
* the session has been closed by a {@code logoff()}
* @throws IOException
* if an I/O error occurs.
*
* @since 2.0
*/
public RemoteOutputStream(RemoteSession remoteSession, String pathname,
long length) throws IOException {
initConstructors(remoteSession, pathname, length);
}
/**
* Creates an output stream to write to the remote file with the specified
* pathname.
*
* The real path of the remote file depends on the Awake FILE configuration
* on the server. See User Documentation.
*
* @param fileSession
* the current file session
* @param pathname
* the pathname on host with "/" as file separator. Must be
* absolute.
* @param length
* the final length of the remote file after creation, -1 if
* unknown
*
* @throws IllegalArgumentException
* if remoteFile is null or length is < -1
* @throws InvalidLoginException
* the session has been closed by a {@code logoff()}
* @throws IOException
* if an I/O error occurs.
*
* @deprecated As of version 3.0, replaced by:
* {@link RemoteOutputStream#RemoteOutputStream(RemoteSession, String, long)}
* @since 2.0
*/
public RemoteOutputStream(FileSession fileSession, String pathname,
long length) throws IOException {
if (fileSession == null) {
throw new IllegalArgumentException("fileSession is null!");
}
initConstructors(fileSession.getRemoteSession(), pathname, length);
}
/**
* Creates an output stream to write to the remote file represented by
* the specified RemoteFile
object.
*
* @param remoteFile
* the remote file
* @param length
* the final remote file length after creation, -1 if unknown
*
* @throws IllegalArgumentException
* if pathname is null or length is < -1
* @throws InvalidLoginException
* the session has been closed by a {@code logoff()}
* @throws IOException
* if an I/O error occurs.
*
* @since 3.0
*/
public RemoteOutputStream(RemoteFile remoteFile, long length) throws IOException {
if (remoteFile == null) {
throw new IllegalArgumentException("remoteFile is null!");
}
initConstructors(remoteFile.getRemoteSession(), remoteFile.getPath(), length);
}
/**
* Init done in constructors.
*
* @param remoteSession
* @param pathname
* @param length
* @throws IllegalArgumentException
* @throws InvalidLoginException
* @throws IOException
* @throws FileNotFoundException
*/
private void initConstructors(RemoteSession remoteSession, String pathname,
long length) throws IllegalArgumentException,
InvalidLoginException, IOException, FileNotFoundException {
if (remoteSession == null) {
throw new IllegalArgumentException("remoteSession is null!");
}
if (remoteSession.getUsername() == null
|| remoteSession.getAuthenticationToken() == null) {
throw new InvalidLoginException(RemoteSession.REMOTE_SESSION_IS_CLOSED);
}
if (pathname == null) {
throw new IllegalArgumentException("pathname is null!");
}
if (! pathname.startsWith("/")) {
throw new IllegalArgumentException("pathname must be asbsolute and start with \"/\": " + pathname);
}
if (length < -1) {
throw new IllegalArgumentException("length must be > -1.");
}
this.remoteSession = remoteSession;
this.pathname = pathname;
this.remoteFileLength = length;
// Create the unique filename corresponding to username & pathname name
// Must be done in constructor because close() uses fileUnique
fileUnique = UniqueFileCreator.createUnique(remoteSession.getUsername(),
this.pathname);
out = new BufferedOutputStream(new FileOutputStream(fileUnique));
}
/**
* Returns the remote file's pathname
*
* @return the remote file's pathname
*/
public String getPathname() {
return pathname;
}
/**
* Writes len
bytes from the specified byte array starting at
* offset off
to this output stream. The general contract for
* write(b, off, len)
is that some of the bytes in the array
* b
are written to the output stream in order; element
* b[off]
is the first byte written and
* b[off+len-1]
is the last byte written by this operation.
*
* The write
method of OutputStream
calls the
* write method of one argument on each of the bytes to be written out.
*
* If b
is null
, a
* NullPointerException
is thrown.
*
* If off
is negative, or len
is negative, or
* off+len
is greater than the length of the array
* b
, then an IndexOutOfBoundsException is thrown.
*
* @param b
* the data.
* @param off
* the start offset in the data.
* @param len
* the number of bytes to write.
* @exception IOException
* if an I/O error occurs. In particular, an
* IOException
is thrown if the output stream is
* closed.
*/
@Override
public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len);
totalLength += len;
tempLength += len;
// if length = exaclty only chunk==> mark it for the close
if (tempLength == ChunkUtil.getUploadChunkLength(remoteSession)) {
noSendInclose = true;
} else {
noSendInclose = false;
}
// if length >= chunkLength ==> send the chunk
if (tempLength >= ChunkUtil.getUploadChunkLength(remoteSession)) {
out.close();
cpt++;
uploadPerChunks(remoteSession, fileUnique, pathname, cpt, false);
tempLength = 0;
fileUnique = UniqueFileCreator.createUnique(
remoteSession.getUsername(), pathname);
out = new BufferedOutputStream(new FileOutputStream(fileUnique));
}
}
/**
* Writes the specified byte to this output stream. The general contract for
* write
is that one byte is written to the output stream. The
* byte to be written is the eight low-order bits of the argument
* b
. The 24 high-order bits of b
are ignored.
*
* @param b
* the byte
.
* @exception IOException
* if an I/O error occurs. In particular, an
* IOException
may be thrown if the output
* stream has been closed.
*/
@Override
public void write(int b) throws IOException {
// out.write(b);
throw new IOException(Tag.PRODUCT
+ " write(int b) method is not supported.");
}
/**
* Writes b.length
bytes from the specified byte array to this
* output stream. The general contract for write(b)
is that it
* should have exactly the same effect as the call
* write(b, 0, b.length)
.
*
* @param b
* the data.
* @exception IOException
* if an I/O error occurs.
* @see java.io.OutputStream#write(byte[], int, int)
*/
@Override
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
/**
* Flushes this stream by writing any buffered output to the underlying
* stream.
*
* @throws IOException
* If an I/O error occurs
*/
@Override
public void flush() throws IOException {
out.flush();
}
/**
* Closes this output stream and releases any system resources associated
* with this stream. The general contract of close
is that it
* closes the output stream. A closed stream cannot perform output
* operations and cannot be reopened.
*
* WARNING: before the output stream close, the remaining bytes are sent to
* server.
*
*
* @exception IOException
* if an I/O error occurs.
*/
@Override
public void close() throws IOException {
// if (out == null) : it has been closed already so escape now
// We don't want to resend datas...
if (out == null) {
return;
}
out.close();
// We immediately set out to null to avoid recall of this method
out = null;
try {
// If close is due to Exception throw, total written lenth will be
// < remote file length, so do nothing
if (remoteFileLength != -1 && totalLength < remoteFileLength) {
return;
}
if (noSendInclose) {
// Last send is exactly chunk length. We have nothing to do:
return;
}
// We must upload the end of our output stream before getting out
if (totalLength <= ChunkUtil.getUploadChunkLength(remoteSession)) {
ApiOutputStreamUploader apiOutputStreamUploader = new ApiOutputStreamUploader(
remoteSession.getUsername(),
remoteSession.getAuthenticationToken(),
remoteSession.getHttpTransfer());
apiOutputStreamUploader.uploadOneChunk(fileUnique, pathname,
ChunkUtil.getUploadChunkLength(remoteSession));
} else {
sendLastChunk();
}
} finally {
FileUtils.deleteQuietly(fileUnique);
}
}
/**
* As is says.
*
* @throws FileNotFoundException
* @throws IOException
* @throws IllegalArgumentException
* @throws InvalidLoginException
* @throws UnknownHostException
* @throws ConnectException
* @throws SocketException
* @throws RemoteException
*/
private void sendLastChunk() throws FileNotFoundException, IOException,
IllegalArgumentException, InvalidLoginException,
UnknownHostException, ConnectException, SocketException,
RemoteException {
cpt++;
uploadPerChunks(remoteSession, fileUnique, pathname, cpt, true);
// 1) Always remove if we don't know the remote file length
// 2) If remote file length is known, remove only at end of
// successfull transfer
if (remoteFileLength == -1
|| (remoteFileLength != -1 && totalLength >= remoteFileLength)) {
RemoteFilePartStore remoteFilePartStore = new RemoteFilePartStore(
remoteSession.getUsername(), fileUnique, pathname);
remoteFilePartStore.remove();
}
}
/**
* Upload the file per chunk
*
* @param file
* the file to upload
* @param pathname
* the file name on the host
* @param cpt
* the counter for file chunks
* @param isLastChunk
* says if this is the lastchunk
*
* @throws FileNotFoundException
* @throws IOException
* @throws IllegalArgumentException
* @throws InvalidLoginException
* @throws UnknownHostException
* @throws ConnectException
* @throws SocketException
* @throws RemoteException
*/
private void uploadPerChunks(RemoteSession remoteSession, File file,
String remoteFile, int cpt, boolean isLastChunk)
throws FileNotFoundException, IOException,
IllegalArgumentException, InvalidLoginException,
UnknownHostException, ConnectException, SocketException,
RemoteException {
// Upload files in chunk, creating temporary file with default size 10Mb
RemoteFilePartStore remoteFilePartStore = new RemoteFilePartStore(
remoteSession.getUsername(), file, remoteFile);
String remoteFilePart = remoteFile + "." + cpt + ".kawanfw.chunk";
if (isLastChunk) {
remoteFilePart += ".LASTCHUNK";
}
ExceptionThrower.throwSocketExceptionIfFlagFileExists();
// Do the upload only if it has not been done
if (!remoteFilePartStore.alreadyUploaded(remoteFilePart)) {
debug(new Date() + " Uploading " + remoteFilePart + "...");
ApiOutputStreamUploader apiOutputStreamUploader = new ApiOutputStreamUploader(
remoteSession.getUsername(),
remoteSession.getAuthenticationToken(),
remoteSession.getHttpTransfer());
ExceptionThrower.throwSocketExceptionIfFlagFileExists();
apiOutputStreamUploader.uploadOneChunk(file, remoteFilePart,
ChunkUtil.getUploadChunkLength(remoteSession));
remoteFilePartStore.storeFilePart(remoteFilePart);
totalFileLength += file.length();
debug("");
debug("totalLength / totalFileLength : " + totalLength + " / "
+ totalFileLength + " UPLOADED!");
FileUtils.deleteQuietly(file);
} else {
debug(new Date() + " No Uploading of " + remoteFilePart
+ ". Already done!");
}
}
/**
* debug tool
*/
private void debug(String s) {
if (DEBUG) {
ClientLogger.getLogger().log(Level.WARNING, s);
}
}
}