Skip to content

Commit 8871b4b

Browse files
committed
feat(core-file): add core file module for the FileSystemManager
# Conflicts: # app-common/build.gradle.kts # app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt
1 parent 598647e commit 8871b4b

File tree

17 files changed

+193
-25
lines changed

17 files changed

+193
-25
lines changed

app-common/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ dependencies {
3131
implementation(projects.core.configstore.implBackend)
3232

3333
implementation(projects.core.featureflag)
34+
implementation(projects.core.file)
3435

3536
implementation(projects.core.ui.setting.api)
3637
implementation(projects.core.ui.setting.implDialog)

app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package net.thunderbird.app.common.core
22

3+
import android.content.Context
34
import net.thunderbird.app.common.core.configstore.appCommonCoreConfigStoreModule
45
import net.thunderbird.app.common.core.logging.appCommonCoreLogger
56
import net.thunderbird.app.common.core.ui.appCommonCoreUiModule
7+
import net.thunderbird.core.file.AndroidFileSystemManager
8+
import net.thunderbird.core.file.FileSystemManager
69
import org.koin.core.module.Module
710
import org.koin.dsl.module
811

@@ -12,4 +15,10 @@ val appCommonCoreModule: Module = module {
1215
appCommonCoreLogger,
1316
appCommonCoreUiModule,
1417
)
18+
19+
single<FileSystemManager> {
20+
AndroidFileSystemManager(
21+
contentResolver = get<Context>().contentResolver,
22+
)
23+
}
1524
}

app-common/src/main/kotlin/net/thunderbird/app/common/core/logging/LoggerModule.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import net.thunderbird.core.logging.LogSink
1313
import net.thunderbird.core.logging.Logger
1414
import net.thunderbird.core.logging.composite.CompositeLogSink
1515
import net.thunderbird.core.logging.console.ConsoleLogSink
16-
import net.thunderbird.core.logging.file.AndroidFileSystemManager
1716
import net.thunderbird.core.logging.file.FileLogSink
1817
import org.koin.core.qualifier.named
1918
import org.koin.dsl.bind
@@ -64,7 +63,7 @@ val appCommonCoreLogger = module {
6463
level = LogLevel.DEBUG,
6564
fileName = "thunderbird-sync-debug",
6665
fileLocation = get<Context>().filesDir.path,
67-
fileSystemManager = AndroidFileSystemManager(get<Context>().contentResolver),
66+
fileSystemManager = get(),
6867
)
6968
}
7069

core/file/build.gradle.kts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
plugins {
2+
id(ThunderbirdPlugins.Library.kmp)
3+
}
4+
5+
android {
6+
namespace = "net.thunderbird.core.file"
7+
}
8+
9+
kotlin {
10+
sourceSets {
11+
commonMain.dependencies {
12+
implementation(libs.kotlinx.io.core)
13+
}
14+
}
15+
}

core/logging/impl-file/src/androidMain/kotlin/net/thunderbird/core/logging/file/AndroidFileSystemManager.kt renamed to core/file/src/androidMain/kotlin/net/thunderbird/core/file/AndroidFileSystemManager.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
package net.thunderbird.core.logging.file
1+
package net.thunderbird.core.file
22

33
import android.content.ContentResolver
44
import android.net.Uri
55
import androidx.core.net.toUri
66
import kotlinx.io.RawSink
7+
import kotlinx.io.RawSource
78
import kotlinx.io.asSink
9+
import kotlinx.io.asSource
810

911
/**
1012
* Android implementation of [FileSystemManager] that uses [ContentResolver] to perform file operations.
@@ -16,4 +18,9 @@ class AndroidFileSystemManager(
1618
val uri: Uri = uriString.toUri()
1719
return contentResolver.openOutputStream(uri, mode)?.asSink()
1820
}
21+
22+
override fun openSource(uriString: String): RawSource? {
23+
val uri: Uri = uriString.toUri()
24+
return contentResolver.openInputStream(uri)?.asSource()
25+
}
1926
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package net.thunderbird.core.file
2+
3+
import android.content.Context
4+
import android.net.Uri
5+
import assertk.assertThat
6+
import assertk.assertions.isEqualTo
7+
import kotlinx.io.Buffer
8+
import org.junit.Rule
9+
import org.junit.Test
10+
import org.junit.rules.TemporaryFolder
11+
import org.junit.runner.RunWith
12+
import org.robolectric.RobolectricTestRunner
13+
import org.robolectric.RuntimeEnvironment
14+
import org.robolectric.annotation.Config
15+
16+
@RunWith(RobolectricTestRunner::class)
17+
@Config(manifest = Config.NONE)
18+
class AndroidFileSystemManagerTest {
19+
20+
private val appContext: Context = RuntimeEnvironment.getApplication()
21+
22+
private val testSubject = AndroidFileSystemManager(appContext.contentResolver)
23+
24+
@JvmField
25+
@Rule
26+
val folder = TemporaryFolder()
27+
28+
@Test
29+
fun openSinkAndOpenSource_writeAndReadFileContentRoundtrip() {
30+
// Arrange
31+
val tempFile = folder.newFile("tb-file-fs-test-android.txt")
32+
val uri: Uri = Uri.fromFile(tempFile)
33+
val testText = "Hello Thunderbird Android!"
34+
35+
// Act
36+
val sink = checkNotNull(testSubject.openSink(uri.toString(), "wt"))
37+
val writeBuffer = Buffer().apply { write(testText.encodeToByteArray()) }
38+
sink.write(writeBuffer, writeBuffer.size)
39+
sink.flush()
40+
sink.close()
41+
42+
val source = checkNotNull(testSubject.openSource(uri.toString()))
43+
val readBuffer = Buffer()
44+
source.readAtMostTo(readBuffer, 1024)
45+
val bytes = ByteArray(readBuffer.size.toInt())
46+
for (i in bytes.indices) {
47+
bytes[i] = readBuffer.readByte()
48+
}
49+
val result = bytes.decodeToString()
50+
source.close()
51+
52+
// Assert
53+
assertThat(result).isEqualTo(testText)
54+
}
55+
}

core/logging/impl-file/src/commonMain/kotlin/net/thunderbird/core/logging/file/FileSystemManager.kt renamed to core/file/src/commonMain/kotlin/net/thunderbird/core/file/FileSystemManager.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
package net.thunderbird.core.logging.file
1+
package net.thunderbird.core.file
22

33
import kotlinx.io.RawSink
4+
import kotlinx.io.RawSource
45

56
/**
67
* An interface for file system operations that are platform-specific.
@@ -14,4 +15,12 @@ interface FileSystemManager {
1415
* @return A sink for writing to the URI, or null if the URI couldn't be opened
1516
*/
1617
fun openSink(uriString: String, mode: String): RawSink?
18+
19+
/**
20+
* Opens a source for reading from a URI.
21+
*
22+
* @param uriString The URI string to open a source for
23+
* @return A source for reading from the URI, or null if the URI couldn't be opened
24+
*/
25+
fun openSource(uriString: String): RawSource?
1726
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package net.thunderbird.core.file
2+
3+
import java.io.File
4+
import java.io.FileInputStream
5+
import java.io.FileOutputStream
6+
import kotlinx.io.RawSink
7+
import kotlinx.io.RawSource
8+
import kotlinx.io.asSink
9+
import kotlinx.io.asSource
10+
11+
/**
12+
* JVM implementation of [FileSystemManager] using java.io streams.
13+
*/
14+
class JvmFileSystemManager : FileSystemManager {
15+
override fun openSink(uriString: String, mode: String): RawSink? {
16+
// Only support simple file paths for JVM implementation
17+
return try {
18+
val file = File(uriString)
19+
// create parent directories if necessary
20+
file.parentFile?.mkdirs()
21+
val append = mode.contains("a") // crude check for append mode
22+
FileOutputStream(file, append).asSink()
23+
} catch (_: Throwable) {
24+
null
25+
}
26+
}
27+
28+
override fun openSource(uriString: String): RawSource? {
29+
return try {
30+
val file = File(uriString)
31+
FileInputStream(file).asSource()
32+
} catch (_: Throwable) {
33+
null
34+
}
35+
}
36+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package net.thunderbird.core.file
2+
3+
import assertk.assertThat
4+
import assertk.assertions.isEqualTo
5+
import java.io.File
6+
import kotlinx.io.Buffer
7+
import org.junit.Rule
8+
import org.junit.Test
9+
import org.junit.rules.TemporaryFolder
10+
11+
class JvmFileSystemManagerTest {
12+
13+
private val testSubject = JvmFileSystemManager()
14+
15+
@JvmField
16+
@Rule
17+
val folder = TemporaryFolder()
18+
19+
@Test
20+
fun `openSink and openSource should write and read file content roundtrip`() {
21+
// Arrange
22+
val tempFile: File = folder.newFile("tb-file-fs-test.txt")
23+
val testText = "Hello Thunderbird!"
24+
val sink = checkNotNull(testSubject.openSink(tempFile.absolutePath, "wt"))
25+
26+
// Act
27+
val writeBuffer = Buffer().apply { write(testText.encodeToByteArray()) }
28+
sink.write(writeBuffer, writeBuffer.size)
29+
sink.flush()
30+
sink.close()
31+
32+
val source = checkNotNull(testSubject.openSource(tempFile.absolutePath))
33+
val readBuffer = Buffer()
34+
source.readAtMostTo(readBuffer, 1024)
35+
val bytes = ByteArray(readBuffer.size.toInt())
36+
for (i in bytes.indices) {
37+
bytes[i] = readBuffer.readByte()
38+
}
39+
val result = bytes.decodeToString()
40+
source.close()
41+
42+
// Assert
43+
assertThat(result).isEqualTo(testText)
44+
}
45+
}

core/logging/impl-file/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ kotlin {
1111
commonMain.dependencies {
1212
implementation(libs.kotlinx.io.core)
1313
implementation(projects.core.logging.api)
14+
implementation(projects.core.file)
1415
}
1516
}
1617
}

0 commit comments

Comments
 (0)