/*
* Copyright 2016 The Simple File Server Authors
*
* 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.sfs.io;
import io.vertx.core.Context;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.logging.Logger;
import io.vertx.core.streams.ReadStream;
import io.vertx.core.streams.WriteStream;
import org.sfs.SfsVertx;
import org.sfs.encryption.Algorithm;
import org.sfs.encryption.AlgorithmDef;
import org.sfs.rx.RxHelper;
import rx.Observable;
import java.io.IOException;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.HashSet;
import java.util.Set;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static io.vertx.core.buffer.Buffer.buffer;
import static io.vertx.core.logging.LoggerFactory.getLogger;
import static java.nio.channels.AsynchronousFileChannel.open;
import static java.nio.file.Files.createTempFile;
import static java.nio.file.Files.deleteIfExists;
import static java.nio.file.StandardOpenOption.READ;
import static java.nio.file.StandardOpenOption.WRITE;
import static java.util.Arrays.fill;
import static org.sfs.encryption.AlgorithmDef.getPreferred;
import static org.sfs.rx.Defer.aVoid;
public class FileBackedBuffer implements BufferEndableWriteStream {
private static final Logger LOGGER = getLogger(FileBackedBuffer.class);
private static final int MAX_WRITES = 16 * 1024;
private static final AlgorithmDef ALGORITHM_DEF = getPreferred();
private static byte[] SALT = ALGORITHM_DEF.generateSaltBlocking();
private int fileThreshold;
private SfsVertx sfsVertx;
private Path tempFileDirectory;
private Path tempFile;
private AsynchronousFileChannel channel;
private boolean fileOpen = false;
private long size = 0;
private Handler<Throwable> exceptionHandler;
private Handler<Void> endHandler;
private Handler<Void> drainHandler;
private Buffer memory;
private BufferEndableWriteStream fileWriteStreamConsumer;
private CountingEndableWriteStream countingEndableWriteStream;
private Algorithm algorithm;
private boolean openedReadStream = false;
private boolean ended = false;
private boolean encryptTempFile;
private Context context;
public FileBackedBuffer(SfsVertx sfsVertx, int fileThreshold, boolean encryptTempFile, Path tempFileDirectory) {
this.sfsVertx = sfsVertx;
this.encryptTempFile = encryptTempFile;
this.fileThreshold = fileThreshold;
this.memory = buffer();
this.tempFileDirectory = tempFileDirectory;
this.context = sfsVertx.getOrCreateContext();
}
public FileBackedBuffer(SfsVertx sfsVertx, int fileThreshold, Path tempFileDirectory) {
this(sfsVertx, fileThreshold, true, tempFileDirectory);
}
public boolean isFileOpen() {
return fileOpen;
}
public long length() {
return size;
}
public ReadStream<Buffer> readStream() {
checkReadStreamNotOpen();
openedReadStream = true;
if (fileOpen) {
AsyncFileReader asyncFileReader = new AsyncFileReaderImpl(context, 0, 8192, countingEndableWriteStream.count(), channel, LOGGER);
if (encryptTempFile) {
return algorithm.decrypt(asyncFileReader);
} else {
return asyncFileReader;
}
} else {
return new BufferReadStream(memory);
}
}
@Override
public BufferEndableWriteStream endHandler(Handler<Void> endHandler) {
this.endHandler = endHandler;
BufferEndableWriteStream dst = maybeGetWriteStream();
if (dst != null) {
dst.endHandler(endHandler);
} else {
handleEnd();
}
return this;
}
@Override
public WriteStream<Buffer> exceptionHandler(Handler<Throwable> handler) {
this.exceptionHandler = handler;
BufferEndableWriteStream dst = maybeGetWriteStream();
if (dst != null) {
dst.exceptionHandler(handler);
}
return this;
}
@Override
public WriteStream<Buffer> write(Buffer data) {
checkReadStreamNotOpen();
size += data.length();
BufferEndableWriteStream dst = maybeGetWriteStream();
if (dst != null) {
dst.write(data);
} else {
memory.appendBuffer(data);
handleDrain();
}
return this;
}
@Override
public void end() {
checkReadStreamNotOpen();
checkState(!ended, "Already ended");
ended = true;
BufferEndableWriteStream dst = maybeGetWriteStream();
if (dst != null) {
dst.end();
} else {
handleEnd();
}
}
@Override
public WriteStream<Buffer> setWriteQueueMaxSize(int maxSize) {
return this;
}
@Override
public boolean writeQueueFull() {
BufferEndableWriteStream dst = maybeGetWriteStream();
if (dst != null) {
return dst.writeQueueFull();
} else {
return false;
}
}
@Override
public WriteStream<Buffer> drainHandler(Handler<Void> drainHandler) {
this.drainHandler = drainHandler;
BufferEndableWriteStream dst = maybeGetWriteStream();
if (dst != null) {
dst.drainHandler(drainHandler);
} else {
handleDrain();
}
return this;
}
@Override
public void end(Buffer data) {
checkReadStreamNotOpen();
size += data.length();
checkState(!ended, "Already ended");
ended = true;
BufferEndableWriteStream dst = maybeGetWriteStream();
if (dst != null) {
dst.end(data);
} else {
memory.appendBuffer(data);
handleEnd();
}
}
public Observable<Void> close() {
if (channel != null || tempFile != null) {
return RxHelper.executeBlocking(context, sfsVertx.getBackgroundPool(), () -> {
if (channel != null) {
try {
channel.close();
} catch (IOException e) {
LOGGER.warn("Handling close exception", e);
}
}
if (tempFile != null) {
try {
deleteIfExists(tempFile);
} catch (IOException e) {
LOGGER.warn("Handling close exception", e);
}
}
return (Void) null;
});
} else {
return aVoid();
}
}
protected void handleError(Throwable e) {
if (exceptionHandler != null) {
exceptionHandler.handle(e);
} else {
LOGGER.error("Unhandled Exception", e);
}
}
protected void handleDrain() {
Handler<Void> handler = drainHandler;
if (handler != null) {
drainHandler = null;
handler.handle(null);
}
}
protected void handleEnd() {
if (ended) {
Handler<Void> handler = endHandler;
if (handler != null) {
endHandler = null;
handler.handle(null);
}
}
}
protected BufferEndableWriteStream maybeGetWriteStream() {
if (size > fileThreshold) {
if (!fileOpen) {
fileOpen = true;
try {
Set<StandardOpenOption> options = new HashSet<>();
options.add(READ);
options.add(WRITE);
tempFile = createTempFile(tempFileDirectory, "FileBackedBufferWriteStreamConsumer", "");
channel =
open(
tempFile,
options,
sfsVertx.getIoPool());
} catch (IOException e) {
throw new RuntimeException(e);
}
WriteQueueSupport<AsyncFileWriter> writeQueueSupport = new WriteQueueSupport<>(MAX_WRITES);
fileWriteStreamConsumer = new AsyncFileWriterImpl(0L, writeQueueSupport, context, channel, LOGGER);
countingEndableWriteStream = new CountingEndableWriteStream(fileWriteStreamConsumer);
fileWriteStreamConsumer = countingEndableWriteStream;
if (encryptTempFile) {
byte[] secret = ALGORITHM_DEF.generateKeyBlocking();
try {
algorithm = ALGORITHM_DEF.create(secret, SALT);
fileWriteStreamConsumer = algorithm.encrypt(fileWriteStreamConsumer);
} finally {
fill(secret, (byte) 0);
}
}
fileWriteStreamConsumer.exceptionHandler(exceptionHandler);
if (memory.length() > 0) {
fileWriteStreamConsumer.write(memory);
}
fileWriteStreamConsumer.drainHandler(drainHandler);
fileWriteStreamConsumer.endHandler(endHandler);
}
return fileWriteStreamConsumer;
} else {
return null;
}
}
protected void checkReadStreamNotOpen() {
checkArgument(!openedReadStream, "ReadStream is open");
}
}