feature/18-add-existing-files-cache #26
|
@ -50,7 +50,7 @@ dependencies {
|
|||
implementation("org.springframework.boot:spring-boot-devtools:${springBootVersion}")
|
||||
|
||||
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||
testImplementation("io.mockk:mockk:1.12.0")
|
||||
testImplementation("io.mockk:mockk:1.12.1")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test:${kotlinVersion}")
|
||||
testImplementation("org.mockito:mockito-inline:3.11.2")
|
||||
testImplementation("org.springframework:spring-test:5.3.13")
|
||||
|
@ -68,8 +68,8 @@ springBoot {
|
|||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
@ -83,15 +83,18 @@ sourceSets {
|
|||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
|
||||
jvmArgs("-XX:+ShowCodeDetailsInExceptionMessages")
|
||||
testLogging {
|
||||
events("skipped", "failed")
|
||||
setExceptionFormat("full")
|
||||
}
|
||||
|
||||
reports {
|
||||
junitXml.required.set(true)
|
||||
html.required.set(false)
|
||||
}
|
||||
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events("skipped", "failed")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile>() {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||
|
||||
class FileExistCache {
|
||||
private val map = hashMapOf<FilePath, Boolean>()
|
||||
|
||||
operator fun contains(path: FilePath) =
|
||||
path in map
|
||||
|
||||
fun exists(path: FilePath) =
|
||||
map[path] ?: false
|
||||
|
||||
fun set(path: FilePath, exists: Boolean) {
|
||||
map[path] = exists
|
||||
}
|
||||
|
||||
fun setExists(path: FilePath) =
|
||||
set(path, true)
|
||||
|
||||
fun setDoesNotExists(path: FilePath) =
|
||||
set(path, false)
|
||||
}
|
|
@ -2,6 +2,9 @@ package dev.fyloz.colorrecipesexplorer.service.files
|
|||
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||
import dev.fyloz.colorrecipesexplorer.utils.WrappedFile
|
||||
import dev.fyloz.colorrecipesexplorer.utils.withFileAt
|
||||
import org.slf4j.Logger
|
||||
import org.springframework.core.io.ByteArrayResource
|
||||
import org.springframework.core.io.Resource
|
||||
|
@ -49,8 +52,17 @@ class FileServiceImpl(
|
|||
private val creProperties: CreProperties,
|
||||
private val logger: Logger
|
||||
) : WriteableFileService {
|
||||
override fun exists(path: String) = withFileAt(path.fullPath()) {
|
||||
this.exists() && this.isFile
|
||||
private val existsCache = FileExistCache()
|
||||
|
||||
override fun exists(path: String): Boolean {
|
||||
val fullPath = path.fullPath()
|
||||
return if (fullPath in existsCache) {
|
||||
existsCache.exists(fullPath)
|
||||
} else {
|
||||
withFileAt(fullPath) {
|
||||
this.exists() && this.isFile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun read(path: String) = ByteArrayResource(
|
||||
|
@ -70,6 +82,7 @@ class FileServiceImpl(
|
|||
try {
|
||||
withFileAt(fullPath) {
|
||||
this.create()
|
||||
existsCache.setExists(fullPath)
|
||||
}
|
||||
} catch (ex: IOException) {
|
||||
FileCreateException(path).logAndThrow(ex, logger)
|
||||
|
@ -89,9 +102,12 @@ class FileServiceImpl(
|
|||
|
||||
override fun delete(path: String) {
|
||||
try {
|
||||
withFileAt(path.fullPath()) {
|
||||
val fullPath = path.fullPath()
|
||||
withFileAt(fullPath) {
|
||||
if (!exists(path)) throw FileNotFoundException(path)
|
||||
!this.delete()
|
||||
|
||||
this.delete()
|
||||
existsCache.setDoesNotExists(fullPath)
|
||||
}
|
||||
} catch (ex: IOException) {
|
||||
FileDeleteException(path).logAndThrow(ex, logger)
|
||||
|
@ -106,7 +122,7 @@ class FileServiceImpl(
|
|||
return FilePath("${creProperties.dataDirectory}/$this")
|
||||
}
|
||||
|
||||
private fun prepareWrite(path: String, overwrite: Boolean, op: File.() -> Unit) {
|
||||
private fun prepareWrite(path: String, overwrite: Boolean, op: WrappedFile.() -> Unit) {
|
||||
val fullPath = path.fullPath()
|
||||
|
||||
if (exists(path)) {
|
||||
|
@ -123,15 +139,6 @@ class FileServiceImpl(
|
|||
FileWriteException(path).logAndThrow(ex, logger)
|
||||
}
|
||||
}
|
||||
|
||||
/** Runs the given [block] in the context of a file with the given [fullPath]. */
|
||||
private fun <T> withFileAt(fullPath: FilePath, block: File.() -> T) =
|
||||
fullPath.file.block()
|
||||
}
|
||||
|
||||
data class FilePath(val path: String) {
|
||||
val file: File
|
||||
get() = File(path)
|
||||
}
|
||||
|
||||
/** Shortcut to create a file and its parent directories. */
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||
import org.springframework.core.io.Resource
|
||||
import org.springframework.core.io.ResourceLoader
|
||||
import org.springframework.stereotype.Service
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package dev.fyloz.colorrecipesexplorer.utils
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.service.files.create
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
|
||||
/** Mockable file wrapper, to prevent issues when mocking [java.io.File]. */
|
||||
class WrappedFile(val file: File) {
|
||||
val isFile: Boolean
|
||||
get() = file.isFile
|
||||
|
||||
fun toPath(): Path =
|
||||
file.toPath()
|
||||
|
||||
fun exists() =
|
||||
file.exists()
|
||||
|
||||
fun readBytes() =
|
||||
file.readBytes()
|
||||
|
||||
fun writeBytes(array: ByteArray) =
|
||||
file.writeBytes(array)
|
||||
|
||||
fun create() =
|
||||
file.create()
|
||||
|
||||
fun delete(): Boolean =
|
||||
file.delete()
|
||||
|
||||
companion object {
|
||||
fun from(path: String) =
|
||||
WrappedFile(File(path))
|
||||
|
||||
fun from(path: FilePath) =
|
||||
from(path.path)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
value class FilePath(val path: String)
|
||||
|
||||
/** Runs the given [block] in the context of a file with the given [fullPath]. */
|
||||
fun <T> withFileAt(fullPath: FilePath, block: WrappedFile.() -> T) =
|
||||
WrappedFile.from(fullPath).block()
|
|
@ -1,6 +1,7 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.utils.WrappedFile
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -24,20 +25,18 @@ private class FileServiceTestContext {
|
|||
val fileService = spyk(FileServiceImpl(creProperties, mockk {
|
||||
every { error(any(), any<Exception>()) } just Runs
|
||||
}))
|
||||
val mockFile = mockk<File> {
|
||||
every { path } returns mockFilePath
|
||||
val mockFile = mockk<WrappedFile> {
|
||||
every { file } returns mockk()
|
||||
every { exists() } returns true
|
||||
every { isFile } returns true
|
||||
every { toPath() } returns mockFilePathPath
|
||||
}
|
||||
val mockFileFullPath = spyk(FilePath("${creProperties.dataDirectory}/$mockFilePath")) {
|
||||
every { file } returns mockFile
|
||||
|
||||
with(fileService) {
|
||||
every { mockFilePath.fullPath() } returns this@spyk
|
||||
}
|
||||
}
|
||||
val mockMultipartFile = spyk(MockMultipartFile(mockFilePath, mockFileData))
|
||||
|
||||
init {
|
||||
mockkObject(WrappedFile.Companion)
|
||||
every { WrappedFile.from(any<String>()) } returns mockFile
|
||||
}
|
||||
}
|
||||
|
||||
class FileServiceTest {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||
import io.mockk.clearAllMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
|
Loading…
Reference in New Issue