1 /* 2 * Copyright 2022 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.libraries.mobiledatadownload.file.openers; 17 18 import android.net.Uri; 19 import com.google.android.libraries.mobiledatadownload.file.Behavior; 20 import com.google.android.libraries.mobiledatadownload.file.OpenContext; 21 import com.google.android.libraries.mobiledatadownload.file.Opener; 22 import com.google.errorprone.annotations.CanIgnoreReturnValue; 23 import java.io.ByteArrayInputStream; 24 import java.io.Closeable; 25 import java.io.FileNotFoundException; 26 import java.io.IOException; 27 import java.io.InputStream; 28 import java.io.OutputStream; 29 import java.util.List; 30 import javax.annotation.Nullable; 31 32 /** 33 * An opener for updating a file atomically: does not modify the destination file until all of the 34 * data has been successfully written. Instead, it writes into a scratch file which it renames to 35 * the destination file once the data has been written successfully. 36 * 37 * <p>In order to implement isolation (preventing other processes from modifying this file during 38 * read-modify-write transaction), pass in a LockFileOpener instance to {@link #withLocking} call. 39 * 40 * <p>In order to implement durability (ensuring the data is in persistent storage), pass 41 * SyncBehavior to the original open call. 42 * 43 * <p>NOTE: This does not fsync the directory itself. See <internal> for possible implementation 44 * using NIO. 45 */ 46 public final class StreamMutationOpener implements Opener<StreamMutationOpener.Mutator> { 47 48 private Behavior[] behaviors; 49 50 /** 51 * Override this interface to implement the transformation. It is ok to read input and output in 52 * parallel. If an exception is thrown, execution stops and the destination file remains 53 * untouched. 54 */ 55 public interface Mutation { apply(InputStream in, OutputStream out)56 boolean apply(InputStream in, OutputStream out) throws IOException; 57 } 58 59 @Nullable private LockFileOpener locking = null; 60 StreamMutationOpener()61 private StreamMutationOpener() {} 62 63 /** Create an instance of this opener. */ create()64 public static StreamMutationOpener create() { 65 return new StreamMutationOpener(); 66 } 67 68 /** 69 * Enable exclusive locking with this opener. This is useful if multiple processes or threads need 70 * to maintain transactional isolation. 71 */ 72 @CanIgnoreReturnValue withLocking(LockFileOpener locking)73 public StreamMutationOpener withLocking(LockFileOpener locking) { 74 this.locking = locking; 75 return this; 76 } 77 78 /** Apply these behaviors while writing only. */ 79 @CanIgnoreReturnValue withBehaviors(Behavior... behaviors)80 public StreamMutationOpener withBehaviors(Behavior... behaviors) { 81 this.behaviors = behaviors; 82 return this; 83 } 84 85 /** Open this URI for mutating. If the file does not exist, create it. */ 86 @Override open(OpenContext openContext)87 public Mutator open(OpenContext openContext) throws IOException { 88 return new Mutator(openContext, locking, behaviors); 89 } 90 91 /** An intermediate result returned by this opener. */ 92 public static final class Mutator implements Closeable { 93 private static final InputStream EMPTY_INPUTSTREAM = new ByteArrayInputStream(new byte[0]); 94 private final OpenContext openContext; 95 private final Closeable lock; 96 private final Behavior[] behaviors; 97 Mutator( OpenContext openContext, @Nullable LockFileOpener locking, @Nullable Behavior[] behaviors)98 private Mutator( 99 OpenContext openContext, @Nullable LockFileOpener locking, @Nullable Behavior[] behaviors) 100 throws IOException { 101 this.openContext = openContext; 102 this.behaviors = behaviors; 103 if (locking != null) { 104 lock = locking.open(openContext); 105 if (lock == null) { 106 throw new IOException("Couldn't acquire lock"); 107 } 108 } else { 109 lock = null; 110 } 111 } 112 mutate(Mutation mutation)113 public void mutate(Mutation mutation) throws IOException { 114 try (InputStream backendIn = openForReadOrEmpty(openContext.encodedUri()); 115 InputStream in = openContext.chainTransformsForRead(backendIn).get(0)) { 116 Uri tempUri = ScratchFile.scratchUri(openContext.originalUri()); 117 boolean commit = false; 118 try (OutputStream backendOut = openContext.backend().openForWrite(tempUri)) { 119 List<OutputStream> outputChain = openContext.chainTransformsForWrite(backendOut); 120 if (behaviors != null) { 121 for (Behavior behavior : behaviors) { 122 behavior.forOutputChain(outputChain); 123 } 124 } 125 try (OutputStream out = outputChain.get(0)) { 126 commit = mutation.apply(in, out); 127 if (commit) { 128 if (behaviors != null) { 129 for (Behavior behavior : behaviors) { 130 behavior.commit(); 131 } 132 } 133 } 134 } 135 } catch (Exception ex) { 136 try { 137 openContext.storage().deleteFile(tempUri); 138 } catch (FileNotFoundException ex2) { 139 // Ignore. 140 } 141 if (ex instanceof IOException) { 142 throw (IOException) ex; 143 } 144 throw new IOException(ex); 145 } 146 if (commit) { 147 openContext.storage().rename(tempUri, openContext.originalUri()); 148 } 149 } 150 } 151 152 @Override close()153 public void close() throws IOException { 154 if (lock != null) { 155 lock.close(); 156 } 157 } 158 159 // Open the file for read if it's present, otherwise return an empty stream. openForReadOrEmpty(Uri uri)160 private InputStream openForReadOrEmpty(Uri uri) throws IOException { 161 try { 162 return openContext.backend().openForRead(uri); 163 } catch (FileNotFoundException ex) { 164 return EMPTY_INPUTSTREAM; 165 } 166 } 167 } 168 } 169