Skip to content

Commit

Permalink
Create an internal delegate class to ensure log folder exists before …
Browse files Browse the repository at this point in the history
…each operation.
  • Loading branch information
airxnoor committed May 28, 2024
1 parent bb0102e commit 37116bb
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 53 deletions.
5 changes: 5 additions & 0 deletions src/commonMain/kotlin/com/airthings/lib/logging/TypeAlias.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,8 @@ internal typealias LoggerFacilitiesMap = Map<String, LoggerFacility>
* An alias for a [MutableMap] of [LoggerFacility] instances associated by their unique names.
*/
internal typealias LoggerFacilitiesMutableMap = MutableMap<String, LoggerFacility>

/**
* An alias for a function that handles a missing folder incident.
*/
internal typealias LogFolderMissingHandler = (String) -> Unit
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import com.airthings.lib.logging.LogMessage
import com.airthings.lib.logging.LoggerFacility
import com.airthings.lib.logging.dateStamp
import com.airthings.lib.logging.datetimeStampPrefix
import com.airthings.lib.logging.platform.DelegateFileInputOutput
import com.airthings.lib.logging.platform.PlatformDirectoryListing
import com.airthings.lib.logging.platform.PlatformFileInputOutput
import com.airthings.lib.logging.platform.PlatformFileInputOutputImpl
Expand Down Expand Up @@ -128,15 +129,15 @@ class FileLoggerFacility(
notifier = null,
)

private val io: PlatformFileInputOutput = PlatformFileInputOutputImpl()
private val io: PlatformFileInputOutput = DelegateFileInputOutput(
folder = baseFolder,
io = PlatformFileInputOutputImpl(),
onFolderMissing = {
notifier?.onLogFolderInvalid(it) ?: DelegateFileInputOutput.reportMissingFolder(it)
},
)
private val currentLogFile = AtomicReference<String?>(null)

init {
coroutineScope.launch {
ensureBaseFolder()
}
}

/**
* Returns the platform-dependent [PlatformDirectoryListing] instance.
*/
Expand Down Expand Up @@ -196,10 +197,7 @@ class FileLoggerFacility(
*
* Note: The returned list contains absolute (canonical) paths to the files.
*/
suspend fun files(): Collection<String> {
ensureBaseFolder()
return io.of(baseFolder)
}
suspend fun files(): Collection<String> = io.of(baseFolder)

/**
* Scans the [baseFolder] and returns the list of log files residing in it that
Expand All @@ -209,18 +207,14 @@ class FileLoggerFacility(
*
* @param date The date from which log files should be considered.
*/
suspend fun files(date: LogDate): Collection<String> {
ensureBaseFolder()
return io.of(baseFolder, date)
}
suspend fun files(date: LogDate): Collection<String> = io.of(baseFolder, date)

/**
* Deletes a file residing in [baseFolder].
*
* @param name The file name to delete.
*/
suspend fun delete(name: String) {
ensureBaseFolder()
io.delete("$baseFolder${io.pathSeparator}$name")
}

Expand All @@ -233,14 +227,6 @@ class FileLoggerFacility(
io.delete(path)
}

private suspend fun ensureBaseFolder() {
// Please note: The call to `io.mkdirs()` returns true if the directory exists, which may be
// different from the platform's implementation.
if (!io.mkdirs(baseFolder)) {
throw IllegalArgumentException("Base log folder is invalid: $baseFolder")
}
}

private fun withLogLevel(
level: LogLevel,
action: suspend (String) -> Unit,
Expand All @@ -250,8 +236,6 @@ class FileLoggerFacility(
}

coroutineScope.launch {
ensureBaseFolder()

val logFile = "$baseFolder${io.pathSeparator}${dateStamp(null)}.log"
val currentLogFileLocked = currentLogFile.value

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.airthings.lib.logging.LogMessage
import com.airthings.lib.logging.LoggerFacility
import com.airthings.lib.logging.dateStamp
import com.airthings.lib.logging.datetimeStamp
import com.airthings.lib.logging.platform.DelegateFileInputOutput
import com.airthings.lib.logging.platform.PlatformDirectoryListing
import com.airthings.lib.logging.platform.PlatformFileInputOutput
import com.airthings.lib.logging.platform.PlatformFileInputOutputImpl
Expand Down Expand Up @@ -128,15 +129,15 @@ class JsonLoggerFacility(
notifier = null,
)

private val io: PlatformFileInputOutput = PlatformFileInputOutputImpl()
private val io: PlatformFileInputOutput = DelegateFileInputOutput(
folder = baseFolder,
io = PlatformFileInputOutputImpl(),
onFolderMissing = {
notifier?.onLogFolderInvalid(it) ?: DelegateFileInputOutput.reportMissingFolder(it)
},
)
private val currentLogFile = AtomicReference<String?>(null)

init {
coroutineScope.launch {
ensureBaseFolder()
}
}

/**
* Returns the platform-dependent [PlatformDirectoryListing] instance.
*/
Expand Down Expand Up @@ -217,28 +218,14 @@ class JsonLoggerFacility(
/**
* Scans the [baseFolder] and returns the list of log files residing in it.
*/
suspend fun files(): Collection<String> {
ensureBaseFolder()
return io.of(baseFolder)
}
suspend fun files(): Collection<String> = io.of(baseFolder)

/**
* Scans the [baseFolder] and returns the list of log files residing in it that were created after a certain date.
*
* @param date The date from which log files should be considered.
*/
suspend fun files(date: LogDate): Collection<String> {
ensureBaseFolder()
return io.of(baseFolder, date)
}

private suspend fun ensureBaseFolder() {
// Please note: The call to `io.mkdirs()` returns true if the directory exists, which may be
// different from the platform's implementation.
if (!io.mkdirs(baseFolder)) {
throw IllegalArgumentException("Base log folder is invalid: $baseFolder")
}
}
suspend fun files(date: LogDate): Collection<String> = io.of(baseFolder, date)

private fun withLogLevel(
level: LogLevel,
Expand All @@ -249,8 +236,6 @@ class JsonLoggerFacility(
}

coroutineScope.launch {
ensureBaseFolder()

val logFile = "$baseFolder${io.pathSeparator}${dateStamp(null)}.json"
val currentLogFileLocked = currentLogFile.value

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright 2024 Airthings ASA. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the “Software”), to deal in the Software without
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package com.airthings.lib.logging.platform

import com.airthings.lib.logging.LogDate
import com.airthings.lib.logging.LogFolderMissingHandler

internal class DelegateFileInputOutput(
private val folder: String,
private val io: PlatformFileInputOutput,
private val onFolderMissing: LogFolderMissingHandler? = null,
) : PlatformFileInputOutput {
override val pathSeparator: Char = io.pathSeparator

override suspend fun mkdirs(path: String): Boolean = ensureDir {
mkdirs(path)
}

override suspend fun size(path: String): Long = ensureDir {
size(path)
}

override suspend fun write(
path: String,
position: Long,
contents: String,
) = ensureDir {
write(
path = path,
position = position,
contents = contents,
)
}

override suspend fun append(
path: String,
contents: String,
) = ensureDir {
append(
path = path,
contents = contents,
)
}

override suspend fun ensure(path: String) = ensureDir {
ensure(path)
}

override suspend fun delete(path: String) = ensureDir {
delete(path)
}

override suspend fun of(path: String): Collection<String> = ensureDir {
of(path)
}

override suspend fun of(
path: String,
date: LogDate,
): Collection<String> = ensureDir {
of(
path = path,
date = date,
)
}

private suspend fun <T> ensureDir(
action: suspend PlatformFileInputOutput.() -> T,
): T = with(io) {
val isDirectoryExists = mkdirs(folder)
try {
io.action()
} finally {
if (!isDirectoryExists) {
onFolderMissing?.invoke(folder)
}
}
}

companion object {
/**
* Default handler when a folder is invalid.
*
* @param folder The invalid folder's path.
*/
fun reportMissingFolder(folder: String) {
throw IllegalStateException("Log folder is invalid: $folder")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
package com.airthings.lib.logging.platform

/**
* Defines a contract that notifies about opening and closing log files.
* Defines a contract that notifies about opening and closing log files and folders.
*/
interface PlatformFileInputOutputNotifier {
/**
Expand All @@ -36,4 +36,11 @@ interface PlatformFileInputOutputNotifier {
* @param path The location of the log file.
*/
fun onLogFileClosed(path: String)

/**
* Invoked when the log folder cannot be prepared and is effectively invalid.
*
* @param path The location of the log folder.
*/
fun onLogFolderInvalid(path: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import com.airthings.lib.logging.ifAfter
import java.io.File
import java.io.FileOutputStream

internal actual class PlatformFileInputOutputImpl : PlatformFileInputOutput {
internal actual class PlatformFileInputOutputImpl : DelegateFileInputOutput() {
private val writeLock = Any()

override val pathSeparator: Char = File.separatorChar
Expand Down

0 comments on commit 37116bb

Please sign in to comment.