Skip to content

Commit

Permalink
Speed up access to shared storage (#581)
Browse files Browse the repository at this point in the history
  • Loading branch information
qnga authored Oct 28, 2024
1 parent f1deb9d commit 94ba891
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 200 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,21 @@ import android.net.Uri
import android.provider.MediaStore
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.readium.r2.shared.InternalReadiumApi
import org.readium.r2.shared.extensions.*
import org.readium.r2.shared.extensions.coerceFirstNonNegative
import org.readium.r2.shared.extensions.queryProjection
import org.readium.r2.shared.extensions.readFully
import org.readium.r2.shared.extensions.requireLengthFitInt
import org.readium.r2.shared.util.AbsoluteUrl
import org.readium.r2.shared.util.DebugError
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.util.data.ReadError
import org.readium.r2.shared.util.flatMap
import org.readium.r2.shared.util.getOrElse
import org.readium.r2.shared.util.io.CountingInputStream
import org.readium.r2.shared.util.mediatype.MediaType
import org.readium.r2.shared.util.resource.Resource
import org.readium.r2.shared.util.resource.filename
Expand All @@ -45,9 +49,12 @@ public class ContentResource(

private lateinit var _properties: Try<Resource.Properties, ReadError>

private var stream: CountingInputStream? = null

override val sourceUrl: AbsoluteUrl? = uri.toUrl() as? AbsoluteUrl

override fun close() {
stream?.close()
}

override suspend fun properties(): Try<Resource.Properties, ReadError> {
Expand Down Expand Up @@ -95,22 +102,12 @@ public class ContentResource(
}

private suspend fun readFully(): Try<ByteArray, ReadError> =
withStream { it.readFully() }
withStream(fromIndex = 0) { it.readFully() }

private suspend fun readRange(range: LongRange): Try<ByteArray, ReadError> =
withStream {
withStream(fromIndex = range.first) {
withContext(Dispatchers.IO) {
var skipped: Long = 0

while (skipped != range.first) {
skipped += it.skip(range.first - skipped)
if (skipped == 0L) {
throw IOException("Could not skip InputStream to read ranges from $uri.")
}
}

val length = range.last - range.first + 1
it.read(length)
it.readRange(range)
}
}

Expand All @@ -134,16 +131,37 @@ public class ContentResource(
return _length
}

private suspend fun <T> withStream(block: suspend (InputStream) -> T): Try<T, ReadError> {
private suspend fun <T> withStream(
fromIndex: Long,
block: suspend (CountingInputStream) -> T
): Try<T, ReadError> {
val stream = stream(fromIndex)
.getOrElse { return Try.failure(it) }

return Try.catching {
val stream = contentResolver.openInputStream(uri)
block(stream)
}
}

private fun stream(fromIndex: Long): Try<CountingInputStream, ReadError> {
// Reuse the current stream if it didn't exceed the requested index.
stream
?.takeIf { it.count <= fromIndex }
?.let { return Try.success(it) }

stream?.close()

val contentStream =
contentResolver.openInputStream(uri)
?: return Try.failure(
ReadError.Access(
ContentResolverError.NotAvailable()
)
)
stream.use { block(stream) }
}

stream = CountingInputStream(contentStream)

return Try.success(stream!!)
}

private inline fun <T> Try.Companion.catching(closure: () -> T): Try<T, ReadError> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,16 @@ public class CountingInputStream(
return ByteArray(0)
}

skip(range.first - count)
val toSkip = range.first - count
var skipped: Long = 0

while (skipped != toSkip) {
skipped += skip(toSkip - skipped)
if (skipped == 0L) {
throw IOException("Could not skip InputStream to read ranges.")
}
}

val length = range.last - range.first + 1
return read(length)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,13 @@ internal class StreamingZipArchiveProvider {
val datasourceChannel = ReadableChannelAdapter(readable, wrapError)
val channel = wrapBaseChannel(datasourceChannel)
val zipFile = ZipFile(channel, true)
StreamingZipContainer(zipFile, sourceUrl)
val sourceScheme = (readable as? Resource)?.sourceUrl?.scheme
val cacheEntryMaxSize =
when {
sourceScheme?.isContent ?: false -> 5242880
else -> 0
}
StreamingZipContainer(zipFile, sourceUrl, cacheEntryMaxSize)
}

internal suspend fun openFile(file: File): Container<Resource> = withContext(Dispatchers.IO) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,18 @@ import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile

internal class StreamingZipContainer(
private val zipFile: ZipFile,
override val sourceUrl: AbsoluteUrl?
override val sourceUrl: AbsoluteUrl?,
private val cacheEntryMaxSize: Int = 0
) : Container<Resource> {

private inner class Entry(
private val url: Url,
private val entry: ZipArchiveEntry
) : Resource {

private var cache: ByteArray? =
null

override val sourceUrl: AbsoluteUrl? get() = null

override suspend fun properties(): ReadTry<Resource.Properties> =
Expand Down Expand Up @@ -102,8 +106,25 @@ internal class StreamingZipContainer(
it.readFully()
}

private fun readRange(range: LongRange): ByteArray =
stream(range.first).readRange(range)
private suspend fun readRange(range: LongRange): ByteArray =
when {
cache != null -> {
// If the entry is cached, its size fit into an Int.
val rangeSize = (range.last - range.first + 1).toInt()
cache!!.copyInto(
ByteArray(rangeSize),
startIndex = range.first.toInt(),
endIndex = range.last.toInt() + 1
)
}

entry.size in 0 until cacheEntryMaxSize -> {
cache = readFully()
readRange(range)
}
else ->
stream(range.first).readRange(range)
}

/**
* Reading an entry in chunks (e.g. from the HTTP server) can be really slow if the entry
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ class Bookshelf(
}
}

fun importPublicationFromHttp(
url: AbsoluteUrl
) {
coroutineScope.launch {
addBookFeedback(publicationRetriever.retrieveFromHttp(url))
}
}

fun importPublicationFromOpds(
publication: Publication
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package org.readium.r2.testapp.domain
import org.readium.r2.lcp.LcpError
import org.readium.r2.shared.util.DebugError
import org.readium.r2.shared.util.Error
import org.readium.r2.shared.util.content.ContentResolverError
import org.readium.r2.shared.util.file.FileSystemError
import org.readium.r2.shared.util.http.HttpError
import org.readium.r2.testapp.R
Expand Down Expand Up @@ -36,6 +37,10 @@ sealed class ImportError(
override val cause: FileSystemError
) : ImportError(cause)

class ContentResolver(
override val cause: ContentResolverError
) : ImportError(cause)

class Download(
override val cause: HttpError
) : ImportError(cause)
Expand All @@ -57,6 +62,7 @@ sealed class ImportError(
is Opds -> UserError(R.string.import_publication_no_acquisition, cause = this)
is Publication -> cause.toUserError()
is FileSystem -> cause.toUserError()
is ContentResolver -> cause.toUserError()
is InconsistentState -> UserError(
R.string.import_publication_inconsistent_state,
cause = this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ import timber.log.Timber
class PublicationRetriever(
context: Context,
private val assetRetriever: AssetRetriever,
httpClient: HttpClient,
private val httpClient: HttpClient,
lcpService: LcpService?,
private val bookshelfDir: File,
tempDir: File
private val tempDir: File
) {
data class Result(
val publication: File,
Expand Down Expand Up @@ -109,6 +109,46 @@ class PublicationRetriever(
)
}

suspend fun retrieveFromHttp(
url: AbsoluteUrl
): Try<Result, ImportError> {
val request = HttpRequest(
url,
headers = emptyMap()
)

val tempFile = when (val result = httpClient.stream(request)) {
is Try.Failure ->
return Try.failure(ImportError.Download(result.value))
is Try.Success -> {
result.value.body
.copyToNewFile(tempDir)
.getOrElse { return Try.failure(ImportError.FileSystem(it)) }
}
}

val localResult = localPublicationRetriever
.retrieve(tempFile)
.getOrElse {
tryOrLog { tempFile.delete() }
return Try.failure(it)
}

val finalResult = moveToBookshelfDir(
localResult.tempFile,
localResult.format,
localResult.coverUrl
)
.getOrElse {
tryOrLog { localResult.tempFile.delete() }
return Try.failure(it)
}

return Try.success(
Result(finalResult.publication, finalResult.format, finalResult.coverUrl)
)
}

private suspend fun moveToBookshelfDir(
tempFile: File,
format: Format?,
Expand Down Expand Up @@ -167,7 +207,7 @@ private class LocalPublicationRetriever(
): Try<Result, ImportError> {
val tempFile = uri.copyToTempFile(context, tempDir)
.getOrElse {
return Try.failure(ImportError.FileSystem(FileSystemError.IO(it)))
return Try.failure(ImportError.ContentResolver(it))
}
return retrieveFromStorage(tempFile, coverUrl = null)
.onFailure { tryOrLog { tempFile.delete() } }
Expand Down
Loading

0 comments on commit 94ba891

Please sign in to comment.