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