• 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.content.Context;
19 import android.util.Log;
20 import com.google.android.libraries.mobiledatadownload.file.OpenContext;
21 import com.google.android.libraries.mobiledatadownload.file.Opener;
22 import com.google.android.libraries.mobiledatadownload.file.common.FileConvertible;
23 import com.google.android.libraries.mobiledatadownload.file.common.ReleasableResource;
24 import com.google.android.libraries.mobiledatadownload.file.openers.WriteFileOpener.FileCloser;
25 import com.google.errorprone.annotations.CanIgnoreReturnValue;
26 import java.io.Closeable;
27 import java.io.File;
28 import java.io.FileInputStream;
29 import java.io.FileOutputStream;
30 import java.io.IOException;
31 import java.io.OutputStream;
32 import java.util.concurrent.ExecutorService;
33 import java.util.concurrent.Future;
34 import java.util.concurrent.atomic.AtomicInteger;
35 import javax.annotation.Nullable;
36 
37 /**
38  * Opener for writing data to a {@link java.io.File} object. Depending on the backend, this may work
39  * one of three ways,
40  *
41  * <ol>
42  *   <li>The simple posix path.
43  *   <li>A /proc/self/fd/ path referring to a file descriptor for the original file.
44  *   <li>A path to a FIFO (named pipe) to which data can be written.
45  * </ol>
46  *
47  * Note that the third option is disabled by default, and must be turned on with {@link
48  * #withFallbackToPipeUsingExecutor}.
49  *
50  * <p>Usage: <code>
51  * try (WriteFileOpener.FileCloser closer =
52  *     storage.open(
53  *         uri, WriteFileOpener.create().withFallbackToPipeUsingExecutor(executor, context))) {
54  *   // Write to closer.file()
55  * }
56  * </code>
57  */
58 public final class WriteFileOpener implements Opener<FileCloser> {
59 
60   private static final String TAG = "WriteFileOpener";
61   private static final int STREAM_BUFFER_SIZE = 4096;
62   private static final AtomicInteger FIFO_COUNTER = new AtomicInteger();
63 
64   /** A file, closeable pair. */
65   public interface FileCloser extends Closeable {
file()66     File file();
67   }
68 
69   /** A FileCloser that contains a stream. */
70   private static class StreamFileCloser implements FileCloser {
71 
72     private final File file;
73     private final OutputStream stream;
74 
StreamFileCloser(File file, OutputStream stream)75     StreamFileCloser(File file, OutputStream stream) {
76       this.file = file;
77       this.stream = stream;
78     }
79 
80     @Override
file()81     public File file() {
82       return file;
83     }
84 
85     @Override
close()86     public void close() throws IOException {
87       stream.close();
88     }
89   }
90 
91   /** A FileCloser that contains a named pipe and a future to the thread pumping data through it. */
92   private static class PipeFileCloser implements FileCloser {
93 
94     private final File fifo;
95     private final Future<Throwable> pumpFuture;
96 
PipeFileCloser(File fifo, Future<Throwable> pumpFuture)97     PipeFileCloser(File fifo, Future<Throwable> pumpFuture) {
98       this.fifo = fifo;
99       this.pumpFuture = pumpFuture;
100     }
101 
102     @Override
file()103     public File file() {
104       return fifo;
105     }
106 
107     /**
108      * Closes the wrapped file and any associated system resources. This method will block on system
109      * IO if the file is piped and there is remaining data to be written to the stream.
110      *
111      * @throws IOException
112      */
113     @Override
close()114     public void close() throws IOException {
115       // If the pipe's write-side was never opened, open it in order to unblock the pump thread.
116       // Otherwise, this is harmless to the existing stream.
117       try (FileOutputStream unused = new FileOutputStream(fifo)) {
118         // Do nothing.
119       } catch (IOException e) {
120         Log.w(TAG, "close() threw exception when trying to unblock pump", e);
121       } finally {
122         fifo.delete();
123       }
124       Pipes.getAndPropagateAsIOException(pumpFuture);
125     }
126   }
127 
128   @Nullable private ExecutorService executor;
129   @Nullable private Context context;
130 
WriteFileOpener()131   private WriteFileOpener() {}
132 
create()133   public static WriteFileOpener create() {
134     return new WriteFileOpener();
135   }
136 
137   /**
138    * If enabled, still try to return a raw file path but, if that fails, return a FIFO (aka named
139    * pipe) to which the data can be written as a stream. Raw file paths are not available if there
140    * are any transforms installed; if there are any monitors installed; or if the backend lacks such
141    * support.
142    *
143    * <p>The caller MUST close the returned closeable in order to avoid a possible thread leak.
144    *
145    * <p>WARNING: FIFOs require SDK level 21+ (Lollipop). If the raw file path is unavailable and the
146    * current SDK level is insufficient for FIFOs, the fallback will fail (throw IOException).
147    *
148    * @param executor Executor that pumps data.
149    * @param context Android context for the root directory where fifos are stored.
150    * @return This opener.
151    */
152   @CanIgnoreReturnValue
withFallbackToPipeUsingExecutor( ExecutorService executor, Context context)153   public WriteFileOpener withFallbackToPipeUsingExecutor(
154       ExecutorService executor, Context context) {
155     this.executor = executor;
156     this.context = context;
157     return this;
158   }
159 
160   @Override
open(OpenContext openContext)161   public FileCloser open(OpenContext openContext) throws IOException {
162     try (ReleasableResource<OutputStream> out =
163         ReleasableResource.create(WriteStreamOpener.create().open(openContext))) {
164       if (out.get() instanceof FileConvertible) {
165         File file = ((FileConvertible) out.get()).toFile();
166         return new StreamFileCloser(file, out.release());
167       }
168       if (executor != null) {
169         return pipeFromFile(out.release());
170       }
171       throw new IOException("Not convertible and fallback to pipe is disabled.");
172     }
173   }
174 
pipeFromFile(OutputStream out)175   private FileCloser pipeFromFile(OutputStream out) throws IOException {
176     File fifo = Pipes.makeFifo(context.getCacheDir(), TAG, FIFO_COUNTER);
177     Future<Throwable> future =
178         executor.submit(
179             () -> {
180               try (FileInputStream in = new FileInputStream(fifo)) {
181                 // In order to reach this point, writer must have opened the FIFO, so it's ok
182                 // to delete it.
183                 fifo.delete();
184                 byte[] tmp = new byte[STREAM_BUFFER_SIZE];
185                 try {
186                   int len;
187                   while ((len = in.read(tmp)) != -1) {
188                     out.write(tmp, 0, len);
189                   }
190                   out.flush();
191                 } finally {
192                   out.close();
193                 }
194               } catch (IOException e) {
195                 Log.w(TAG, "pump", e);
196                 return e;
197               } catch (Throwable t) {
198                 Log.e(TAG, "pump", t);
199                 return t;
200               }
201               return null;
202             });
203     return new PipeFileCloser(fifo, future);
204   }
205 }
206