Skip to content

Commit

Permalink
feat(api): configure scan directory exclusions at library level
Browse files Browse the repository at this point in the history
note that the existing values from configuration will not be migrated
  • Loading branch information
gotson committed Sep 22, 2023
1 parent b48c113 commit b518473
Show file tree
Hide file tree
Showing 15 changed files with 147 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
CREATE TABLE LIBRARY_EXCLUSIONS
(
LIBRARY_ID varchar NOT NULL,
EXCLUSION varchar NOT NULL,
PRIMARY KEY (LIBRARY_ID, EXCLUSION),
FOREIGN KEY (LIBRARY_ID) REFERENCES LIBRARY (ID)
);

CREATE INDEX idx__library_exclusions__library_id on LIBRARY_EXCLUSIONS (LIBRARY_ID);

INSERT INTO LIBRARY_EXCLUSIONS
WITH cte_exclusions(exclude) AS (VALUES ('#recycle'),
('@eaDir'),
('@Recycle'))
SELECT LIBRARY.ID, cte_exclusions.exclude
FROM LIBRARY
cross join cte_exclusions;
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ data class Library(
val scanCbx: Boolean = true,
val scanPdf: Boolean = true,
val scanEpub: Boolean = true,
val scanDirectoryExclusions: Set<String> = emptySet(),
val repairExtensions: Boolean = false,
val convertToCbz: Boolean = false,
val emptyTrashAfterScan: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import org.gotson.komga.domain.model.DirectoryNotFoundException
import org.gotson.komga.domain.model.ScanResult
import org.gotson.komga.domain.model.Series
import org.gotson.komga.domain.model.Sidecar
import org.gotson.komga.infrastructure.configuration.KomgaProperties
import org.gotson.komga.infrastructure.sidecar.SidecarBookConsumer
import org.gotson.komga.infrastructure.sidecar.SidecarSeriesConsumer
import org.springframework.stereotype.Service
Expand Down Expand Up @@ -34,7 +33,6 @@ private val logger = KotlinLogging.logger {}

@Service
class FileSystemScanner(
private val komgaProperties: KomgaProperties,
private val sidecarBookConsumers: List<SidecarBookConsumer>,
private val sidecarSeriesConsumers: List<SidecarSeriesConsumer>,
) {
Expand All @@ -55,6 +53,7 @@ class FileSystemScanner(
scanCbx: Boolean = true,
scanPdf: Boolean = true,
scanEpub: Boolean = true,
directoryExclusions: Set<String> = emptySet(),
): ScanResult {
val scanForExtensions = buildList {
if (scanCbx) addAll(listOf("cbz", "zip", "cbr", "rar"))
Expand All @@ -63,7 +62,7 @@ class FileSystemScanner(
}
logger.info { "Scanning folder: $root" }
logger.info { "Scan for extensions: $scanForExtensions" }
logger.info { "Excluded patterns: ${komgaProperties.librariesScanDirectoryExclusions}" }
logger.info { "Excluded directory patterns: $directoryExclusions" }
logger.info { "Force directory modified time: $forceDirectoryModifiedTime" }

if (!(Files.isDirectory(root) && Files.isReadable(root)))
Expand All @@ -88,7 +87,7 @@ class FileSystemScanner(
override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
logger.trace { "preVisit: $dir (regularFile:${attrs.isRegularFile}, directory:${attrs.isDirectory}, symbolicLink:${attrs.isSymbolicLink}, other:${attrs.isOther})" }
if (dir.name.startsWith(".") ||
komgaProperties.librariesScanDirectoryExclusions.any { exclude ->
directoryExclusions.any { exclude ->
dir.pathString.contains(exclude, true)
}
) return FileVisitResult.SKIP_SUBTREE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class LibraryContentLifecycle(
library.scanCbx,
library.scanPdf,
library.scanEpub,
library.scanDirectoryExclusions,
)
} catch (e: DirectoryNotFoundException) {
library.copy(unavailableDate = LocalDateTime.now()).let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class LibraryLifecycle(
if (existing.scanPdf != updated.scanPdf) return true
if (existing.scanEpub != updated.scanEpub) return true
if (existing.scanForceModifiedTime != updated.scanForceModifiedTime) return true
if (existing.scanDirectoryExclusions != updated.scanDirectoryExclusions) return true
return false
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class KomgaProperties {
@Deprecated("Moved to library options since 1.5.0")
var librariesScanStartup: Boolean = false

@Deprecated("Moved to library options since 1.5.0")
var librariesScanDirectoryExclusions: List<String> = emptyList()

var deleteEmptyReadLists: Boolean = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import org.gotson.komga.domain.persistence.LibraryRepository
import org.gotson.komga.jooq.Tables
import org.gotson.komga.jooq.tables.records.LibraryRecord
import org.jooq.DSLContext
import org.jooq.Record
import org.jooq.ResultQuery
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional
import java.net.URL
Expand All @@ -18,39 +20,51 @@ class LibraryDao(

private val l = Tables.LIBRARY
private val ul = Tables.USER_LIBRARY_SHARING
private val le = Tables.LIBRARY_EXCLUSIONS

override fun findByIdOrNull(libraryId: String): Library? =
findOne(libraryId)
?.toDomain()
.fetchAndMap()
.firstOrNull()

override fun findById(libraryId: String): Library =
findOne(libraryId)!!
.toDomain()
findOne(libraryId)
.fetchAndMap()
.first()

private fun findOne(libraryId: String) =
dsl.selectFrom(l)
selectBase()
.where(l.ID.eq(libraryId))
.fetchOneInto(l)

override fun findAll(): Collection<Library> =
dsl.selectFrom(l)
.fetchInto(l)
.map { it.toDomain() }
selectBase()
.fetchAndMap()

override fun findAllByIds(libraryIds: Collection<String>): Collection<Library> =
dsl.selectFrom(l)
selectBase()
.where(l.ID.`in`(libraryIds))
.fetchInto(l)
.map { it.toDomain() }
.fetchAndMap()

private fun selectBase() =
dsl.select()
.from(l)
.leftJoin(le).onKey()

private fun ResultQuery<Record>.fetchAndMap(): Collection<Library> =
this.fetchGroups({ it.into(l) }, { it.into(le) })
.map { (lr, ler) ->
lr.toDomain(ler.mapNotNull { it.exclusion }.toSet())
}

@Transactional
override fun delete(libraryId: String) {
dsl.deleteFrom(le).where(le.LIBRARY_ID.eq(libraryId)).execute()
dsl.deleteFrom(ul).where(ul.LIBRARY_ID.eq(libraryId)).execute()
dsl.deleteFrom(l).where(l.ID.eq(libraryId)).execute()
}

@Transactional
override fun deleteAll() {
dsl.deleteFrom(le).execute()
dsl.deleteFrom(ul).execute()
dsl.deleteFrom(l).execute()
}
Expand Down Expand Up @@ -87,6 +101,8 @@ class LibraryDao(
.set(l.ONESHOTS_DIRECTORY, library.oneshotsDirectory)
.set(l.UNAVAILABLE_DATE, library.unavailableDate)
.execute()

insertDirectoryExclusions(library)
}

@Transactional
Expand Down Expand Up @@ -122,11 +138,32 @@ class LibraryDao(
.set(l.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z")))
.where(l.ID.eq(library.id))
.execute()

dsl.deleteFrom(le).where(le.LIBRARY_ID.eq(library.id)).execute()
insertDirectoryExclusions(library)
}

override fun count(): Long = dsl.fetchCount(l).toLong()
fun findDirectoryExclusions(libraryId: String): Set<String> =
dsl.select(le.EXCLUSION)
.from(le)
.where(le.LIBRARY_ID.eq(libraryId))
.fetchSet(le.EXCLUSION)

private fun insertDirectoryExclusions(library: Library) {
if (library.scanDirectoryExclusions.isNotEmpty()) {
dsl.batch(
dsl.insertInto(le, le.LIBRARY_ID, le.EXCLUSION)
.values(null as String?, null),
).also { step ->
library.scanDirectoryExclusions.forEach {
step.bind(library.id, it)
}
}.execute()
}
}

private fun LibraryRecord.toDomain() =
private fun LibraryRecord.toDomain(directoryExclusions: Set<String>) =
Library(
name = name,
root = URL(root),
Expand All @@ -146,6 +183,7 @@ class LibraryDao(
scanEpub = scanEpub,
scanOnStartup = scanStartup,
scanInterval = Library.ScanInterval.valueOf(scanInterval),
scanDirectoryExclusions = directoryExclusions,
repairExtensions = repairExtensions,
convertToCbz = convertToCbz,
emptyTrashAfterScan = emptyTrashAfterScan,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class LibraryController(
scanCbx = library.scanCbx,
scanPdf = library.scanPdf,
scanEpub = library.scanEpub,
scanDirectoryExclusions = library.scanDirectoryExclusions,
repairExtensions = library.repairExtensions,
convertToCbz = library.convertToCbz,
emptyTrashAfterScan = library.emptyTrashAfterScan,
Expand Down Expand Up @@ -165,6 +166,7 @@ class LibraryController(
scanCbx = scanCbx ?: existing.scanCbx,
scanPdf = scanPdf ?: existing.scanPdf,
scanEpub = scanEpub ?: existing.scanEpub,
scanDirectoryExclusions = if (isSet("scanDirectoryExclusions")) scanDirectoryExclusions ?: emptySet() else existing.scanDirectoryExclusions,
repairExtensions = repairExtensions ?: existing.repairExtensions,
convertToCbz = convertToCbz ?: existing.convertToCbz,
emptyTrashAfterScan = emptyTrashAfterScan ?: existing.emptyTrashAfterScan,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ data class LibraryCreationDto(
val scanCbx: Boolean = true,
val scanPdf: Boolean = true,
val scanEpub: Boolean = true,
val scanDirectoryExclusions: Set<String> = emptySet(),
val repairExtensions: Boolean = false,
val convertToCbz: Boolean = false,
val emptyTrashAfterScan: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ data class LibraryDto(
val scanCbx: Boolean,
val scanPdf: Boolean,
val scanEpub: Boolean,
val scanDirectoryExclusions: Set<String>,
val repairExtensions: Boolean,
val convertToCbz: Boolean,
val emptyTrashAfterScan: Boolean,
Expand Down Expand Up @@ -54,6 +55,7 @@ fun Library.toDto(includeRoot: Boolean) = LibraryDto(
scanCbx = scanCbx,
scanPdf = scanPdf,
scanEpub = scanEpub,
scanDirectoryExclusions = scanDirectoryExclusions,
repairExtensions = repairExtensions,
convertToCbz = convertToCbz,
emptyTrashAfterScan = emptyTrashAfterScan,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ class LibraryUpdateDto {
val scanCbx: Boolean? = null
val scanPdf: Boolean? = null
val scanEpub: Boolean? = null
var scanDirectoryExclusions: Set<String>?
by Delegates.observable(null) { prop, _, _ ->
isSet[prop.name] = true
}

val repairExtensions: Boolean? = null
val convertToCbz: Boolean? = null
Expand Down
4 changes: 0 additions & 4 deletions komga/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ logging:

komga:
libraries-scan-cron: "0 0 */8 * * ?"
libraries-scan-directory-exclusions:
- "#recycle" # Synology
- "@eaDir" # Synology
- "@Recycle" # Qnap
database:
file: \${komga.config-dir}/database.sqlite
lucene:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import org.apache.commons.io.FilenameUtils
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.catchThrowable
import org.gotson.komga.domain.model.DirectoryNotFoundException
import org.gotson.komga.infrastructure.configuration.KomgaProperties
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
Expand All @@ -16,12 +15,7 @@ import java.nio.file.Path
import java.util.stream.Stream

class FileSystemScannerTest {

private val komgaProperties = KomgaProperties().apply {
librariesScanDirectoryExclusions = listOf("#recycle")
}

private val scanner = FileSystemScanner(komgaProperties, emptyList(), emptyList())
private val scanner = FileSystemScanner(emptyList(), emptyList())

@Test
fun `given unavailable root directory when scanning then throw exception`() {
Expand Down Expand Up @@ -294,7 +288,7 @@ class FileSystemScannerTest {
makeSubDir(recycle, "subtrash", listOf("trash2.cbz"))

// when
val scan = scanner.scanRootFolder(root).series
val scan = scanner.scanRootFolder(root, directoryExclusions = setOf("#recycle")).series

// then
assertThat(scan).hasSize(2)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class LibraryDaoTest(
scanPdf = false,
scanInterval = Library.ScanInterval.DAILY,
scanOnStartup = true,
scanDirectoryExclusions = setOf("a", "b"),
)
}

Expand Down Expand Up @@ -111,6 +112,7 @@ class LibraryDaoTest(
assertThat(modified.scanPdf).isEqualTo(updated.scanPdf)
assertThat(modified.scanInterval).isEqualTo(updated.scanInterval)
assertThat(modified.scanOnStartup).isEqualTo(updated.scanOnStartup)
assertThat(modified.scanDirectoryExclusions).containsExactlyInAnyOrderElementsOf(updated.scanDirectoryExclusions)
}

@Test
Expand Down
Loading

0 comments on commit b518473

Please sign in to comment.