1 /* 2 * Copyright (C) 2020 Square, Inc. 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 okio 17 18 import kotlin.jvm.JvmName 19 20 /** 21 * A [FileSystem] that forwards calls to another, intended for subclassing. 22 * 23 * ### Fault Injection 24 * 25 * You can use this to deterministically trigger file system failures in tests. This is useful to 26 * confirm that your program behaves correctly even if its file system operations fail. For example, 27 * this subclass fails every access of files named `unlucky.txt`: 28 * 29 * ``` 30 * val faultyFileSystem = object : ForwardingFileSystem(FileSystem.SYSTEM) { 31 * override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path { 32 * if (path.name == "unlucky.txt") throw IOException("synthetic failure!") 33 * return path 34 * } 35 * } 36 * ``` 37 * 38 * You can fail specific operations by overriding them directly: 39 * 40 * ``` 41 * val faultyFileSystem = object : ForwardingFileSystem(FileSystem.SYSTEM) { 42 * override fun delete(path: Path) { 43 * throw IOException("synthetic failure!") 44 * } 45 * } 46 * ``` 47 * 48 * ### Observability 49 * 50 * You can extend this to verify which files your program accesses. This is a testing file system 51 * that records accesses as they happen: 52 * 53 * ``` 54 * class LoggingFileSystem : ForwardingFileSystem(FileSystem.SYSTEM) { 55 * val log = mutableListOf<String>() 56 * 57 * override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path { 58 * log += "$functionName($parameterName=$path)" 59 * return path 60 * } 61 * } 62 * ``` 63 * 64 * This makes it easy for tests to assert exactly which files were accessed. 65 * 66 * ``` 67 * @Test 68 * fun testMergeJsonReports() { 69 * createSampleJsonReports() 70 * loggingFileSystem.log.clear() 71 * 72 * mergeJsonReports() 73 * 74 * assertThat(loggingFileSystem.log).containsExactly( 75 * "list(dir=json_reports)", 76 * "source(file=json_reports/2020-10.json)", 77 * "source(file=json_reports/2020-12.json)", 78 * "source(file=json_reports/2020-11.json)", 79 * "sink(file=json_reports/2020-all.json)" 80 * ) 81 * } 82 * ``` 83 * 84 * ### Transformations 85 * 86 * Subclasses can transform file names and content. 87 * 88 * For example, your program may be written to operate on a well-known directory like `/etc/` or 89 * `/System`. You can rewrite paths to make such operations safer to test. 90 * 91 * You may also transform file content to apply application-layer encryption or compression. This 92 * is particularly useful in situations where it's difficult or impossible to enable those features 93 * in the underlying file system. 94 * 95 * ### Abstract Functions Only 96 * 97 * Some file system functions like [copy] are implemented by using other features. These are the 98 * non-abstract functions in the [FileSystem] interface. 99 * 100 * **This class forwards only the abstract functions;** non-abstract functions delegate to the 101 * other functions of this class. If desired, subclasses may override non-abstract functions to 102 * forward them. 103 */ 104 abstract class ForwardingFileSystem( 105 /** [FileSystem] to which this instance is delegating. */ 106 @get:JvmName("delegate") 107 val delegate: FileSystem, 108 ) : FileSystem() { 109 110 /** 111 * Invoked each time a path is passed as a parameter to this file system. This returns the path to 112 * pass to [delegate], which should be [path] itself or a path on [delegate] that corresponds to 113 * it. 114 * 115 * Subclasses may override this to log accesses, fail on unexpected accesses, or map paths across 116 * file systems. 117 * 118 * The base implementation returns [path]. 119 * 120 * Note that this function will be called twice for calls to [atomicMove]; once for the source 121 * file and once for the target file. 122 * 123 * @param path the path passed to any of the functions of this. 124 * @param functionName a string like "canonicalize", "metadataOrNull", or "appendingSink". 125 * @param parameterName a string like "path", "file", "source", or "target". 126 * @return the path to pass to [delegate] for the same parameter. 127 */ onPathParameternull128 open fun onPathParameter(path: Path, functionName: String, parameterName: String): Path = path 129 130 /** 131 * Invoked each time a path is returned by [delegate]. This returns the path to return to the 132 * caller, which should be [path] itself or a path on this that corresponds to it. 133 * 134 * Subclasses may override this to log accesses, fail on unexpected path accesses, or map 135 * directories or path names. 136 * 137 * The base implementation returns [path]. 138 * 139 * @param path the path returned by any of the functions of this. 140 * @param functionName a string like "canonicalize" or "list". 141 * @return the path to return to the caller. 142 */ 143 open fun onPathResult(path: Path, functionName: String): Path = path 144 145 @Throws(IOException::class) 146 override fun canonicalize(path: Path): Path { 147 val path = onPathParameter(path, "canonicalize", "path") 148 val result = delegate.canonicalize(path) 149 return onPathResult(result, "canonicalize") 150 } 151 152 @Throws(IOException::class) metadataOrNullnull153 override fun metadataOrNull(path: Path): FileMetadata? { 154 val path = onPathParameter(path, "metadataOrNull", "path") 155 val metadataOrNull = delegate.metadataOrNull(path) ?: return null 156 if (metadataOrNull.symlinkTarget == null) return metadataOrNull 157 158 val symlinkTarget = onPathResult(metadataOrNull.symlinkTarget, "metadataOrNull") 159 return metadataOrNull.copy(symlinkTarget = symlinkTarget) 160 } 161 162 @Throws(IOException::class) listnull163 override fun list(dir: Path): List<Path> { 164 val dir = onPathParameter(dir, "list", "dir") 165 val result = delegate.list(dir) 166 val paths = result.mapTo(mutableListOf()) { onPathResult(it, "list") } 167 paths.sort() 168 return paths 169 } 170 listOrNullnull171 override fun listOrNull(dir: Path): List<Path>? { 172 val dir = onPathParameter(dir, "listOrNull", "dir") 173 val result = delegate.listOrNull(dir) ?: return null 174 val paths = result.mapTo(mutableListOf()) { onPathResult(it, "listOrNull") } 175 paths.sort() 176 return paths 177 } 178 listRecursivelynull179 override fun listRecursively(dir: Path, followSymlinks: Boolean): Sequence<Path> { 180 val dir = onPathParameter(dir, "listRecursively", "dir") 181 val result = delegate.listRecursively(dir, followSymlinks) 182 return result.map { onPathResult(it, "listRecursively") } 183 } 184 185 @Throws(IOException::class) openReadOnlynull186 override fun openReadOnly(file: Path): FileHandle { 187 val file = onPathParameter(file, "openReadOnly", "file") 188 return delegate.openReadOnly(file) 189 } 190 191 @Throws(IOException::class) openReadWritenull192 override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle { 193 val file = onPathParameter(file, "openReadWrite", "file") 194 return delegate.openReadWrite(file, mustCreate, mustExist) 195 } 196 197 @Throws(IOException::class) sourcenull198 override fun source(file: Path): Source { 199 val file = onPathParameter(file, "source", "file") 200 return delegate.source(file) 201 } 202 203 @Throws(IOException::class) sinknull204 override fun sink(file: Path, mustCreate: Boolean): Sink { 205 val file = onPathParameter(file, "sink", "file") 206 return delegate.sink(file, mustCreate) 207 } 208 209 @Throws(IOException::class) appendingSinknull210 override fun appendingSink(file: Path, mustExist: Boolean): Sink { 211 val file = onPathParameter(file, "appendingSink", "file") 212 return delegate.appendingSink(file, mustExist) 213 } 214 215 @Throws(IOException::class) createDirectorynull216 override fun createDirectory(dir: Path, mustCreate: Boolean) { 217 val dir = onPathParameter(dir, "createDirectory", "dir") 218 delegate.createDirectory(dir, mustCreate) 219 } 220 221 @Throws(IOException::class) atomicMovenull222 override fun atomicMove(source: Path, target: Path) { 223 val source = onPathParameter(source, "atomicMove", "source") 224 val target = onPathParameter(target, "atomicMove", "target") 225 delegate.atomicMove(source, target) 226 } 227 228 @Throws(IOException::class) deletenull229 override fun delete(path: Path, mustExist: Boolean) { 230 val path = onPathParameter(path, "delete", "path") 231 delegate.delete(path, mustExist) 232 } 233 234 @Throws(IOException::class) createSymlinknull235 override fun createSymlink(source: Path, target: Path) { 236 val source = onPathParameter(source, "createSymlink", "source") 237 val target = onPathParameter(target, "createSymlink", "target") 238 delegate.createSymlink(source, target) 239 } 240 toStringnull241 override fun toString() = "${this::class.simpleName}($delegate)" 242 } 243