Skip to content

Commit c15b749

Browse files
sentinelwebsentinelweb
authored andcommitted
#496 - build diskspaceutil
1 parent 318c796 commit c15b749

File tree

20 files changed

+532
-9
lines changed

20 files changed

+532
-9
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// DiskSpaceDomain.kt
2+
package uk.co.sentinelweb.cuer.domain
3+
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
data class DiskVolumeDomain(
8+
val name: String,
9+
val mountPoint: String,
10+
val totalSpace: Long,
11+
val usedSpace: Long,
12+
val availableSpace: Long,
13+
val isRemovable: Boolean = false
14+
) : Domain {
15+
val usedPercentage: Float
16+
get() = if (totalSpace > 0) usedSpace.toFloat() / totalSpace.toFloat() * 100f else 0f
17+
}
18+
19+
@Serializable
20+
data class DiskSpaceInfoDomain(
21+
val volumes: List<DiskVolumeDomain>,
22+
val deviceType: NodeDomain.DeviceType
23+
) : Domain

domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/ext/SerializationExt.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ val domainSerializersModule = SerializersModule {
181181
polymorphic(Domain::class, PlaylistAndChildrenDomain::class, PlaylistAndChildrenDomain.serializer())
182182
polymorphic(Domain::class, TransferDomain::class, TransferDomain.serializer())
183183
polymorphic(Domain::class, AddressDomain::class, AddressDomain.serializer())
184+
polymorphic(Domain::class, DiskSpaceInfoDomain::class, DiskSpaceInfoDomain.serializer())
185+
polymorphic(Domain::class, DiskVolumeDomain::class, DiskVolumeDomain.serializer())
184186

185187
polymorphic(AuthConfig::class, AuthConfig.Open::class, AuthConfig.Open.serializer())
186188
polymorphic(AuthConfig::class, AuthConfig.Confirm::class, AuthConfig.Confirm.serializer())

domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/remote/server/RemoteWebServerContract.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ interface RemoteWebServerContract {
6060

6161
}
6262

63+
object DISKSPACE_API {
64+
const val PATH = "/diskspace"
65+
}
66+
6367
object DELETE_API {
6468
// only works locally
6569
const val PATH = "/delete"

hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/di/Modules.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.russhwolf.settings.JvmPreferencesSettings
77
import com.russhwolf.settings.Settings
88
import org.koin.core.qualifier.named
99
import org.koin.dsl.module
10+
import uk.co.sentinelweb.cuer.app.DesktopSharedModule
1011
import uk.co.sentinelweb.cuer.app.db.repository.file.AFile
1112
import uk.co.sentinelweb.cuer.app.db.repository.file.AssetOperations
1213
import uk.co.sentinelweb.cuer.app.db.repository.file.ConfigDirectory
@@ -240,5 +241,6 @@ object Modules {
240241
.plus(NetModule.modules)
241242
.plus(JvmDatabaseModule.modules)
242243
.plus(DatabaseCommonModule.modules)
244+
.plus(DesktopSharedModule.modules)
243245
.plus(emptyModule)
244246
}

remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/JvmRemoteWebServer.kt

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ import io.ktor.server.plugins.callloging.*
99
import io.ktor.server.plugins.compression.*
1010
import io.ktor.server.plugins.contentnegotiation.*
1111
import io.ktor.server.plugins.cors.routing.*
12+
import io.ktor.server.request.*
1213
import io.ktor.server.routing.*
1314
import org.koin.core.component.KoinComponent
1415
import org.koin.core.component.inject
16+
import org.slf4j.event.Level
1517
import uk.co.sentinelweb.cuer.core.wrapper.LogWrapper
1618
import uk.co.sentinelweb.cuer.domain.ext.domainMessageJsonSerializer
1719
import uk.co.sentinelweb.cuer.remote.server.controller.*
20+
import java.util.*
1821

1922
// todo break this up into use cases
2023
class JvmRemoteWebServer(
@@ -70,8 +73,41 @@ class JvmRemoteWebServer(
7073
gzip()
7174
}
7275
install(CallLogging) {
73-
//level = Level.DEBUG
76+
level = Level.INFO
77+
filter { call -> true }
78+
format { call ->
79+
val status = call.response.status()
80+
val httpMethod = call.request.httpMethod.value
81+
val uri = call.request.uri
82+
"$httpMethod $uri - $status"
83+
}
84+
mdc("requestId") { call -> UUID.randomUUID().toString() }
7485
}
86+
// intercept(ApplicationCallPipeline.Monitoring) {
87+
// val requestId = call.attributes.getOrNull(requestIdKey) ?: UUID.randomUUID().toString()
88+
//
89+
// // Log request details
90+
// application.log.info("Request: ${call.request.httpMethod.value} ${call.request.uri} [ID: $requestId]")
91+
//
92+
// // Record timing
93+
// val startTime = System.currentTimeMillis()
94+
//
95+
// try {
96+
// // Proceed with the request
97+
// proceed()
98+
// } finally {
99+
// val duration = System.currentTimeMillis() - startTime
100+
// val status = call.response.status() ?: HttpStatusCode.InternalServerError
101+
//
102+
// // Log response
103+
// if (status.value >= 400) {
104+
// application.log.error("Response: $status in ${duration}ms [ID: $requestId]")
105+
// } else {
106+
// application.log.info("Response: $status in ${duration}ms [ID: $requestId]")
107+
// }
108+
// }
109+
// }
110+
75111
routing {
76112
with(webpageController) { registerRoutes() }
77113
with(playlistController) { registerRoutes() }

remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/controller/TransferController.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
// TransferController.kt
22
package uk.co.sentinelweb.cuer.remote.server.controller
33

4+
import io.ktor.http.*
5+
import io.ktor.http.HttpStatusCode.Companion.InternalServerError
6+
import io.ktor.server.application.*
7+
import io.ktor.server.response.*
48
import io.ktor.server.routing.*
9+
import uk.co.sentinelweb.cuer.app.usecase.GetDiskSpaceUseCase
510
import uk.co.sentinelweb.cuer.core.wrapper.LogWrapper
11+
import uk.co.sentinelweb.cuer.domain.ext.serialise
12+
import uk.co.sentinelweb.cuer.domain.system.ErrorDomain
13+
import uk.co.sentinelweb.cuer.domain.system.ErrorDomain.Level.ERROR
14+
import uk.co.sentinelweb.cuer.domain.system.ErrorDomain.Type.HTTP
15+
import uk.co.sentinelweb.cuer.domain.system.ResponseDomain
16+
import uk.co.sentinelweb.cuer.remote.server.RemoteWebServerContract.Companion.DISKSPACE_API
617
import uk.co.sentinelweb.cuer.remote.server.RemoteWebServerContract.Companion.TRANSFER_API
718

819
class TransferController(
20+
private val diskSpaceUseCase: GetDiskSpaceUseCase,
921
private val logWrapper: LogWrapper
1022
) : BaseController {
1123
init {
@@ -16,5 +28,27 @@ class TransferController(
1628
post(TRANSFER_API.PATH) {
1729

1830
}
31+
32+
get(DISKSPACE_API.PATH) {
33+
try {
34+
val diskSpaceInfo = diskSpaceUseCase.getDiskSpace()
35+
ResponseDomain(diskSpaceInfo)
36+
.also { call.respondText(it.serialise(), ContentType.Application.Json) }
37+
} catch (e: Exception) {
38+
application.log.error("Error getting disk space", e)
39+
call.respondText(
40+
text = ResponseDomain(
41+
ErrorDomain(
42+
level = ERROR,
43+
type = HTTP,
44+
code = InternalServerError.value,
45+
message = "Failed to get disk space: ${e.message}"
46+
)
47+
).serialise(),
48+
contentType = ContentType.Application.Json,
49+
status = InternalServerError
50+
)
51+
}
52+
}
1953
}
2054
}

remote/src/jvmAndAndroid/kotlin/uk/co/sentinelweb/cuer/remote/server/di/RemoteModule.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ object RemoteModule {
5959
}
6060
factory {
6161
TransferController(
62-
logWrapper = get()
62+
logWrapper = get(),
63+
diskSpaceUseCase = get()
6364
)
6465
}
6566
factory {

shared/src/androidMain/kotlin/uk/co/sentinelweb/cuer/app/di/SharedAppAndroidModule.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ package uk.co.sentinelweb.cuer.app.di
33
import org.koin.android.ext.koin.androidContext
44
import org.koin.dsl.module
55
import uk.co.sentinelweb.cuer.app.db.repository.file.AssetOperations
6+
import uk.co.sentinelweb.cuer.app.util.disk.AndroidDiskSpacePlatformUtil
7+
import uk.co.sentinelweb.cuer.app.util.disk.DiskSpacePlatformUtil
68

79
object SharedAppAndroidModule {
810

911
private val utilModule = module {
1012
factory { AssetOperations(androidContext()) }
13+
factory<DiskSpacePlatformUtil> { AndroidDiskSpacePlatformUtil(androidContext()) }
1114
}
1215

1316
val modules = listOf(utilModule)
14-
}
17+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// AndroidDiskSpaceUtil.kt
2+
package uk.co.sentinelweb.cuer.app.util.disk
3+
4+
import android.content.Context
5+
import android.os.Build
6+
import android.os.Environment
7+
import android.os.StatFs
8+
import android.os.storage.StorageManager
9+
import uk.co.sentinelweb.cuer.domain.DiskVolumeDomain
10+
11+
class AndroidDiskSpacePlatformUtil(
12+
private val context: Context
13+
) : DiskSpacePlatformUtil {
14+
15+
override suspend fun getVolumes(): List<DiskVolumeDomain> {
16+
val volumes = mutableListOf<DiskVolumeDomain>()
17+
18+
// Add internal storage
19+
volumes.add(getInternalStorageInfo())
20+
21+
// Check for external storage if available
22+
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
23+
getExternalStorageInfo()?.let { volumes.add(it) }
24+
}
25+
26+
// On newer Android versions, we can get all storage volumes
27+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
28+
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
29+
val storageVolumes = storageManager.storageVolumes
30+
31+
for (volume in storageVolumes) {
32+
// Skip volumes we've already added
33+
if (volume.isPrimary && volumes.any { it.name == "Internal Storage" }) {
34+
continue
35+
}
36+
37+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
38+
// Android 11+ allows direct access to storage volume path
39+
volume.directory?.let { directory ->
40+
val stats = StatFs(directory.path)
41+
volumes.add(
42+
DiskVolumeDomain(
43+
name = if (volume.isPrimary) "Internal Storage" else volume.getDescription(context),
44+
mountPoint = directory.path,
45+
totalSpace = stats.totalBytes,
46+
usedSpace = stats.totalBytes - stats.availableBytes,
47+
availableSpace = stats.availableBytes,
48+
isRemovable = !volume.isPrimary
49+
)
50+
)
51+
}
52+
}
53+
}
54+
}
55+
56+
return volumes
57+
}
58+
59+
private fun getInternalStorageInfo(): DiskVolumeDomain {
60+
val path = Environment.getDataDirectory()
61+
val stat = StatFs(path.path)
62+
val totalSpace = stat.totalBytes
63+
val availableSpace = stat.availableBytes
64+
65+
return DiskVolumeDomain(
66+
name = "Internal Storage",
67+
mountPoint = path.absolutePath,
68+
totalSpace = totalSpace,
69+
usedSpace = totalSpace - availableSpace,
70+
availableSpace = availableSpace,
71+
isRemovable = false
72+
)
73+
}
74+
75+
private fun getExternalStorageInfo(): DiskVolumeDomain? {
76+
val path = Environment.getExternalStorageDirectory() ?: return null
77+
val stat = StatFs(path.path)
78+
val totalSpace = stat.totalBytes
79+
val availableSpace = stat.availableBytes
80+
81+
return DiskVolumeDomain(
82+
name = "SD Card",
83+
mountPoint = path.absolutePath,
84+
totalSpace = totalSpace,
85+
usedSpace = totalSpace - availableSpace,
86+
availableSpace = availableSpace,
87+
isRemovable = true
88+
)
89+
}
90+
}

shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/di/SharedAppModule.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,12 @@ object SharedAppModule {
226226
log = get(),
227227
)
228228
}
229-
229+
factory {
230+
GetDiskSpaceUseCase(
231+
buildConfigDomain = get(),
232+
diskSpacePlatformUtil = get()
233+
)
234+
}
230235
}
231236

232237
private val playerModule = module {

0 commit comments

Comments
 (0)