blob: d26538f0a3ce261274eaa0a041b41b11d1544340 [file] [log] [blame]
/*
* Copyright 2022 Google LLC
*
* 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 com.google.android.libraries.mobiledatadownload.file.openers;
import android.net.Uri;
import com.google.android.libraries.mobiledatadownload.file.Behavior;
import com.google.android.libraries.mobiledatadownload.file.OpenContext;
import com.google.android.libraries.mobiledatadownload.file.Opener;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import javax.annotation.Nullable;
/**
* An opener for updating a file atomically: does not modify the destination file until all of the
* data has been successfully written. Instead, it writes into a scratch file which it renames to
* the destination file once the data has been written successfully.
*
* <p>In order to implement isolation (preventing other processes from modifying this file during
* read-modify-write transaction), pass in a LockFileOpener instance to {@link #withLocking} call.
*
* <p>In order to implement durability (ensuring the data is in persistent storage), pass
* SyncBehavior to the original open call.
*
* <p>NOTE: This does not fsync the directory itself. See <internal> for possible implementation
* using NIO.
*/
public final class StreamMutationOpener implements Opener<StreamMutationOpener.Mutator> {
private Behavior[] behaviors;
/**
* Override this interface to implement the transformation. It is ok to read input and output in
* parallel. If an exception is thrown, execution stops and the destination file remains
* untouched.
*/
public interface Mutation {
boolean apply(InputStream in, OutputStream out) throws IOException;
}
@Nullable private LockFileOpener locking = null;
private StreamMutationOpener() {}
/** Create an instance of this opener. */
public static StreamMutationOpener create() {
return new StreamMutationOpener();
}
/**
* Enable exclusive locking with this opener. This is useful if multiple processes or threads need
* to maintain transactional isolation.
*/
public StreamMutationOpener withLocking(LockFileOpener locking) {
this.locking = locking;
return this;
}
/** Apply these behaviors while writing only. */
public StreamMutationOpener withBehaviors(Behavior... behaviors) {
this.behaviors = behaviors;
return this;
}
/** Open this URI for mutating. If the file does not exist, create it. */
@Override
public Mutator open(OpenContext openContext) throws IOException {
return new Mutator(openContext, locking, behaviors);
}
/** An intermediate result returned by this opener. */
public static final class Mutator implements Closeable {
private static final InputStream EMPTY_INPUTSTREAM = new ByteArrayInputStream(new byte[0]);
private final OpenContext openContext;
private final Closeable lock;
private final Behavior[] behaviors;
private Mutator(
OpenContext openContext, @Nullable LockFileOpener locking, @Nullable Behavior[] behaviors)
throws IOException {
this.openContext = openContext;
this.behaviors = behaviors;
if (locking != null) {
lock = locking.open(openContext);
if (lock == null) {
throw new IOException("Couldn't acquire lock");
}
} else {
lock = null;
}
}
public void mutate(Mutation mutation) throws IOException {
try (InputStream backendIn = openForReadOrEmpty(openContext.encodedUri());
InputStream in = openContext.chainTransformsForRead(backendIn).get(0)) {
Uri tempUri = ScratchFile.scratchUri(openContext.originalUri());
boolean commit = false;
try (OutputStream backendOut = openContext.backend().openForWrite(tempUri)) {
List<OutputStream> outputChain = openContext.chainTransformsForWrite(backendOut);
if (behaviors != null) {
for (Behavior behavior : behaviors) {
behavior.forOutputChain(outputChain);
}
}
try (OutputStream out = outputChain.get(0)) {
commit = mutation.apply(in, out);
if (commit) {
if (behaviors != null) {
for (Behavior behavior : behaviors) {
behavior.commit();
}
}
}
}
} catch (Exception ex) {
try {
openContext.storage().deleteFile(tempUri);
} catch (FileNotFoundException ex2) {
// Ignore.
}
if (ex instanceof IOException) {
throw (IOException) ex;
}
throw new IOException(ex);
}
if (commit) {
openContext.storage().rename(tempUri, openContext.originalUri());
}
}
}
@Override
public void close() throws IOException {
if (lock != null) {
lock.close();
}
}
// Open the file for read if it's present, otherwise return an empty stream.
private InputStream openForReadOrEmpty(Uri uri) throws IOException {
try {
return openContext.backend().openForRead(uri);
} catch (FileNotFoundException ex) {
return EMPTY_INPUTSTREAM;
}
}
}
}