summaryrefslogtreecommitdiff
path: root/java/com/google/android/libraries/mobiledatadownload/file/openers/StreamMutationOpener.java
blob: d26538f0a3ce261274eaa0a041b41b11d1544340 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
/*
 * 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;
      }
    }
  }
}