mirror of
https://github.com/square/okhttp.git
synced 2025-08-07 12:42:57 +03:00
Okio Filesystem (#6500)
* Testing okio * Working tests * Working tests * Working tests * Working tests * okio 3 * Fix dependencies * File system * Cleanup * Cleanup * Cleanup * Cache fixes * Cache fixes * Review comments * Cleanup * Cleanup * Build fixes
This commit is contained in:
12
build.gradle
12
build.gradle
@@ -16,7 +16,7 @@ buildscript {
|
|||||||
'junit5': '5.7.0',
|
'junit5': '5.7.0',
|
||||||
'kotlin': '1.4.30',
|
'kotlin': '1.4.30',
|
||||||
'moshi': '1.11.0',
|
'moshi': '1.11.0',
|
||||||
'okio': '2.9.0',
|
'okio': '3.0.0-alpha.1',
|
||||||
'ktlint': '0.38.0',
|
'ktlint': '0.38.0',
|
||||||
'picocli': '4.5.1',
|
'picocli': '4.5.1',
|
||||||
'openjsse': '1.1.0',
|
'openjsse': '1.1.0',
|
||||||
@@ -50,6 +50,7 @@ buildscript {
|
|||||||
'moshi': "com.squareup.moshi:moshi:${versions.moshi}",
|
'moshi': "com.squareup.moshi:moshi:${versions.moshi}",
|
||||||
'moshiKotlin': "com.squareup.moshi:moshi-kotlin-codegen:${versions.moshi}",
|
'moshiKotlin': "com.squareup.moshi:moshi-kotlin-codegen:${versions.moshi}",
|
||||||
'okio': "com.squareup.okio:okio:${versions.okio}",
|
'okio': "com.squareup.okio:okio:${versions.okio}",
|
||||||
|
'okioFakeFileSystem': "com.squareup.okio:okio-fakefilesystem:${versions.okio}",
|
||||||
'openjsse': "org.openjsse:openjsse:${versions.openjsse}",
|
'openjsse': "org.openjsse:openjsse:${versions.openjsse}",
|
||||||
'bnd': "biz.aQute.bnd:biz.aQute.bnd.gradle:${versions.bnd}",
|
'bnd': "biz.aQute.bnd:biz.aQute.bnd.gradle:${versions.bnd}",
|
||||||
'bndResolve': "biz.aQute.bnd:biz.aQute.resolve:${versions.bnd}",
|
'bndResolve': "biz.aQute.bnd:biz.aQute.resolve:${versions.bnd}",
|
||||||
@@ -110,6 +111,13 @@ allprojects {
|
|||||||
project.ext.artifactId = rootProject.ext.publishedArtifactId(project)
|
project.ext.artifactId = rootProject.ext.publishedArtifactId(project)
|
||||||
version = '5.0.0-SNAPSHOT'
|
version = '5.0.0-SNAPSHOT'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven { url 'https://dl.bintray.com/kotlin/dokka' }
|
||||||
|
maven { url 'https://kotlin.bintray.com/kotlinx/' }
|
||||||
|
google()
|
||||||
|
}
|
||||||
|
|
||||||
task downloadDependencies() {
|
task downloadDependencies() {
|
||||||
description 'Download all dependencies to the Gradle cache'
|
description 'Download all dependencies to the Gradle cache'
|
||||||
doLast {
|
doLast {
|
||||||
@@ -358,7 +366,7 @@ def alpnBootVersion() {
|
|||||||
return alpnBootVersionForPatchVersion(javaVersion, patchVersion)
|
return alpnBootVersionForPatchVersion(javaVersion, patchVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
def alpnBootVersionForPatchVersion(String javaVersion, int patchVersion) {
|
static def alpnBootVersionForPatchVersion(String javaVersion, int patchVersion) {
|
||||||
// https://www.eclipse.org/jetty/documentation/current/alpn-chapter.html#alpn-versions
|
// https://www.eclipse.org/jetty/documentation/current/alpn-chapter.html#alpn-versions
|
||||||
switch (patchVersion) {
|
switch (patchVersion) {
|
||||||
case 0..24:
|
case 0..24:
|
||||||
|
@@ -9,6 +9,7 @@ dependencies {
|
|||||||
implementation deps.junit5Api
|
implementation deps.junit5Api
|
||||||
implementation deps.junit5JupiterEngine
|
implementation deps.junit5JupiterEngine
|
||||||
implementation deps.junitPlatformConsole
|
implementation deps.junitPlatformConsole
|
||||||
|
implementation deps.okioFakeFileSystem
|
||||||
|
|
||||||
implementation project(':okhttp')
|
implementation project(':okhttp')
|
||||||
implementation project(':okhttp-brotli')
|
implementation project(':okhttp-brotli')
|
||||||
|
@@ -1,116 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Square, Inc.
|
|
||||||
*
|
|
||||||
* 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 okhttp3.internal.io
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.IdentityHashMap
|
|
||||||
import okio.Buffer
|
|
||||||
import okio.ForwardingSink
|
|
||||||
import okio.ForwardingSource
|
|
||||||
import okio.Sink
|
|
||||||
import okio.Source
|
|
||||||
import okhttp3.TestUtil.isDescendentOf
|
|
||||||
import org.junit.jupiter.api.extension.AfterEachCallback
|
|
||||||
import org.junit.jupiter.api.extension.ExtensionContext
|
|
||||||
|
|
||||||
/** A simple file system where all files are held in memory. Not safe for concurrent use. */
|
|
||||||
class InMemoryFileSystem : FileSystem, AfterEachCallback {
|
|
||||||
val files = mutableMapOf<File, Buffer>()
|
|
||||||
private val openSources = IdentityHashMap<Source, File>()
|
|
||||||
private val openSinks = IdentityHashMap<Sink, File>()
|
|
||||||
|
|
||||||
override fun afterEach(context: ExtensionContext?) {
|
|
||||||
ensureResourcesClosed()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ensureResourcesClosed() {
|
|
||||||
val openResources = mutableListOf<String>()
|
|
||||||
for (file in openSources.values) {
|
|
||||||
openResources.add("Source for $file")
|
|
||||||
}
|
|
||||||
for (file in openSinks.values) {
|
|
||||||
openResources.add("Sink for $file")
|
|
||||||
}
|
|
||||||
check(openResources.isEmpty()) {
|
|
||||||
"Resources acquired but not closed:\n * ${openResources.joinToString(separator = "\n * ")}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
override fun source(file: File): Source {
|
|
||||||
val result = files[file] ?: throw FileNotFoundException()
|
|
||||||
val source: Source = result.clone()
|
|
||||||
openSources[source] = file
|
|
||||||
return object : ForwardingSource(source) {
|
|
||||||
override fun close() {
|
|
||||||
openSources.remove(source)
|
|
||||||
super.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
override fun sink(file: File) = sink(file, false)
|
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
override fun appendingSink(file: File) = sink(file, true)
|
|
||||||
|
|
||||||
private fun sink(file: File, appending: Boolean): Sink {
|
|
||||||
var result: Buffer? = null
|
|
||||||
if (appending) {
|
|
||||||
result = files[file]
|
|
||||||
}
|
|
||||||
if (result == null) {
|
|
||||||
result = Buffer()
|
|
||||||
}
|
|
||||||
files[file] = result
|
|
||||||
val sink: Sink = result
|
|
||||||
openSinks[sink] = file
|
|
||||||
return object : ForwardingSink(sink) {
|
|
||||||
override fun close() {
|
|
||||||
openSinks.remove(sink)
|
|
||||||
super.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun delete(file: File) {
|
|
||||||
files.remove(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun exists(file: File) = files.containsKey(file)
|
|
||||||
|
|
||||||
override fun size(file: File) = files[file]?.size ?: 0L
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun rename(from: File, to: File) {
|
|
||||||
files[to] = files.remove(from) ?: throw FileNotFoundException()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun deleteContents(directory: File) {
|
|
||||||
val i = files.keys.iterator()
|
|
||||||
while (i.hasNext()) {
|
|
||||||
val file = i.next()
|
|
||||||
if (file.isDescendentOf(directory)) i.remove()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString() = "InMemoryFileSystem"
|
|
||||||
}
|
|
@@ -1,104 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2020 Square, Inc.
|
|
||||||
*
|
|
||||||
* 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 okhttp3.internal.io
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.util.Collections
|
|
||||||
import okhttp3.TestUtil.isDescendentOf
|
|
||||||
import okio.ForwardingSink
|
|
||||||
import okio.ForwardingSource
|
|
||||||
import okio.IOException
|
|
||||||
import okio.Sink
|
|
||||||
import okio.Source
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emulate Windows file system limitations on any file system. In particular, Windows will throw an
|
|
||||||
* [IOException] when asked to delete or rename an open file.
|
|
||||||
*/
|
|
||||||
class WindowsFileSystem(val delegate: FileSystem) : FileSystem {
|
|
||||||
/** Guarded by itself. */
|
|
||||||
private val openFiles = Collections.synchronizedList(mutableListOf<File>())
|
|
||||||
|
|
||||||
override fun source(file: File): Source = FileSource(file, delegate.source(file))
|
|
||||||
|
|
||||||
override fun sink(file: File): Sink = FileSink(file, delegate.sink(file))
|
|
||||||
|
|
||||||
override fun appendingSink(file: File): Sink = FileSink(file, delegate.appendingSink(file))
|
|
||||||
|
|
||||||
override fun delete(file: File) {
|
|
||||||
val fileOpen = file in openFiles
|
|
||||||
if (fileOpen) throw IOException("file is open $file")
|
|
||||||
delegate.delete(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun exists(file: File) = delegate.exists(file)
|
|
||||||
|
|
||||||
override fun size(file: File) = delegate.size(file)
|
|
||||||
|
|
||||||
override fun rename(from: File, to: File) {
|
|
||||||
val fromOpen = from in openFiles
|
|
||||||
if (fromOpen) throw IOException("file is open $from")
|
|
||||||
|
|
||||||
val toOpen = to in openFiles
|
|
||||||
if (toOpen) throw IOException("file is open $to")
|
|
||||||
|
|
||||||
delegate.rename(from, to)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deleteContents(directory: File) {
|
|
||||||
val openChild = synchronized(openFiles) {
|
|
||||||
openFiles.firstOrNull { it.isDescendentOf(directory) }
|
|
||||||
}
|
|
||||||
if (openChild != null) throw IOException("file is open $openChild")
|
|
||||||
delegate.deleteContents(directory)
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class FileSink(val file: File, delegate: Sink) : ForwardingSink(delegate) {
|
|
||||||
var closed = false
|
|
||||||
|
|
||||||
init {
|
|
||||||
openFiles += file
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
if (!closed) {
|
|
||||||
closed = true
|
|
||||||
val removed = openFiles.remove(file)
|
|
||||||
check(removed)
|
|
||||||
}
|
|
||||||
delegate.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class FileSource(val file: File, delegate: Source) : ForwardingSource(delegate) {
|
|
||||||
var closed = false
|
|
||||||
|
|
||||||
init {
|
|
||||||
openFiles += file
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
if (!closed) {
|
|
||||||
closed = true
|
|
||||||
val removed = openFiles.remove(file)
|
|
||||||
check(removed)
|
|
||||||
}
|
|
||||||
delegate.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString() = "$delegate for Windows™"
|
|
||||||
}
|
|
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Square, Inc.
|
||||||
|
*
|
||||||
|
* 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 okhttp3.okio
|
||||||
|
|
||||||
|
import okio.ExperimentalFileSystem
|
||||||
|
import okio.FileSystem
|
||||||
|
import okio.ForwardingFileSystem
|
||||||
|
import okio.Path
|
||||||
|
import okio.Sink
|
||||||
|
import okio.Source
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFileSystem::class)
|
||||||
|
class LoggingFilesystem(fileSystem: FileSystem) : ForwardingFileSystem(fileSystem) {
|
||||||
|
fun log(line: String) {
|
||||||
|
println(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun appendingSink(file: Path): Sink {
|
||||||
|
log("appendingSink($file)")
|
||||||
|
|
||||||
|
return super.appendingSink(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun atomicMove(source: Path, target: Path) {
|
||||||
|
log("atomicMove($source, $target)")
|
||||||
|
|
||||||
|
super.atomicMove(source, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createDirectory(dir: Path) {
|
||||||
|
log("createDirectory($dir)")
|
||||||
|
|
||||||
|
super.createDirectory(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(path: Path) {
|
||||||
|
log("delete($path)")
|
||||||
|
|
||||||
|
super.delete(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sink(path: Path): Sink {
|
||||||
|
log("sink($path)")
|
||||||
|
|
||||||
|
return super.sink(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun source(file: Path): Source {
|
||||||
|
log("source($file)")
|
||||||
|
|
||||||
|
return super.source(file)
|
||||||
|
}
|
||||||
|
}
|
@@ -82,6 +82,7 @@ dependencies {
|
|||||||
testImplementation project(':okhttp-brotli')
|
testImplementation project(':okhttp-brotli')
|
||||||
testImplementation project(':okhttp-dnsoverhttps')
|
testImplementation project(':okhttp-dnsoverhttps')
|
||||||
testImplementation project(':okhttp-sse')
|
testImplementation project(':okhttp-sse')
|
||||||
|
testImplementation deps.okioFakeFileSystem
|
||||||
testImplementation deps.conscrypt
|
testImplementation deps.conscrypt
|
||||||
testImplementation deps.junit
|
testImplementation deps.junit
|
||||||
testImplementation deps.junit5Api
|
testImplementation deps.junit5Api
|
||||||
|
@@ -25,7 +25,6 @@ import okhttp3.internal.closeQuietly
|
|||||||
import okhttp3.internal.concurrent.TaskRunner
|
import okhttp3.internal.concurrent.TaskRunner
|
||||||
import okhttp3.internal.http.HttpMethod
|
import okhttp3.internal.http.HttpMethod
|
||||||
import okhttp3.internal.http.StatusLine
|
import okhttp3.internal.http.StatusLine
|
||||||
import okhttp3.internal.io.FileSystem
|
|
||||||
import okhttp3.internal.platform.Platform
|
import okhttp3.internal.platform.Platform
|
||||||
import okhttp3.internal.platform.Platform.Companion.WARN
|
import okhttp3.internal.platform.Platform.Companion.WARN
|
||||||
import okhttp3.internal.toLongOrDefault
|
import okhttp3.internal.toLongOrDefault
|
||||||
@@ -35,8 +34,12 @@ import okio.BufferedSource
|
|||||||
import okio.ByteString.Companion.decodeBase64
|
import okio.ByteString.Companion.decodeBase64
|
||||||
import okio.ByteString.Companion.encodeUtf8
|
import okio.ByteString.Companion.encodeUtf8
|
||||||
import okio.ByteString.Companion.toByteString
|
import okio.ByteString.Companion.toByteString
|
||||||
|
import okio.ExperimentalFileSystem
|
||||||
|
import okio.FileSystem
|
||||||
import okio.ForwardingSink
|
import okio.ForwardingSink
|
||||||
import okio.ForwardingSource
|
import okio.ForwardingSource
|
||||||
|
import okio.Path
|
||||||
|
import okio.Path.Companion.toOkioPath
|
||||||
import okio.Sink
|
import okio.Sink
|
||||||
import okio.Source
|
import okio.Source
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
@@ -50,14 +53,6 @@ import java.security.cert.CertificateException
|
|||||||
import java.security.cert.CertificateFactory
|
import java.security.cert.CertificateFactory
|
||||||
import java.util.NoSuchElementException
|
import java.util.NoSuchElementException
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
import kotlin.collections.List
|
|
||||||
import kotlin.collections.MutableIterator
|
|
||||||
import kotlin.collections.MutableSet
|
|
||||||
import kotlin.collections.Set
|
|
||||||
import kotlin.collections.emptyList
|
|
||||||
import kotlin.collections.emptySet
|
|
||||||
import kotlin.collections.none
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Caches HTTP and HTTPS responses to the filesystem so they may be reused, saving time and
|
* Caches HTTP and HTTPS responses to the filesystem so they may be reused, saving time and
|
||||||
@@ -148,18 +143,20 @@ import kotlin.collections.none
|
|||||||
*
|
*
|
||||||
* [rfc_7234]: http://tools.ietf.org/html/rfc7234
|
* [rfc_7234]: http://tools.ietf.org/html/rfc7234
|
||||||
*/
|
*/
|
||||||
class Cache internal constructor(
|
@OptIn(ExperimentalFileSystem::class)
|
||||||
directory: File,
|
class Cache
|
||||||
|
internal constructor(
|
||||||
|
directory: Path,
|
||||||
maxSize: Long,
|
maxSize: Long,
|
||||||
fileSystem: FileSystem
|
fileSystem: FileSystem
|
||||||
) : Closeable, Flushable {
|
) : Closeable, Flushable {
|
||||||
internal val cache = DiskLruCache(
|
internal val cache = DiskLruCache(
|
||||||
fileSystem = fileSystem,
|
fileSystem = fileSystem,
|
||||||
directory = directory,
|
directory = directory,
|
||||||
appVersion = VERSION,
|
appVersion = VERSION,
|
||||||
valueCount = ENTRY_COUNT,
|
valueCount = ENTRY_COUNT,
|
||||||
maxSize = maxSize,
|
maxSize = maxSize,
|
||||||
taskRunner = TaskRunner.INSTANCE
|
taskRunner = TaskRunner.INSTANCE
|
||||||
)
|
)
|
||||||
|
|
||||||
// read and write statistics, all guarded by 'this'.
|
// read and write statistics, all guarded by 'this'.
|
||||||
@@ -173,7 +170,10 @@ class Cache internal constructor(
|
|||||||
get() = cache.isClosed()
|
get() = cache.isClosed()
|
||||||
|
|
||||||
/** Create a cache of at most [maxSize] bytes in [directory]. */
|
/** Create a cache of at most [maxSize] bytes in [directory]. */
|
||||||
constructor(directory: File, maxSize: Long) : this(directory, maxSize, FileSystem.SYSTEM)
|
@OptIn(ExperimentalFileSystem::class)
|
||||||
|
constructor(directory: File, maxSize: Long) : this(
|
||||||
|
directory.toOkioPath(), maxSize, FileSystem.SYSTEM
|
||||||
|
)
|
||||||
|
|
||||||
internal fun get(request: Request): Response? {
|
internal fun get(request: Request): Response? {
|
||||||
val key = key(request.url)
|
val key = key(request.url)
|
||||||
@@ -365,14 +365,18 @@ class Cache internal constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@get:JvmName("directory") val directory: File
|
@get:JvmName("directory") val directory: File
|
||||||
|
get() = cache.directory.toFile()
|
||||||
|
|
||||||
|
@get:JvmName("directoryPath") val directoryPath: Path
|
||||||
get() = cache.directory
|
get() = cache.directory
|
||||||
|
|
||||||
@JvmName("-deprecated_directory")
|
@JvmName("-deprecated_directory")
|
||||||
@Deprecated(
|
@Deprecated(
|
||||||
message = "moved to val",
|
message = "moved to val",
|
||||||
replaceWith = ReplaceWith(expression = "directory"),
|
replaceWith = ReplaceWith(expression = "directory"),
|
||||||
level = DeprecationLevel.ERROR)
|
level = DeprecationLevel.ERROR
|
||||||
fun directory(): File = cache.directory
|
)
|
||||||
|
fun directory(): File = cache.directory.toFile()
|
||||||
|
|
||||||
@Synchronized internal fun trackResponse(cacheStrategy: CacheStrategy) {
|
@Synchronized internal fun trackResponse(cacheStrategy: CacheStrategy) {
|
||||||
requestCount++
|
requestCount++
|
||||||
@@ -575,27 +579,27 @@ class Cache internal constructor(
|
|||||||
sink.writeDecimalLong(varyHeaders.size.toLong()).writeByte('\n'.toInt())
|
sink.writeDecimalLong(varyHeaders.size.toLong()).writeByte('\n'.toInt())
|
||||||
for (i in 0 until varyHeaders.size) {
|
for (i in 0 until varyHeaders.size) {
|
||||||
sink.writeUtf8(varyHeaders.name(i))
|
sink.writeUtf8(varyHeaders.name(i))
|
||||||
.writeUtf8(": ")
|
.writeUtf8(": ")
|
||||||
.writeUtf8(varyHeaders.value(i))
|
.writeUtf8(varyHeaders.value(i))
|
||||||
.writeByte('\n'.toInt())
|
.writeByte('\n'.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
sink.writeUtf8(StatusLine(protocol, code, message).toString()).writeByte('\n'.toInt())
|
sink.writeUtf8(StatusLine(protocol, code, message).toString()).writeByte('\n'.toInt())
|
||||||
sink.writeDecimalLong((responseHeaders.size + 2).toLong()).writeByte('\n'.toInt())
|
sink.writeDecimalLong((responseHeaders.size + 2).toLong()).writeByte('\n'.toInt())
|
||||||
for (i in 0 until responseHeaders.size) {
|
for (i in 0 until responseHeaders.size) {
|
||||||
sink.writeUtf8(responseHeaders.name(i))
|
sink.writeUtf8(responseHeaders.name(i))
|
||||||
.writeUtf8(": ")
|
.writeUtf8(": ")
|
||||||
.writeUtf8(responseHeaders.value(i))
|
.writeUtf8(responseHeaders.value(i))
|
||||||
.writeByte('\n'.toInt())
|
.writeByte('\n'.toInt())
|
||||||
}
|
}
|
||||||
sink.writeUtf8(SENT_MILLIS)
|
sink.writeUtf8(SENT_MILLIS)
|
||||||
.writeUtf8(": ")
|
.writeUtf8(": ")
|
||||||
.writeDecimalLong(sentRequestMillis)
|
.writeDecimalLong(sentRequestMillis)
|
||||||
.writeByte('\n'.toInt())
|
.writeByte('\n'.toInt())
|
||||||
sink.writeUtf8(RECEIVED_MILLIS)
|
sink.writeUtf8(RECEIVED_MILLIS)
|
||||||
.writeUtf8(": ")
|
.writeUtf8(": ")
|
||||||
.writeDecimalLong(receivedResponseMillis)
|
.writeDecimalLong(receivedResponseMillis)
|
||||||
.writeByte('\n'.toInt())
|
.writeByte('\n'.toInt())
|
||||||
|
|
||||||
if (isHttps) {
|
if (isHttps) {
|
||||||
sink.writeByte('\n'.toInt())
|
sink.writeByte('\n'.toInt())
|
||||||
@@ -643,29 +647,29 @@ class Cache internal constructor(
|
|||||||
|
|
||||||
fun matches(request: Request, response: Response): Boolean {
|
fun matches(request: Request, response: Response): Boolean {
|
||||||
return url == request.url &&
|
return url == request.url &&
|
||||||
requestMethod == request.method &&
|
requestMethod == request.method &&
|
||||||
varyMatches(response, varyHeaders, request)
|
varyMatches(response, varyHeaders, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun response(snapshot: DiskLruCache.Snapshot): Response {
|
fun response(snapshot: DiskLruCache.Snapshot): Response {
|
||||||
val contentType = responseHeaders["Content-Type"]
|
val contentType = responseHeaders["Content-Type"]
|
||||||
val contentLength = responseHeaders["Content-Length"]
|
val contentLength = responseHeaders["Content-Length"]
|
||||||
val cacheRequest = Request.Builder()
|
val cacheRequest = Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.method(requestMethod, null)
|
.method(requestMethod, null)
|
||||||
.headers(varyHeaders)
|
.headers(varyHeaders)
|
||||||
.build()
|
.build()
|
||||||
return Response.Builder()
|
return Response.Builder()
|
||||||
.request(cacheRequest)
|
.request(cacheRequest)
|
||||||
.protocol(protocol)
|
.protocol(protocol)
|
||||||
.code(code)
|
.code(code)
|
||||||
.message(message)
|
.message(message)
|
||||||
.headers(responseHeaders)
|
.headers(responseHeaders)
|
||||||
.body(CacheResponseBody(snapshot, contentType, contentLength))
|
.body(CacheResponseBody(snapshot, contentType, contentLength))
|
||||||
.handshake(handshake)
|
.handshake(handshake)
|
||||||
.sentRequestAtMillis(sentRequestMillis)
|
.sentRequestAtMillis(sentRequestMillis)
|
||||||
.receivedResponseAtMillis(receivedResponseMillis)
|
.receivedResponseAtMillis(receivedResponseMillis)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@@ -21,10 +21,12 @@ import okhttp3.Response
|
|||||||
import okhttp3.internal.connection.Exchange
|
import okhttp3.internal.connection.Exchange
|
||||||
import okhttp3.internal.connection.RealCall
|
import okhttp3.internal.connection.RealCall
|
||||||
import okhttp3.internal.connection.RealConnection
|
import okhttp3.internal.connection.RealConnection
|
||||||
import okhttp3.internal.io.FileSystem
|
import okio.ExperimentalFileSystem
|
||||||
import java.io.File
|
import okio.FileSystem
|
||||||
|
import okio.Path
|
||||||
|
|
||||||
fun buildCache(file: File, maxSize: Long, fileSystem: FileSystem): Cache {
|
@OptIn(ExperimentalFileSystem::class)
|
||||||
|
fun buildCache(file: Path, maxSize: Long, fileSystem: FileSystem): Cache {
|
||||||
return Cache(file, maxSize, fileSystem)
|
return Cache(file, maxSize, fileSystem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -17,8 +17,27 @@
|
|||||||
|
|
||||||
package okhttp3.internal
|
package okhttp3.internal
|
||||||
|
|
||||||
|
import okhttp3.EventListener
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.Headers.Companion.headersOf
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.OkHttp
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
|
import okhttp3.internal.http2.Header
|
||||||
|
import okio.Buffer
|
||||||
|
import okio.BufferedSink
|
||||||
|
import okio.BufferedSource
|
||||||
|
import okio.ByteString.Companion.decodeHex
|
||||||
|
import okio.ExperimentalFileSystem
|
||||||
|
import okio.FileSystem
|
||||||
|
import okio.Options
|
||||||
|
import okio.Path
|
||||||
|
import okio.Source
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.io.File
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InterruptedIOException
|
import java.io.InterruptedIOException
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
@@ -38,24 +57,6 @@ import java.util.concurrent.ThreadFactory
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.text.Charsets.UTF_32BE
|
import kotlin.text.Charsets.UTF_32BE
|
||||||
import kotlin.text.Charsets.UTF_32LE
|
import kotlin.text.Charsets.UTF_32LE
|
||||||
import okhttp3.Call
|
|
||||||
import okhttp3.EventListener
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.Headers.Companion.headersOf
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.OkHttp
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
|
||||||
import okhttp3.internal.http2.Header
|
|
||||||
import okhttp3.internal.io.FileSystem
|
|
||||||
import okio.Buffer
|
|
||||||
import okio.BufferedSink
|
|
||||||
import okio.BufferedSource
|
|
||||||
import okio.ByteString.Companion.decodeHex
|
|
||||||
import okio.Options
|
|
||||||
import okio.Source
|
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val EMPTY_BYTE_ARRAY = ByteArray(0)
|
val EMPTY_BYTE_ARRAY = ByteArray(0)
|
||||||
@@ -530,7 +531,8 @@ fun ServerSocket.closeQuietly() {
|
|||||||
*
|
*
|
||||||
* @param file a file in the directory to check. This file shouldn't already exist!
|
* @param file a file in the directory to check. This file shouldn't already exist!
|
||||||
*/
|
*/
|
||||||
fun FileSystem.isCivilized(file: File): Boolean {
|
@OptIn(ExperimentalFileSystem::class)
|
||||||
|
fun FileSystem.isCivilized(file: Path): Boolean {
|
||||||
sink(file).use {
|
sink(file).use {
|
||||||
try {
|
try {
|
||||||
delete(file)
|
delete(file)
|
||||||
@@ -542,6 +544,45 @@ fun FileSystem.isCivilized(file: File): Boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Delete file we expect but don't require to exist. */
|
||||||
|
@OptIn(ExperimentalFileSystem::class)
|
||||||
|
fun FileSystem.deleteIfExists(path: Path) {
|
||||||
|
try {
|
||||||
|
delete(path)
|
||||||
|
} catch (fnfe: FileNotFoundException) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tolerant delete, try to clear as many files as possible even after a failure.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalFileSystem::class)
|
||||||
|
fun FileSystem.deleteContents(directory: Path) {
|
||||||
|
var exception: IOException? = null
|
||||||
|
val files = try {
|
||||||
|
list(directory)
|
||||||
|
} catch (fnfe: FileNotFoundException) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (file in files) {
|
||||||
|
try {
|
||||||
|
if (metadata(file).isDirectory) {
|
||||||
|
deleteContents(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(file)
|
||||||
|
} catch (ioe: IOException) {
|
||||||
|
if (exception == null) {
|
||||||
|
exception = ioe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exception != null) {
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Long.toHexString(): String = java.lang.Long.toHexString(this)
|
fun Long.toHexString(): String = java.lang.Long.toHexString(this)
|
||||||
|
|
||||||
fun Int.toHexString(): String = Integer.toHexString(this)
|
fun Int.toHexString(): String = Integer.toHexString(this)
|
||||||
|
@@ -20,16 +20,27 @@ import okhttp3.internal.cache.DiskLruCache.Editor
|
|||||||
import okhttp3.internal.closeQuietly
|
import okhttp3.internal.closeQuietly
|
||||||
import okhttp3.internal.concurrent.Task
|
import okhttp3.internal.concurrent.Task
|
||||||
import okhttp3.internal.concurrent.TaskRunner
|
import okhttp3.internal.concurrent.TaskRunner
|
||||||
import okhttp3.internal.io.FileSystem
|
import okhttp3.internal.deleteContents
|
||||||
|
import okhttp3.internal.deleteIfExists
|
||||||
import okhttp3.internal.isCivilized
|
import okhttp3.internal.isCivilized
|
||||||
import okhttp3.internal.okHttpName
|
import okhttp3.internal.okHttpName
|
||||||
import okhttp3.internal.platform.Platform
|
import okhttp3.internal.platform.Platform
|
||||||
import okhttp3.internal.platform.Platform.Companion.WARN
|
import okhttp3.internal.platform.Platform.Companion.WARN
|
||||||
import okio.*
|
import okio.BufferedSink
|
||||||
import java.io.*
|
import okio.ExperimentalFileSystem
|
||||||
|
import okio.FileNotFoundException
|
||||||
|
import okio.FileSystem
|
||||||
|
import okio.ForwardingFileSystem
|
||||||
|
import okio.ForwardingSource
|
||||||
|
import okio.Path
|
||||||
|
import okio.Sink
|
||||||
|
import okio.Source
|
||||||
|
import okio.blackholeSink
|
||||||
|
import okio.buffer
|
||||||
|
import java.io.Closeable
|
||||||
import java.io.EOFException
|
import java.io.EOFException
|
||||||
|
import java.io.Flushable
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A cache that uses a bounded amount of space on a filesystem. Each cache entry has a string key
|
* A cache that uses a bounded amount of space on a filesystem. Each cache entry has a string key
|
||||||
@@ -74,11 +85,12 @@ import java.util.*
|
|||||||
* @param valueCount the number of values per cache entry. Must be positive.
|
* @param valueCount the number of values per cache entry. Must be positive.
|
||||||
* @param maxSize the maximum number of bytes this cache should use to store.
|
* @param maxSize the maximum number of bytes this cache should use to store.
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalFileSystem::class)
|
||||||
class DiskLruCache(
|
class DiskLruCache(
|
||||||
internal val fileSystem: FileSystem,
|
fileSystem: FileSystem,
|
||||||
|
|
||||||
/** Returns the directory where this cache stores its data. */
|
/** Returns the directory where this cache stores its data. */
|
||||||
val directory: File,
|
val directory: Path,
|
||||||
|
|
||||||
private val appVersion: Int,
|
private val appVersion: Int,
|
||||||
|
|
||||||
@@ -90,6 +102,18 @@ class DiskLruCache(
|
|||||||
/** Used for asynchronous journal rebuilds. */
|
/** Used for asynchronous journal rebuilds. */
|
||||||
taskRunner: TaskRunner
|
taskRunner: TaskRunner
|
||||||
) : Closeable, Flushable {
|
) : Closeable, Flushable {
|
||||||
|
internal val fileSystem: FileSystem = object : ForwardingFileSystem(fileSystem) {
|
||||||
|
override fun sink(file: Path): Sink {
|
||||||
|
file.parent?.let {
|
||||||
|
// TODO from okhttp3.internal.io.FileSystem
|
||||||
|
if (!exists(it)) {
|
||||||
|
createDirectories(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.sink(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** The maximum number of bytes that this cache should use to store its data. */
|
/** The maximum number of bytes that this cache should use to store its data. */
|
||||||
@get:Synchronized @set:Synchronized var maxSize: Long = maxSize
|
@get:Synchronized @set:Synchronized var maxSize: Long = maxSize
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -139,9 +163,9 @@ class DiskLruCache(
|
|||||||
* compaction; that file should be deleted if it exists when the cache is opened.
|
* compaction; that file should be deleted if it exists when the cache is opened.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private val journalFile: File
|
private val journalFile: Path
|
||||||
private val journalFileTmp: File
|
private val journalFileTmp: Path
|
||||||
private val journalFileBackup: File
|
private val journalFileBackup: Path
|
||||||
private var size: Long = 0L
|
private var size: Long = 0L
|
||||||
private var journalWriter: BufferedSink? = null
|
private var journalWriter: BufferedSink? = null
|
||||||
internal val lruEntries = LinkedHashMap<String, Entry>(0, 0.75f, true)
|
internal val lruEntries = LinkedHashMap<String, Entry>(0, 0.75f, true)
|
||||||
@@ -195,9 +219,9 @@ class DiskLruCache(
|
|||||||
require(maxSize > 0L) { "maxSize <= 0" }
|
require(maxSize > 0L) { "maxSize <= 0" }
|
||||||
require(valueCount > 0) { "valueCount <= 0" }
|
require(valueCount > 0) { "valueCount <= 0" }
|
||||||
|
|
||||||
this.journalFile = File(directory, JOURNAL_FILE)
|
this.journalFile = directory / JOURNAL_FILE
|
||||||
this.journalFileTmp = File(directory, JOURNAL_FILE_TEMP)
|
this.journalFileTmp = directory / JOURNAL_FILE_TEMP
|
||||||
this.journalFileBackup = File(directory, JOURNAL_FILE_BACKUP)
|
this.journalFileBackup = directory / JOURNAL_FILE_BACKUP
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized @Throws(IOException::class)
|
@Synchronized @Throws(IOException::class)
|
||||||
@@ -214,7 +238,7 @@ class DiskLruCache(
|
|||||||
if (fileSystem.exists(journalFile)) {
|
if (fileSystem.exists(journalFile)) {
|
||||||
fileSystem.delete(journalFileBackup)
|
fileSystem.delete(journalFileBackup)
|
||||||
} else {
|
} else {
|
||||||
fileSystem.rename(journalFileBackup, journalFile)
|
fileSystem.atomicMove(journalFileBackup, journalFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,12 +274,12 @@ class DiskLruCache(
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun readJournal() {
|
private fun readJournal() {
|
||||||
fileSystem.source(journalFile).buffer().use { source ->
|
fileSystem.read(journalFile) {
|
||||||
val magic = source.readUtf8LineStrict()
|
val magic = readUtf8LineStrict()
|
||||||
val version = source.readUtf8LineStrict()
|
val version = readUtf8LineStrict()
|
||||||
val appVersionString = source.readUtf8LineStrict()
|
val appVersionString = readUtf8LineStrict()
|
||||||
val valueCountString = source.readUtf8LineStrict()
|
val valueCountString = readUtf8LineStrict()
|
||||||
val blank = source.readUtf8LineStrict()
|
val blank = readUtf8LineStrict()
|
||||||
|
|
||||||
if (MAGIC != magic ||
|
if (MAGIC != magic ||
|
||||||
VERSION_1 != version ||
|
VERSION_1 != version ||
|
||||||
@@ -269,7 +293,7 @@ class DiskLruCache(
|
|||||||
var lineCount = 0
|
var lineCount = 0
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
readJournalLine(source.readUtf8LineStrict())
|
readJournalLine(readUtf8LineStrict())
|
||||||
lineCount++
|
lineCount++
|
||||||
} catch (_: EOFException) {
|
} catch (_: EOFException) {
|
||||||
break // End of journal.
|
break // End of journal.
|
||||||
@@ -279,7 +303,7 @@ class DiskLruCache(
|
|||||||
redundantOpCount = lineCount - lruEntries.size
|
redundantOpCount = lineCount - lruEntries.size
|
||||||
|
|
||||||
// If we ended on a truncated line, rebuild the journal before appending to it.
|
// If we ended on a truncated line, rebuild the journal before appending to it.
|
||||||
if (!source.exhausted()) {
|
if (!exhausted()) {
|
||||||
rebuildJournal()
|
rebuildJournal()
|
||||||
} else {
|
} else {
|
||||||
journalWriter = newJournalWriter()
|
journalWriter = newJournalWriter()
|
||||||
@@ -348,7 +372,7 @@ class DiskLruCache(
|
|||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun processJournal() {
|
private fun processJournal() {
|
||||||
fileSystem.delete(journalFileTmp)
|
fileSystem.deleteIfExists(journalFileTmp)
|
||||||
val i = lruEntries.values.iterator()
|
val i = lruEntries.values.iterator()
|
||||||
while (i.hasNext()) {
|
while (i.hasNext()) {
|
||||||
val entry = i.next()
|
val entry = i.next()
|
||||||
@@ -359,8 +383,8 @@ class DiskLruCache(
|
|||||||
} else {
|
} else {
|
||||||
entry.currentEditor = null
|
entry.currentEditor = null
|
||||||
for (t in 0 until valueCount) {
|
for (t in 0 until valueCount) {
|
||||||
fileSystem.delete(entry.cleanFiles[t])
|
fileSystem.deleteIfExists(entry.cleanFiles[t])
|
||||||
fileSystem.delete(entry.dirtyFiles[t])
|
fileSystem.deleteIfExists(entry.dirtyFiles[t])
|
||||||
}
|
}
|
||||||
i.remove()
|
i.remove()
|
||||||
}
|
}
|
||||||
@@ -375,32 +399,34 @@ class DiskLruCache(
|
|||||||
internal fun rebuildJournal() {
|
internal fun rebuildJournal() {
|
||||||
journalWriter?.close()
|
journalWriter?.close()
|
||||||
|
|
||||||
fileSystem.sink(journalFileTmp).buffer().use { sink ->
|
fileSystem.write(journalFileTmp) {
|
||||||
sink.writeUtf8(MAGIC).writeByte('\n'.toInt())
|
writeUtf8(MAGIC).writeByte('\n'.toInt())
|
||||||
sink.writeUtf8(VERSION_1).writeByte('\n'.toInt())
|
writeUtf8(VERSION_1).writeByte('\n'.toInt())
|
||||||
sink.writeDecimalLong(appVersion.toLong()).writeByte('\n'.toInt())
|
writeDecimalLong(appVersion.toLong()).writeByte('\n'.toInt())
|
||||||
sink.writeDecimalLong(valueCount.toLong()).writeByte('\n'.toInt())
|
writeDecimalLong(valueCount.toLong()).writeByte('\n'.toInt())
|
||||||
sink.writeByte('\n'.toInt())
|
writeByte('\n'.toInt())
|
||||||
|
|
||||||
for (entry in lruEntries.values) {
|
for (entry in lruEntries.values) {
|
||||||
if (entry.currentEditor != null) {
|
if (entry.currentEditor != null) {
|
||||||
sink.writeUtf8(DIRTY).writeByte(' '.toInt())
|
writeUtf8(DIRTY).writeByte(' '.toInt())
|
||||||
sink.writeUtf8(entry.key)
|
writeUtf8(entry.key)
|
||||||
sink.writeByte('\n'.toInt())
|
writeByte('\n'.toInt())
|
||||||
} else {
|
} else {
|
||||||
sink.writeUtf8(CLEAN).writeByte(' '.toInt())
|
writeUtf8(CLEAN).writeByte(' '.toInt())
|
||||||
sink.writeUtf8(entry.key)
|
writeUtf8(entry.key)
|
||||||
entry.writeLengths(sink)
|
entry.writeLengths(this)
|
||||||
sink.writeByte('\n'.toInt())
|
writeByte('\n'.toInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileSystem.exists(journalFile)) {
|
if (fileSystem.exists(journalFile)) {
|
||||||
fileSystem.rename(journalFile, journalFileBackup)
|
fileSystem.atomicMove(journalFile, journalFileBackup)
|
||||||
|
fileSystem.atomicMove(journalFileTmp, journalFile)
|
||||||
|
fileSystem.deleteIfExists(journalFileBackup)
|
||||||
|
} else {
|
||||||
|
fileSystem.atomicMove(journalFileTmp, journalFile)
|
||||||
}
|
}
|
||||||
fileSystem.rename(journalFileTmp, journalFile)
|
|
||||||
fileSystem.delete(journalFileBackup)
|
|
||||||
|
|
||||||
journalWriter = newJournalWriter()
|
journalWriter = newJournalWriter()
|
||||||
hasJournalErrors = false
|
hasJournalErrors = false
|
||||||
@@ -519,14 +545,15 @@ class DiskLruCache(
|
|||||||
if (success && !entry.zombie) {
|
if (success && !entry.zombie) {
|
||||||
if (fileSystem.exists(dirty)) {
|
if (fileSystem.exists(dirty)) {
|
||||||
val clean = entry.cleanFiles[i]
|
val clean = entry.cleanFiles[i]
|
||||||
fileSystem.rename(dirty, clean)
|
fileSystem.atomicMove(dirty, clean)
|
||||||
val oldLength = entry.lengths[i]
|
val oldLength = entry.lengths[i]
|
||||||
val newLength = fileSystem.size(clean)
|
// TODO check null behaviour
|
||||||
|
val newLength = fileSystem.metadata(clean).size ?: 0
|
||||||
entry.lengths[i] = newLength
|
entry.lengths[i] = newLength
|
||||||
size = size - oldLength + newLength
|
size = size - oldLength + newLength
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fileSystem.delete(dirty)
|
fileSystem.deleteIfExists(dirty)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -613,7 +640,7 @@ class DiskLruCache(
|
|||||||
entry.currentEditor?.detach() // Prevent the edit from completing normally.
|
entry.currentEditor?.detach() // Prevent the edit from completing normally.
|
||||||
|
|
||||||
for (i in 0 until valueCount) {
|
for (i in 0 until valueCount) {
|
||||||
fileSystem.delete(entry.cleanFiles[i])
|
fileSystem.deleteIfExists(entry.cleanFiles[i])
|
||||||
size -= entry.lengths[i]
|
size -= entry.lengths[i]
|
||||||
entry.lengths[i] = 0
|
entry.lengths[i] = 0
|
||||||
}
|
}
|
||||||
@@ -916,8 +943,8 @@ class DiskLruCache(
|
|||||||
|
|
||||||
/** Lengths of this entry's files. */
|
/** Lengths of this entry's files. */
|
||||||
internal val lengths: LongArray = LongArray(valueCount)
|
internal val lengths: LongArray = LongArray(valueCount)
|
||||||
internal val cleanFiles = mutableListOf<File>()
|
internal val cleanFiles = mutableListOf<Path>()
|
||||||
internal val dirtyFiles = mutableListOf<File>()
|
internal val dirtyFiles = mutableListOf<Path>()
|
||||||
|
|
||||||
/** True if this entry has ever been published. */
|
/** True if this entry has ever been published. */
|
||||||
internal var readable: Boolean = false
|
internal var readable: Boolean = false
|
||||||
@@ -946,9 +973,9 @@ class DiskLruCache(
|
|||||||
val truncateTo = fileBuilder.length
|
val truncateTo = fileBuilder.length
|
||||||
for (i in 0 until valueCount) {
|
for (i in 0 until valueCount) {
|
||||||
fileBuilder.append(i)
|
fileBuilder.append(i)
|
||||||
cleanFiles += File(directory, fileBuilder.toString())
|
cleanFiles += directory / fileBuilder.toString()
|
||||||
fileBuilder.append(".tmp")
|
fileBuilder.append(".tmp")
|
||||||
dirtyFiles += File(directory, fileBuilder.toString())
|
dirtyFiles += directory / fileBuilder.toString()
|
||||||
fileBuilder.setLength(truncateTo)
|
fileBuilder.setLength(truncateTo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,149 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Square, Inc.
|
|
||||||
*
|
|
||||||
* 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 okhttp3.internal.io
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.IOException
|
|
||||||
import okio.Sink
|
|
||||||
import okio.Source
|
|
||||||
import okio.appendingSink
|
|
||||||
import okio.sink
|
|
||||||
import okio.source
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Access to read and write files on a hierarchical data store. Most callers should use the
|
|
||||||
* [SYSTEM] implementation, which uses the host machine's local file system. Alternate
|
|
||||||
* implementations may be used to inject faults (for testing) or to transform stored data (to add
|
|
||||||
* encryption, for example).
|
|
||||||
*
|
|
||||||
* All operations on a file system are racy. For example, guarding a call to [source] with
|
|
||||||
* [exists] does not guarantee that [FileNotFoundException] will not be thrown. The
|
|
||||||
* file may be moved between the two calls!
|
|
||||||
*
|
|
||||||
* This interface is less ambitious than [java.nio.file.FileSystem] introduced in Java 7.
|
|
||||||
* It lacks important features like file watching, metadata, permissions, and disk space
|
|
||||||
* information. In exchange for these limitations, this interface is easier to implement and works
|
|
||||||
* on all versions of Java and Android.
|
|
||||||
*/
|
|
||||||
interface FileSystem {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/** The host machine's local file system. */
|
|
||||||
@JvmField
|
|
||||||
val SYSTEM: FileSystem = SystemFileSystem()
|
|
||||||
private class SystemFileSystem : FileSystem {
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
override fun source(file: File): Source = file.source()
|
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
override fun sink(file: File): Sink {
|
|
||||||
return try {
|
|
||||||
file.sink()
|
|
||||||
} catch (_: FileNotFoundException) {
|
|
||||||
// Maybe the parent directory doesn't exist? Try creating it first.
|
|
||||||
file.parentFile.mkdirs()
|
|
||||||
file.sink()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
override fun appendingSink(file: File): Sink {
|
|
||||||
return try {
|
|
||||||
file.appendingSink()
|
|
||||||
} catch (_: FileNotFoundException) {
|
|
||||||
// Maybe the parent directory doesn't exist? Try creating it first.
|
|
||||||
file.parentFile.mkdirs()
|
|
||||||
file.appendingSink()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun delete(file: File) {
|
|
||||||
// If delete() fails, make sure it's because the file didn't exist!
|
|
||||||
if (!file.delete() && file.exists()) {
|
|
||||||
throw IOException("failed to delete $file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun exists(file: File): Boolean = file.exists()
|
|
||||||
|
|
||||||
override fun size(file: File): Long = file.length()
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun rename(from: File, to: File) {
|
|
||||||
delete(to)
|
|
||||||
if (!from.renameTo(to)) {
|
|
||||||
throw IOException("failed to rename $from to $to")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun deleteContents(directory: File) {
|
|
||||||
val files = directory.listFiles() ?: throw IOException("not a readable directory: $directory")
|
|
||||||
for (file in files) {
|
|
||||||
if (file.isDirectory) {
|
|
||||||
deleteContents(file)
|
|
||||||
}
|
|
||||||
if (!file.delete()) {
|
|
||||||
throw IOException("failed to delete $file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString() = "FileSystem.SYSTEM"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Reads from [file]. */
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun source(file: File): Source
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes to [file], discarding any data already present. Creates parent directories if
|
|
||||||
* necessary.
|
|
||||||
*/
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun sink(file: File): Sink
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes to [file], appending if data is already present. Creates parent directories if
|
|
||||||
* necessary.
|
|
||||||
*/
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun appendingSink(file: File): Sink
|
|
||||||
|
|
||||||
/** Deletes [file] if it exists. Throws if the file exists and cannot be deleted. */
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun delete(file: File)
|
|
||||||
|
|
||||||
/** Returns true if [file] exists on the file system. */
|
|
||||||
fun exists(file: File): Boolean
|
|
||||||
|
|
||||||
/** Returns the number of bytes stored in [file], or 0 if it does not exist. */
|
|
||||||
fun size(file: File): Long
|
|
||||||
|
|
||||||
/** Renames [from] to [to]. Throws if the file cannot be renamed. */
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun rename(from: File, to: File)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively delete the contents of [directory]. Throws an IOException if any file could
|
|
||||||
* not be deleted, or if `dir` is not a readable directory.
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun deleteContents(directory: File)
|
|
||||||
}
|
|
@@ -18,15 +18,17 @@ package okhttp3
|
|||||||
import mockwebserver3.MockResponse
|
import mockwebserver3.MockResponse
|
||||||
import mockwebserver3.MockWebServer
|
import mockwebserver3.MockWebServer
|
||||||
import okhttp3.internal.buildCache
|
import okhttp3.internal.buildCache
|
||||||
import okhttp3.internal.io.InMemoryFileSystem
|
import okhttp3.okio.LoggingFilesystem
|
||||||
import okhttp3.testing.PlatformRule
|
import okhttp3.testing.PlatformRule
|
||||||
import okhttp3.tls.internal.TlsUtil.localhost
|
import okhttp3.tls.internal.TlsUtil.localhost
|
||||||
|
import okio.ExperimentalFileSystem
|
||||||
|
import okio.Path.Companion.toPath
|
||||||
|
import okio.fakefilesystem.FakeFileSystem
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.RegisterExtension
|
import org.junit.jupiter.api.extension.RegisterExtension
|
||||||
import java.io.File
|
|
||||||
import java.net.CookieManager
|
import java.net.CookieManager
|
||||||
import java.net.ResponseCache
|
import java.net.ResponseCache
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
@@ -38,12 +40,20 @@ import java.util.concurrent.TimeUnit
|
|||||||
import javax.net.ssl.HostnameVerifier
|
import javax.net.ssl.HostnameVerifier
|
||||||
import javax.net.ssl.SSLSession
|
import javax.net.ssl.SSLSession
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFileSystem::class)
|
||||||
class CacheCorruptionTest(
|
class CacheCorruptionTest(
|
||||||
var server: MockWebServer
|
var server: MockWebServer
|
||||||
) {
|
) {
|
||||||
@JvmField @RegisterExtension var fileSystem = InMemoryFileSystem()
|
@OptIn(ExperimentalFileSystem::class)
|
||||||
@JvmField @RegisterExtension val clientTestRule = OkHttpClientTestRule()
|
var fileSystem = FakeFileSystem()
|
||||||
@JvmField @RegisterExtension val platform = PlatformRule()
|
|
||||||
|
@JvmField
|
||||||
|
@RegisterExtension
|
||||||
|
val clientTestRule = OkHttpClientTestRule()
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@RegisterExtension
|
||||||
|
val platform = PlatformRule()
|
||||||
|
|
||||||
private val handshakeCertificates = localhost()
|
private val handshakeCertificates = localhost()
|
||||||
private lateinit var client: OkHttpClient
|
private lateinit var client: OkHttpClient
|
||||||
@@ -52,25 +62,29 @@ class CacheCorruptionTest(
|
|||||||
HostnameVerifier { _: String?, _: SSLSession? -> true }
|
HostnameVerifier { _: String?, _: SSLSession? -> true }
|
||||||
private val cookieManager = CookieManager()
|
private val cookieManager = CookieManager()
|
||||||
|
|
||||||
@BeforeEach fun setUp() {
|
@BeforeEach
|
||||||
|
fun setUp() {
|
||||||
platform.assumeNotOpenJSSE()
|
platform.assumeNotOpenJSSE()
|
||||||
platform.assumeNotBouncyCastle()
|
platform.assumeNotBouncyCastle()
|
||||||
server.protocolNegotiationEnabled = false
|
server.protocolNegotiationEnabled = false
|
||||||
cache = buildCache(File("/cache/"), Int.MAX_VALUE.toLong(), fileSystem)
|
val loggingFileSystem = LoggingFilesystem(fileSystem)
|
||||||
|
cache = buildCache("/cache/".toPath(), Int.MAX_VALUE.toLong(), loggingFileSystem)
|
||||||
client = clientTestRule.newClientBuilder()
|
client = clientTestRule.newClientBuilder()
|
||||||
.cache(cache)
|
.cache(cache)
|
||||||
.cookieJar(JavaNetCookieJar(cookieManager))
|
.cookieJar(JavaNetCookieJar(cookieManager))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach fun tearDown() {
|
@AfterEach
|
||||||
|
fun tearDown() {
|
||||||
ResponseCache.setDefault(null)
|
ResponseCache.setDefault(null)
|
||||||
if (this::cache.isInitialized) {
|
if (this::cache.isInitialized) {
|
||||||
cache.delete()
|
cache.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun corruptedCipher() {
|
@Test
|
||||||
|
fun corruptedCipher() {
|
||||||
val response = testCorruptingCache {
|
val response = testCorruptingCache {
|
||||||
corruptMetadata {
|
corruptMetadata {
|
||||||
// mess with cipher suite
|
// mess with cipher suite
|
||||||
@@ -86,7 +100,8 @@ class CacheCorruptionTest(
|
|||||||
assertThat(response.handshake?.cipherSuite?.javaName).startsWith("SLT_")
|
assertThat(response.handshake?.cipherSuite?.javaName).startsWith("SLT_")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun truncatedMetadataEntry() {
|
@Test
|
||||||
|
fun truncatedMetadataEntry() {
|
||||||
val response = testCorruptingCache {
|
val response = testCorruptingCache {
|
||||||
corruptMetadata {
|
corruptMetadata {
|
||||||
// truncate metadata to 1/4 of length
|
// truncate metadata to 1/4 of length
|
||||||
@@ -115,30 +130,41 @@ class CacheCorruptionTest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun corruptMetadata(corruptor: (String) -> String) {
|
private fun corruptMetadata(corruptor: (String) -> String) {
|
||||||
val metadataFile = fileSystem.files.keys.find { it.name.endsWith(".0") }
|
val metadataFile = fileSystem.allPaths.find {
|
||||||
val metadataBuffer = fileSystem.files[metadataFile]
|
it.name.endsWith(".0")
|
||||||
|
}
|
||||||
|
|
||||||
val contents = metadataBuffer!!.peek().readUtf8()
|
if (metadataFile != null) {
|
||||||
|
val contents = fileSystem.read(metadataFile) {
|
||||||
|
readUtf8()
|
||||||
|
}
|
||||||
|
|
||||||
metadataBuffer.clear()
|
fileSystem.write(metadataFile) {
|
||||||
metadataBuffer.writeUtf8(corruptor(contents))
|
writeUtf8(corruptor(contents))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testCorruptingCache(corruptor: () -> Unit): Response {
|
private fun testCorruptingCache(corruptor: () -> Unit): Response {
|
||||||
server.useHttps(handshakeCertificates.sslSocketFactory(), false)
|
server.useHttps(handshakeCertificates.sslSocketFactory(), false)
|
||||||
server.enqueue(MockResponse()
|
server.enqueue(
|
||||||
|
MockResponse()
|
||||||
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
|
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
|
||||||
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
|
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
|
||||||
.setBody("ABC.1"))
|
.setBody("ABC.1")
|
||||||
server.enqueue(MockResponse()
|
)
|
||||||
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
|
server.enqueue(
|
||||||
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
|
MockResponse()
|
||||||
.setBody("ABC.2"))
|
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
|
||||||
|
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
|
||||||
|
.setBody("ABC.2")
|
||||||
|
)
|
||||||
client = client.newBuilder()
|
client = client.newBuilder()
|
||||||
.sslSocketFactory(
|
.sslSocketFactory(
|
||||||
handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager)
|
handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager
|
||||||
.hostnameVerifier(NULL_HOSTNAME_VERIFIER)
|
)
|
||||||
.build()
|
.hostnameVerifier(NULL_HOSTNAME_VERIFIER)
|
||||||
|
.build()
|
||||||
val request: Request = Request.Builder().url(server.url("/")).build()
|
val request: Request = Request.Builder().url(server.url("/")).build()
|
||||||
val response1: Response = client.newCall(request).execute()
|
val response1: Response = client.newCall(request).execute()
|
||||||
val bodySource = response1.body!!.source()
|
val bodySource = response1.body!!.source()
|
||||||
|
@@ -38,7 +38,6 @@ import mockwebserver3.MockResponse;
|
|||||||
import mockwebserver3.MockWebServer;
|
import mockwebserver3.MockWebServer;
|
||||||
import mockwebserver3.RecordedRequest;
|
import mockwebserver3.RecordedRequest;
|
||||||
import okhttp3.internal.Internal;
|
import okhttp3.internal.Internal;
|
||||||
import okhttp3.internal.io.InMemoryFileSystem;
|
|
||||||
import okhttp3.internal.platform.Platform;
|
import okhttp3.internal.platform.Platform;
|
||||||
import okhttp3.testing.PlatformRule;
|
import okhttp3.testing.PlatformRule;
|
||||||
import okhttp3.tls.HandshakeCertificates;
|
import okhttp3.tls.HandshakeCertificates;
|
||||||
@@ -47,6 +46,8 @@ import okio.BufferedSink;
|
|||||||
import okio.BufferedSource;
|
import okio.BufferedSource;
|
||||||
import okio.GzipSink;
|
import okio.GzipSink;
|
||||||
import okio.Okio;
|
import okio.Okio;
|
||||||
|
import okio.Path;
|
||||||
|
import okio.fakefilesystem.FakeFileSystem;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Tag;
|
import org.junit.jupiter.api.Tag;
|
||||||
@@ -64,7 +65,7 @@ import static org.junit.jupiter.api.Assertions.fail;
|
|||||||
public final class CacheTest {
|
public final class CacheTest {
|
||||||
private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = (name, session) -> true;
|
private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = (name, session) -> true;
|
||||||
|
|
||||||
@RegisterExtension public InMemoryFileSystem fileSystem = new InMemoryFileSystem();
|
public FakeFileSystem fileSystem = new FakeFileSystem();
|
||||||
@RegisterExtension public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule();
|
@RegisterExtension public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule();
|
||||||
@RegisterExtension public final PlatformRule platform = new PlatformRule();
|
@RegisterExtension public final PlatformRule platform = new PlatformRule();
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ public final class CacheTest {
|
|||||||
platform.assumeNotBouncyCastle();
|
platform.assumeNotBouncyCastle();
|
||||||
|
|
||||||
server.setProtocolNegotiationEnabled(false);
|
server.setProtocolNegotiationEnabled(false);
|
||||||
cache = new Cache(new File("/cache/"), Integer.MAX_VALUE, fileSystem);
|
cache = new Cache(Path.get("/cache/"), Integer.MAX_VALUE, fileSystem);
|
||||||
client = clientTestRule.newClientBuilder()
|
client = clientTestRule.newClientBuilder()
|
||||||
.cache(cache)
|
.cache(cache)
|
||||||
.cookieJar(new JavaNetCookieJar(cookieManager))
|
.cookieJar(new JavaNetCookieJar(cookieManager))
|
||||||
@@ -2061,10 +2062,11 @@ public final class CacheTest {
|
|||||||
+ "2\n"
|
+ "2\n"
|
||||||
+ "\n"
|
+ "\n"
|
||||||
+ "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
|
+ "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
|
||||||
writeFile(cache.directory(), urlKey + ".0", entryMetadata);
|
fileSystem.createDirectory(cache.directoryPath());
|
||||||
writeFile(cache.directory(), urlKey + ".1", entryBody);
|
writeFile(cache.directoryPath(), urlKey + ".0", entryMetadata);
|
||||||
writeFile(cache.directory(), "journal", journalBody);
|
writeFile(cache.directoryPath(), urlKey + ".1", entryBody);
|
||||||
cache = new Cache(cache.directory(), Integer.MAX_VALUE, fileSystem);
|
writeFile(cache.directoryPath(), "journal", journalBody);
|
||||||
|
cache = new Cache(Path.get(cache.directory().getPath()), Integer.MAX_VALUE, fileSystem);
|
||||||
client = client.newBuilder()
|
client = client.newBuilder()
|
||||||
.cache(cache)
|
.cache(cache)
|
||||||
.build();
|
.build();
|
||||||
@@ -2110,11 +2112,12 @@ public final class CacheTest {
|
|||||||
+ "\n"
|
+ "\n"
|
||||||
+ "DIRTY " + urlKey + "\n"
|
+ "DIRTY " + urlKey + "\n"
|
||||||
+ "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
|
+ "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
|
||||||
writeFile(cache.directory(), urlKey + ".0", entryMetadata);
|
fileSystem.createDirectory(cache.directoryPath());
|
||||||
writeFile(cache.directory(), urlKey + ".1", entryBody);
|
writeFile(cache.directoryPath(), urlKey + ".0", entryMetadata);
|
||||||
writeFile(cache.directory(), "journal", journalBody);
|
writeFile(cache.directoryPath(), urlKey + ".1", entryBody);
|
||||||
|
writeFile(cache.directoryPath(), "journal", journalBody);
|
||||||
cache.close();
|
cache.close();
|
||||||
cache = new Cache(cache.directory(), Integer.MAX_VALUE, fileSystem);
|
cache = new Cache(Path.get(cache.directory().getPath()), Integer.MAX_VALUE, fileSystem);
|
||||||
client = client.newBuilder()
|
client = client.newBuilder()
|
||||||
.cache(cache)
|
.cache(cache)
|
||||||
.build();
|
.build();
|
||||||
@@ -2160,11 +2163,12 @@ public final class CacheTest {
|
|||||||
+ "\n"
|
+ "\n"
|
||||||
+ "DIRTY " + urlKey + "\n"
|
+ "DIRTY " + urlKey + "\n"
|
||||||
+ "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
|
+ "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
|
||||||
writeFile(cache.directory(), urlKey + ".0", entryMetadata);
|
fileSystem.createDirectory(cache.directoryPath());
|
||||||
writeFile(cache.directory(), urlKey + ".1", entryBody);
|
writeFile(cache.directoryPath(), urlKey + ".0", entryMetadata);
|
||||||
writeFile(cache.directory(), "journal", journalBody);
|
writeFile(cache.directoryPath(), urlKey + ".1", entryBody);
|
||||||
|
writeFile(cache.directoryPath(), "journal", journalBody);
|
||||||
cache.close();
|
cache.close();
|
||||||
cache = new Cache(cache.directory(), Integer.MAX_VALUE, fileSystem);
|
cache = new Cache(Path.get(cache.directory().getPath()), Integer.MAX_VALUE, fileSystem);
|
||||||
client = client.newBuilder()
|
client = client.newBuilder()
|
||||||
.cache(cache)
|
.cache(cache)
|
||||||
.build();
|
.build();
|
||||||
@@ -2197,11 +2201,12 @@ public final class CacheTest {
|
|||||||
+ "\n"
|
+ "\n"
|
||||||
+ "DIRTY " + urlKey + "\n"
|
+ "DIRTY " + urlKey + "\n"
|
||||||
+ "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
|
+ "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
|
||||||
writeFile(cache.directory(), urlKey + ".0", entryMetadata);
|
fileSystem.createDirectory(cache.directoryPath());
|
||||||
writeFile(cache.directory(), urlKey + ".1", entryBody);
|
writeFile(cache.directoryPath(), urlKey + ".0", entryMetadata);
|
||||||
writeFile(cache.directory(), "journal", journalBody);
|
writeFile(cache.directoryPath(), urlKey + ".1", entryBody);
|
||||||
|
writeFile(cache.directoryPath(), "journal", journalBody);
|
||||||
cache.close();
|
cache.close();
|
||||||
cache = new Cache(cache.directory(), Integer.MAX_VALUE, fileSystem);
|
cache = new Cache(Path.get(cache.directory().getPath()), Integer.MAX_VALUE, fileSystem);
|
||||||
client = client.newBuilder()
|
client = client.newBuilder()
|
||||||
.cache(cache)
|
.cache(cache)
|
||||||
.build();
|
.build();
|
||||||
@@ -2496,8 +2501,8 @@ public final class CacheTest {
|
|||||||
return client.newCall(request).execute();
|
return client.newCall(request).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeFile(File directory, String file, String content) throws IOException {
|
private void writeFile(Path directory, String file, String content) throws IOException {
|
||||||
BufferedSink sink = Okio.buffer(fileSystem.sink(new File(directory, file)));
|
BufferedSink sink = Okio.buffer(fileSystem.sink(directory.resolve(file)));
|
||||||
sink.writeUtf8(content);
|
sink.writeUtf8(content);
|
||||||
sink.close();
|
sink.close();
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package okhttp3;
|
package okhttp3;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -69,7 +68,7 @@ import okhttp3.internal.DoubleInetAddressDns;
|
|||||||
import okhttp3.internal.RecordingOkAuthenticator;
|
import okhttp3.internal.RecordingOkAuthenticator;
|
||||||
import okhttp3.internal.Util;
|
import okhttp3.internal.Util;
|
||||||
import okhttp3.internal.http.RecordingProxySelector;
|
import okhttp3.internal.http.RecordingProxySelector;
|
||||||
import okhttp3.internal.io.InMemoryFileSystem;
|
import okhttp3.okio.LoggingFilesystem;
|
||||||
import okhttp3.testing.Flaky;
|
import okhttp3.testing.Flaky;
|
||||||
import okhttp3.testing.PlatformRule;
|
import okhttp3.testing.PlatformRule;
|
||||||
import okhttp3.tls.HandshakeCertificates;
|
import okhttp3.tls.HandshakeCertificates;
|
||||||
@@ -80,6 +79,8 @@ import okio.BufferedSource;
|
|||||||
import okio.ForwardingSource;
|
import okio.ForwardingSource;
|
||||||
import okio.GzipSink;
|
import okio.GzipSink;
|
||||||
import okio.Okio;
|
import okio.Okio;
|
||||||
|
import okio.Path;
|
||||||
|
import okio.fakefilesystem.FakeFileSystem;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Disabled;
|
import org.junit.jupiter.api.Disabled;
|
||||||
@@ -104,7 +105,7 @@ import static org.junit.jupiter.api.Assumptions.assumeFalse;
|
|||||||
@Timeout(30)
|
@Timeout(30)
|
||||||
public final class CallTest {
|
public final class CallTest {
|
||||||
@RegisterExtension final PlatformRule platform = new PlatformRule();
|
@RegisterExtension final PlatformRule platform = new PlatformRule();
|
||||||
@RegisterExtension final InMemoryFileSystem fileSystem = new InMemoryFileSystem();
|
final FakeFileSystem fileSystem = new FakeFileSystem();
|
||||||
@RegisterExtension final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule();
|
@RegisterExtension final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule();
|
||||||
@RegisterExtension final TestLogHandler testLogHandler = new TestLogHandler(OkHttpClient.class);
|
@RegisterExtension final TestLogHandler testLogHandler = new TestLogHandler(OkHttpClient.class);
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ public final class CallTest {
|
|||||||
.eventListenerFactory(clientTestRule.wrap(listener))
|
.eventListenerFactory(clientTestRule.wrap(listener))
|
||||||
.build();
|
.build();
|
||||||
private RecordingCallback callback = new RecordingCallback();
|
private RecordingCallback callback = new RecordingCallback();
|
||||||
private Cache cache = new Cache(new File("/cache/"), Integer.MAX_VALUE, fileSystem);
|
private Cache cache = new Cache(Path.get("/cache"), Integer.MAX_VALUE, new LoggingFilesystem(fileSystem));
|
||||||
|
|
||||||
public CallTest(MockWebServer server, MockWebServer server2) {
|
public CallTest(MockWebServer server, MockWebServer server2) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
@@ -129,7 +130,8 @@ public final class CallTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach public void tearDown() throws Exception {
|
@AfterEach public void tearDown() throws Exception {
|
||||||
cache.delete();
|
cache.close();
|
||||||
|
fileSystem.checkNoOpenFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test public void get() throws Exception {
|
@Test public void get() throws Exception {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,114 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2011 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* 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 okhttp3.internal.io;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
import okio.Buffer;
|
|
||||||
import okio.ForwardingSink;
|
|
||||||
import okio.Sink;
|
|
||||||
import okio.Source;
|
|
||||||
|
|
||||||
public final class FaultyFileSystem implements FileSystem {
|
|
||||||
private final FileSystem delegate;
|
|
||||||
private final Set<File> writeFaults = new LinkedHashSet<>();
|
|
||||||
private final Set<File> deleteFaults = new LinkedHashSet<>();
|
|
||||||
private final Set<File> renameFaults = new LinkedHashSet<>();
|
|
||||||
|
|
||||||
public FaultyFileSystem(FileSystem delegate) {
|
|
||||||
this.delegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFaultyWrite(File file, boolean faulty) {
|
|
||||||
if (faulty) {
|
|
||||||
writeFaults.add(file);
|
|
||||||
} else {
|
|
||||||
writeFaults.remove(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFaultyDelete(File file, boolean faulty) {
|
|
||||||
if (faulty) {
|
|
||||||
deleteFaults.add(file);
|
|
||||||
} else {
|
|
||||||
deleteFaults.remove(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFaultyRename(File file, boolean faulty) {
|
|
||||||
if (faulty) {
|
|
||||||
renameFaults.add(file);
|
|
||||||
} else {
|
|
||||||
renameFaults.remove(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public Source source(File file) throws FileNotFoundException {
|
|
||||||
return delegate.source(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public Sink sink(File file) throws FileNotFoundException {
|
|
||||||
return new FaultySink(delegate.sink(file), file);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public Sink appendingSink(File file) throws FileNotFoundException {
|
|
||||||
return new FaultySink(delegate.appendingSink(file), file);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void delete(File file) throws IOException {
|
|
||||||
if (deleteFaults.contains(file)) throw new IOException("boom!");
|
|
||||||
delegate.delete(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public boolean exists(File file) {
|
|
||||||
return delegate.exists(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public long size(File file) {
|
|
||||||
return delegate.size(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void rename(File from, File to) throws IOException {
|
|
||||||
if (renameFaults.contains(from) || renameFaults.contains(to)) throw new IOException("boom!");
|
|
||||||
delegate.rename(from, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void deleteContents(File directory) throws IOException {
|
|
||||||
if (deleteFaults.contains(directory)) throw new IOException("boom!");
|
|
||||||
delegate.deleteContents(directory);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class FaultySink extends ForwardingSink {
|
|
||||||
private final File file;
|
|
||||||
|
|
||||||
public FaultySink(Sink delegate, File file) {
|
|
||||||
super(delegate);
|
|
||||||
this.file = file;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void write(Buffer source, long byteCount) throws IOException {
|
|
||||||
if (writeFaults.contains(file)) throw new IOException("boom!");
|
|
||||||
super.write(source, byteCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public String toString() {
|
|
||||||
return "Faulty " + delegate;
|
|
||||||
}
|
|
||||||
}
|
|
89
okhttp/src/test/java/okhttp3/internal/io/FaultyFileSystem.kt
Normal file
89
okhttp/src/test/java/okhttp3/internal/io/FaultyFileSystem.kt
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 okhttp3.internal.io
|
||||||
|
|
||||||
|
import okio.Buffer
|
||||||
|
import okio.ExperimentalFileSystem
|
||||||
|
import okio.FileSystem
|
||||||
|
import okio.ForwardingFileSystem
|
||||||
|
import okio.ForwardingSink
|
||||||
|
import okio.Path
|
||||||
|
import okio.Sink
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.LinkedHashSet
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFileSystem::class)
|
||||||
|
class FaultyFileSystem constructor(delegate: FileSystem?) : ForwardingFileSystem(delegate!!) {
|
||||||
|
private val writeFaults: MutableSet<Path> = LinkedHashSet()
|
||||||
|
private val deleteFaults: MutableSet<Path> = LinkedHashSet()
|
||||||
|
private val renameFaults: MutableSet<Path> = LinkedHashSet()
|
||||||
|
|
||||||
|
fun setFaultyWrite(file: Path, faulty: Boolean) {
|
||||||
|
if (faulty) {
|
||||||
|
writeFaults.add(file)
|
||||||
|
} else {
|
||||||
|
writeFaults.remove(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFaultyDelete(file: Path, faulty: Boolean) {
|
||||||
|
if (faulty) {
|
||||||
|
deleteFaults.add(file)
|
||||||
|
} else {
|
||||||
|
deleteFaults.remove(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFaultyRename(file: Path, faulty: Boolean) {
|
||||||
|
if (faulty) {
|
||||||
|
renameFaults.add(file)
|
||||||
|
} else {
|
||||||
|
renameFaults.remove(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun atomicMove(source: Path, target: Path) {
|
||||||
|
if (renameFaults.contains(source) || renameFaults.contains(target)) throw IOException("boom!")
|
||||||
|
super.atomicMove(source, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun delete(path: Path) {
|
||||||
|
if (deleteFaults.contains(path)) throw IOException("boom!")
|
||||||
|
super.delete(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun deleteRecursively(fileOrDirectory: Path) {
|
||||||
|
if (deleteFaults.contains(fileOrDirectory)) throw IOException("boom!")
|
||||||
|
super.deleteRecursively(fileOrDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun appendingSink(file: Path): Sink = FaultySink(super.appendingSink(file), file)
|
||||||
|
|
||||||
|
override fun sink(file: Path): Sink = FaultySink(super.sink(file), file)
|
||||||
|
|
||||||
|
inner class FaultySink(sink: Sink, private val file: Path) : ForwardingSink(sink) {
|
||||||
|
override fun write(source: Buffer, byteCount: Long) {
|
||||||
|
if (writeFaults.contains(file)) {
|
||||||
|
throw IOException("boom!")
|
||||||
|
} else {
|
||||||
|
super.write(source, byteCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,175 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2020 Square, Inc.
|
|
||||||
*
|
|
||||||
* 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 okhttp3.internal.io
|
|
||||||
|
|
||||||
import okhttp3.SimpleProvider
|
|
||||||
import okhttp3.TestUtil
|
|
||||||
import okio.buffer
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.junit.jupiter.api.io.TempDir
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
|
||||||
import org.junit.jupiter.params.provider.ArgumentsSource
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class FileSystemParamProvider: SimpleProvider() {
|
|
||||||
override fun arguments() = listOf(
|
|
||||||
FileSystem.SYSTEM to TestUtil.windows,
|
|
||||||
InMemoryFileSystem() to false,
|
|
||||||
WindowsFileSystem(FileSystem.SYSTEM) to true,
|
|
||||||
WindowsFileSystem(InMemoryFileSystem()) to true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that our file system abstraction is consistent and sufficient for OkHttp's needs. We're
|
|
||||||
* particularly interested in what happens when open files are moved or deleted on Windows.
|
|
||||||
*/
|
|
||||||
class FileSystemTest {
|
|
||||||
@TempDir lateinit var temporaryFolder: File
|
|
||||||
|
|
||||||
private lateinit var fileSystem: FileSystem
|
|
||||||
private var windows: Boolean = false
|
|
||||||
|
|
||||||
internal fun setUp(fileSystem: FileSystem, windows: Boolean) {
|
|
||||||
this.fileSystem = fileSystem
|
|
||||||
this.windows = windows
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@ArgumentsSource(FileSystemParamProvider::class)
|
|
||||||
fun `delete open for writing fails on Windows`(
|
|
||||||
parameters: Pair<FileSystem, Boolean>
|
|
||||||
) {
|
|
||||||
setUp(parameters.first, parameters.second)
|
|
||||||
val file = File(temporaryFolder, "file.txt")
|
|
||||||
expectIOExceptionOnWindows {
|
|
||||||
fileSystem.sink(file).use {
|
|
||||||
fileSystem.delete(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@ArgumentsSource(FileSystemParamProvider::class)
|
|
||||||
fun `delete open for appending fails on Windows`(
|
|
||||||
parameters: Pair<FileSystem, Boolean>
|
|
||||||
) {
|
|
||||||
setUp(parameters.first, parameters.second)
|
|
||||||
val file = File(temporaryFolder, "file.txt")
|
|
||||||
file.write("abc")
|
|
||||||
expectIOExceptionOnWindows {
|
|
||||||
fileSystem.appendingSink(file).use {
|
|
||||||
fileSystem.delete(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@ArgumentsSource(FileSystemParamProvider::class)
|
|
||||||
fun `delete open for reading fails on Windows`(
|
|
||||||
parameters: Pair<FileSystem, Boolean>
|
|
||||||
) {
|
|
||||||
setUp(parameters.first, parameters.second)
|
|
||||||
val file = File(temporaryFolder, "file.txt")
|
|
||||||
file.write("abc")
|
|
||||||
expectIOExceptionOnWindows {
|
|
||||||
fileSystem.source(file).use {
|
|
||||||
fileSystem.delete(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@ArgumentsSource(FileSystemParamProvider::class)
|
|
||||||
fun `rename target exists succeeds on all platforms`(
|
|
||||||
parameters: Pair<FileSystem, Boolean>
|
|
||||||
) {
|
|
||||||
setUp(parameters.first, parameters.second)
|
|
||||||
val from = File(temporaryFolder, "from.txt")
|
|
||||||
val to = File(temporaryFolder, "to.txt")
|
|
||||||
from.write("source file")
|
|
||||||
to.write("target file")
|
|
||||||
fileSystem.rename(from, to)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@ArgumentsSource(FileSystemParamProvider::class)
|
|
||||||
fun `rename source is open fails on Windows`(
|
|
||||||
parameters: Pair<FileSystem, Boolean>
|
|
||||||
) {
|
|
||||||
setUp(parameters.first, parameters.second)
|
|
||||||
val from = File(temporaryFolder, "from.txt")
|
|
||||||
val to = File(temporaryFolder, "to.txt")
|
|
||||||
from.write("source file")
|
|
||||||
to.write("target file")
|
|
||||||
expectIOExceptionOnWindows {
|
|
||||||
fileSystem.source(from).use {
|
|
||||||
fileSystem.rename(from, to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@ArgumentsSource(FileSystemParamProvider::class)
|
|
||||||
fun `rename target is open fails on Windows`(
|
|
||||||
parameters: Pair<FileSystem, Boolean>
|
|
||||||
) {
|
|
||||||
setUp(parameters.first, parameters.second)
|
|
||||||
val from = File(temporaryFolder, "from.txt")
|
|
||||||
val to = File(temporaryFolder, "to.txt")
|
|
||||||
from.write("source file")
|
|
||||||
to.write("target file")
|
|
||||||
expectIOExceptionOnWindows {
|
|
||||||
fileSystem.source(to).use {
|
|
||||||
fileSystem.rename(from, to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@ArgumentsSource(FileSystemParamProvider::class)
|
|
||||||
fun `delete contents of parent of file open for reading fails on Windows`(
|
|
||||||
parameters: Pair<FileSystem, Boolean>
|
|
||||||
) {
|
|
||||||
setUp(parameters.first, parameters.second)
|
|
||||||
val parentA = File(temporaryFolder, "a").also { it.mkdirs() }
|
|
||||||
val parentAB = File(parentA, "b")
|
|
||||||
val parentABC = File(parentAB, "c")
|
|
||||||
val file = File(parentABC, "file.txt")
|
|
||||||
file.write("child file")
|
|
||||||
expectIOExceptionOnWindows {
|
|
||||||
fileSystem.source(file).use {
|
|
||||||
fileSystem.deleteContents(parentA)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun File.write(content: String) {
|
|
||||||
fileSystem.sink(this).buffer().use {
|
|
||||||
it.writeUtf8(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun expectIOExceptionOnWindows(block: () -> Unit) {
|
|
||||||
try {
|
|
||||||
block()
|
|
||||||
assertThat(windows).isFalse()
|
|
||||||
} catch (_: IOException) {
|
|
||||||
assertThat(windows).isTrue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user