• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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