Merge pull request #3474 from davotoula/fix/unchecked-file-delete-results

Handle unchecked File.delete() return values across all modules
This commit is contained in:
Vitor Pamplona
2026-07-04 11:59:21 -04:00
committed by GitHub
17 changed files with 308 additions and 45 deletions
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service.images
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.sha256.sha256
@@ -68,9 +69,7 @@ class ThumbnailDiskCache(
BitmapFactory.decodeFile(file.absolutePath)
} catch (e: Exception) {
Log.w("ThumbnailDiskCache", "Failed to decode cached thumbnail, deleting: ${file.absolutePath}", e)
if (!file.delete()) {
Log.w("ThumbnailDiskCache") { "Failed to delete corrupt cache file: ${file.absolutePath}" }
}
file.deleteOrWarn("ThumbnailDiskCache", "corrupt cache file")
null
}
}
@@ -155,9 +154,7 @@ class ThumbnailDiskCache(
scaled.recycle()
if (!tempFile.renameTo(finalFile)) {
Log.w("ThumbnailDiskCache") { "Failed to rename temp thumbnail to final: ${tempFile.absolutePath}" }
if (!tempFile.delete()) {
Log.w("ThumbnailDiskCache") { "Failed to delete temp thumbnail: ${tempFile.absolutePath}" }
}
tempFile.deleteOrWarn("ThumbnailDiskCache", "temp thumbnail")
return false
}
@@ -193,7 +190,7 @@ class ThumbnailDiskCache(
files
.sortedBy { it.lastModified() }
.take(files.size - maxEntries)
.forEach { it.delete() }
.forEach { it.deleteOrWarn("ThumbnailDiskCache", "thumbnail") }
}
}
@@ -28,6 +28,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
@@ -155,9 +156,8 @@ class VoiceReplyViewModel : ViewModel() {
private fun deleteVoiceLocalFile() {
voiceLocalFile?.let { file ->
try {
if (file.exists()) {
file.delete()
Log.d("VoiceReplyViewModel") { "Deleted voice file: ${file.absolutePath}" }
if (file.deleteOrWarn("VoiceReplyViewModel", "voice file")) {
Log.d("VoiceReplyViewModel") { "Voice file removed or already gone: ${file.absolutePath}" }
}
} catch (e: Exception) {
Log.w("VoiceReplyViewModel", "Failed to delete voice file: ${file.absolutePath}", e)
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* 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 NONINFRINGEMENT. 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.vitorpamplona.amethyst.commons.util
import com.vitorpamplona.quartz.utils.Log
import java.io.File
/**
* Deletes this file, warning on real failures only. [File.delete] returns false
* both when deletion genuinely fails and when the file is already gone (e.g.
* removed by a concurrent eviction in another thread or process); only the
* former deserves a warning.
*
* @param tag the log tag of the calling component
* @param what a short noun for the log message, e.g. "thumbnail" or "blob"
* @return true when the file no longer exists, whether this call deleted it or
* it was already gone; false when it still exists and could not be deleted.
*/
fun File.deleteOrWarn(
tag: String,
what: String,
): Boolean {
if (delete() || !exists()) return true
Log.w(tag) { "Failed to delete $what $absolutePath" }
return false
}
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.commons.keystorage
import com.github.javakeyring.BackendNotSupportedException
import com.github.javakeyring.Keyring
import com.github.javakeyring.PasswordAccessException
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -228,7 +229,7 @@ actual class SecureKeyStorage private actual constructor() {
if (existed) {
if (data.isEmpty()) {
fallbackFile.delete()
fallbackFile.deleteOrWarn("SecureKeyStorage", "fallback key file")
} else {
atomicWriteFallbackData(fallbackFile, data)
}
@@ -275,10 +276,8 @@ actual class SecureKeyStorage private actual constructor() {
StandardCopyOption.REPLACE_EXISTING,
)
} finally {
// Clean up temp file if it still exists
if (tempFile.exists()) {
tempFile.delete()
}
// Clean up any leftover temp file
tempFile.deleteOrWarn("SecureKeyStorage", "temp key file")
}
}
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.commons.service.upload
import com.vitorpamplona.amethyst.commons.service.upload.ImageReencoder.ReencodeResult
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nipB7Blossom.BlossomUploadResult
@@ -144,8 +145,8 @@ class UploadOrchestrator(
// Eager cleanup of every intermediate. NonCancellable so a
// user-cancelled upload still cleans up its temps.
withContext(NonCancellable) {
strippedTemp?.delete()
reencodedTemp?.delete()
strippedTemp?.deleteOrWarn("UploadOrchestrator", "stripped temp")
reencodedTemp?.deleteOrWarn("UploadOrchestrator", "reencoded temp")
}
}
}
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* 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 NONINFRINGEMENT. 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.vitorpamplona.amethyst.commons.util
import com.vitorpamplona.quartz.utils.Log
import java.io.File
import java.nio.file.Files
import java.nio.file.attribute.PosixFilePermission
/**
* Restricts this file or directory to owner-only access (600 for files,
* 700 for directories), best-effort.
*
* Silent on filesystems without POSIX permissions (Windows), where the user
* profile's NTFS ACLs apply instead; warns when a POSIX filesystem refuses.
*
* @param tag the log tag of the calling component
*/
fun File.restrictToOwner(tag: String) {
try {
val permissions =
if (isDirectory) {
setOf(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.OWNER_EXECUTE,
)
} else {
setOf(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
)
}
Files.setPosixFilePermissions(toPath(), permissions)
} catch (_: UnsupportedOperationException) {
// Windows: no POSIX permissions; the user profile's NTFS ACLs apply instead.
} catch (e: Exception) {
Log.w(tag, "Could not restrict permissions on $absolutePath", e)
}
}
@@ -0,0 +1,74 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* 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 NONINFRINGEMENT. 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.vitorpamplona.amethyst.commons.util
import java.io.File
import java.nio.file.Files
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class FileDeletionTest {
private val createdFiles = mutableListOf<File>()
@AfterTest
fun cleanup() {
createdFiles.asReversed().forEach { it.delete() }
createdFiles.clear()
}
private fun tempFile(): File =
File.createTempFile("file-deletion-test", ".tmp").also {
createdFiles.add(it)
}
@Test
fun deletesExistingFileAndReturnsTrue() {
val file = tempFile()
assertTrue(file.exists())
assertTrue(file.deleteOrWarn("FileDeletionTest", "temp file"))
assertFalse(file.exists())
}
@Test
fun alreadyGoneFileIsSuccess() {
val file = tempFile()
assertTrue(file.delete())
// Simulates a concurrent eviction: the file vanished before our delete.
assertTrue(file.deleteOrWarn("FileDeletionTest", "temp file"))
}
@Test
fun undeletableFileReturnsFalseAndKeepsFile() {
// A non-empty directory cannot be deleted, and still exists afterwards —
// the genuine-failure branch.
val dir = Files.createTempDirectory("file-deletion-test").toFile()
val child = File(dir, "child.txt").apply { writeText("keeps dir non-empty") }
createdFiles.add(dir)
createdFiles.add(child)
assertFalse(dir.deleteOrWarn("FileDeletionTest", "non-empty dir"))
assertTrue(dir.exists())
}
}
@@ -0,0 +1,83 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* 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 NONINFRINGEMENT. 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.vitorpamplona.amethyst.commons.util
import java.io.File
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.attribute.PosixFilePermission
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertEquals
class FilePermissionsTest {
private val createdFiles = mutableListOf<File>()
@AfterTest
fun cleanup() {
createdFiles.asReversed().forEach { it.delete() }
createdFiles.clear()
}
private fun isPosix() = FileSystems.getDefault().supportedFileAttributeViews().contains("posix")
@Test
fun restrictsFileToOwnerReadWrite() {
if (!isPosix()) return // Windows: restrictToOwner is documented as a silent no-op
val file = File.createTempFile("file-permissions-test", ".tmp")
createdFiles.add(file)
file.restrictToOwner("FilePermissionsTest")
assertEquals(
setOf(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE),
Files.getPosixFilePermissions(file.toPath()),
)
}
@Test
fun restrictsDirectoryToOwnerReadWriteExecute() {
if (!isPosix()) return
val dir = Files.createTempDirectory("file-permissions-test").toFile()
createdFiles.add(dir)
dir.restrictToOwner("FilePermissionsTest")
assertEquals(
setOf(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.OWNER_EXECUTE,
),
Files.getPosixFilePermissions(dir.toPath()),
)
}
@Test
fun missingFileDoesNotThrow() {
val ghost = File(System.getProperty("java.io.tmpdir"), "file-permissions-test-missing-${System.nanoTime()}")
// Best-effort contract: failures are logged, never thrown.
ghost.restrictToOwner("FilePermissionsTest")
}
}
@@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.commons.keystorage.SecureKeyStorage
import com.vitorpamplona.amethyst.commons.keystorage.SecureStorageException
import com.vitorpamplona.amethyst.commons.model.account.AccountInfo
import com.vitorpamplona.amethyst.commons.model.account.SignerType
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import com.vitorpamplona.amethyst.desktop.network.DesktopHttpClient
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
@@ -260,10 +261,9 @@ class AccountManager internal constructor(
suspend fun loadSavedAccount(): Result<AccountState.LoggedIn> =
try {
// Clean up legacy files (one-time, silent)
File(amethystDir, "last_account.txt").delete()
File(amethystDir, "bunker_uri.txt").delete()
File(amethystDir, "nwc_connection.txt").delete()
// Clean up legacy files (one-time)
listOf("last_account.txt", "bunker_uri.txt", "nwc_connection.txt")
.forEach { File(amethystDir, it).deleteOrWarn("AccountManager", "legacy file") }
// Single source of truth: accounts.json.enc
val activeNpub =
@@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.commons.keystorage.SecureKeyStorage
import com.vitorpamplona.amethyst.commons.model.account.AccountInfo
import com.vitorpamplona.amethyst.commons.model.account.AccountStorage
import com.vitorpamplona.amethyst.commons.model.account.SignerType
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import com.vitorpamplona.quartz.utils.Log
import java.io.File
import java.nio.file.Files
@@ -166,7 +167,7 @@ class DesktopAccountStorage(
val backup = File(file.parent, "accounts.json.enc.corrupt.${System.currentTimeMillis()}")
java.nio.file.Files
.copy(file.toPath(), backup.toPath())
file.delete()
file.deleteOrWarn("DesktopAccountStorage", "corrupt accounts file")
backup.absolutePath
} catch (_: Exception) {
null
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.desktop.relay
import com.vitorpamplona.amethyst.commons.service.BasicBundledInsert
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
@@ -276,7 +277,7 @@ class LocalRelayStore(
private fun deleteDbFiles(path: String) {
listOf("", "-wal", "-shm", "-journal").forEach { suffix ->
File(path + suffix).delete()
File(path + suffix).deleteOrWarn("LocalRelayStore", "relay db file")
}
}
}
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.desktop.service.drafts
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -174,7 +175,7 @@ class DesktopDraftStore(
suspend fun deleteDraft(slug: String) {
val safeSlug = sanitizeSlug(slug)
mutex.withLock {
File(draftsDir, "$safeSlug.md").delete()
File(draftsDir, "$safeSlug.md").deleteOrWarn("DesktopDraftStore", "draft file")
val index = loadIndexMap()
index.remove(safeSlug)
@@ -249,7 +250,7 @@ class DesktopDraftStore(
StandardCopyOption.REPLACE_EXISTING,
)
} finally {
if (tempFile.exists()) tempFile.delete()
tempFile.deleteOrWarn("DesktopDraftStore", "temp draft file")
}
}
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.desktop.service.media
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import com.vitorpamplona.amethyst.desktop.network.DesktopHttpClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -137,7 +138,7 @@ object VideoThumbnailCache {
// a persistent cache hit (decoders may fail on every
// retry against a half-MP4). Discard so the next request
// re-downloads from scratch.
if (!downloaded.persistable) downloaded.file.delete()
if (!downloaded.persistable) downloaded.file.deleteOrWarn("VideoThumbnailCache", "truncated video chunk")
}
}
}
@@ -166,7 +167,7 @@ object VideoThumbnailCache {
val hash = sha1Hex(url)
val cached = File(downloadCacheDir, "$hash.mp4")
if (cached.length() > 0L) return Download(cached, persistable = true)
if (cached.exists()) cached.delete()
cached.deleteOrWarn("VideoThumbnailCache", "empty cached chunk")
var wrote = false
var rangeHonored = false
@@ -181,7 +182,7 @@ object VideoThumbnailCache {
}
}
if (!wrote || cached.length() == 0L) {
cached.delete()
cached.deleteOrWarn("VideoThumbnailCache", "empty cached chunk")
return null
}
return Download(cached, persistable = rangeHonored)
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.desktop.tor
import com.vitorpamplona.amethyst.commons.tor.ITorManager
import com.vitorpamplona.amethyst.commons.tor.TorServiceStatus
import com.vitorpamplona.amethyst.commons.tor.TorType
import com.vitorpamplona.amethyst.commons.util.restrictToOwner
import com.vitorpamplona.quartz.utils.Log
import io.matthewnelson.kmp.tor.resource.exec.tor.ResourceLoaderTorExec
import io.matthewnelson.kmp.tor.runtime.Action.Companion.startDaemonAsync
@@ -190,13 +191,8 @@ class DesktopTorManager(
private fun desktopEnvironment(): TorRuntime.Environment {
val appDir = torDataDirectory()
appDir.mkdirs()
// Restrict permissions to owner only (700)
appDir.setReadable(false, false)
appDir.setReadable(true, true)
appDir.setWritable(false, false)
appDir.setWritable(true, true)
appDir.setExecutable(false, false)
appDir.setExecutable(true, true)
// Owner-only (700) — Tor state includes onion keys.
appDir.restrictToOwner("DesktopTorManager")
return TorRuntime.Environment.Builder(
workDirectory = appDir.resolve("work"),
@@ -68,6 +68,7 @@ import com.vitorpamplona.amethyst.commons.service.upload.CompressionQuality
import com.vitorpamplona.amethyst.commons.service.upload.UploadOrchestrator
import com.vitorpamplona.amethyst.commons.service.upload.UploadResult
import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import com.vitorpamplona.amethyst.desktop.DesktopPreferences
import com.vitorpamplona.amethyst.desktop.ImageCompressionStore
import com.vitorpamplona.amethyst.desktop.account.AccountState
@@ -282,7 +283,7 @@ fun ComposeNoteDialog(
// User opted out of the compressed
// version — drop the temp before we
// ship the original.
item.compressedFile.delete()
item.compressedFile.deleteOrWarn("ComposeNoteDialog", "compressed temp")
orchestrator.upload(
file = file,
alt = null,
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.commons.service.upload.ImageFormatSniffer
import com.vitorpamplona.amethyst.commons.service.upload.ImageReencoder
import com.vitorpamplona.amethyst.commons.service.upload.ImageReencoder.PassReason
import com.vitorpamplona.amethyst.commons.service.upload.ImageReencoder.ReencodeResult
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import java.io.File
import javax.imageio.ImageIO
@@ -163,7 +164,7 @@ suspend fun buildPreview(
fun cleanupPreviewTemps(items: List<PreviewItem>) {
items.forEach { item ->
if (item is PreviewItem.Reencoded) {
item.compressedFile.delete()
item.compressedFile.deleteOrWarn("CompressionPreview", "compressed temp")
}
}
}
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.napplethost
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.sha256.sha256
import java.io.File
@@ -55,8 +55,8 @@ class NappletBlobCache(
dir.mkdirs()
val tmp = File(dir, "$sha256.tmp.${System.nanoTime()}")
tmp.writeBytes(bytes)
if (!tmp.renameTo(target) && !tmp.delete()) {
Log.w("NappletBlobCache") { "Failed to delete leftover temp file ${tmp.absolutePath} after a failed rename" }
if (!tmp.renameTo(target)) {
tmp.deleteOrWarn("NappletBlobCache", "leftover temp file")
}
}
}
@@ -64,13 +64,18 @@ class NappletBlobCache(
/** Best-effort eviction: if the store exceeds [maxBytes], delete oldest blobs until under it. */
fun trimToSize(maxBytes: Long) {
runCatching {
val files = dir.listFiles()?.filter { it.isFile && !it.name.contains(".tmp.") } ?: return
var total = files.sumOf { it.length() }
val files =
dir
.listFiles()
?.filter { it.isFile && !it.name.contains(".tmp.") }
?.map { Triple(it, it.length(), it.lastModified()) } ?: return
var total = files.sumOf { it.second }
if (total <= maxBytes) return
files.sortedBy { it.lastModified() }.forEach { f ->
files.sortedBy { it.third }.forEach { (f, length, _) ->
if (total <= maxBytes) return
total -= f.length()
f.delete()
if (f.deleteOrWarn("NappletBlobCache", "blob")) {
total -= length
}
}
}
}