@@ -4,9 +4,11 @@ import com.potomushto.statik.config.BlogConfig
44import com.potomushto.statik.generators.AssetManager
55import com.potomushto.statik.generators.FileWalker
66import com.potomushto.statik.generators.SiteGenerator
7+ import java.nio.file.Files
78import java.nio.file.Path
89import java.nio.file.Paths
910import java.time.LocalDateTime
11+ import java.util.Comparator
1012
1113class CmsService (
1214 private val rootPath : Path ,
@@ -16,7 +18,7 @@ class CmsService(
1618 private val mediaFileService : MediaFileService = MediaFileService (rootPath, config),
1719 databasePath : Path ? = null ,
1820 private val repository : CmsRepository = CmsRepository (databasePath ? : resolveDatabasePath(rootPath, config.cms.databasePath)),
19- private val gitSyncService : GitSyncService = GitSyncService (rootPath, config.cms.git),
21+ private val gitSyncService : GitSyncService = GitSyncService (rootPath, config.cms.git, config.theme.output ),
2022 private val assetManager : AssetManager = AssetManager (rootPath.toString(), config, FileWalker (rootPath.toString()))
2123) {
2224 private val lock = Any ()
@@ -66,14 +68,35 @@ class CmsService(
6668 fun save (request : CmsSaveRequest , accessToken : String? = null): CmsSaveResponse {
6769 synchronized(lock) {
6870 val normalizedSourcePath = normalizeSourcePath(request.sourcePath)
69- val existing = repository.find(normalizedSourcePath)
70- val saved = contentFileService.save(request.copy(sourcePath = normalizedSourcePath))
71+ val normalizedPreviousSourcePath = request.previousSourcePath
72+ ?.takeIf { it.isNotBlank() }
73+ ?.let (::normalizeSourcePath)
74+
75+ val previousEntry = normalizedPreviousSourcePath?.let (repository::find)
76+ if (previousEntry != null && previousEntry.sourcePath != normalizedSourcePath) {
77+ contentFileService.rename(previousEntry.sourcePath, normalizedSourcePath, previousEntry.type)
78+ }
79+
80+ val existing = repository.find(normalizedSourcePath) ? : previousEntry
81+ val saved = contentFileService.save(
82+ request.copy(
83+ sourcePath = normalizedSourcePath,
84+ previousSourcePath = normalizedPreviousSourcePath
85+ )
86+ )
7187 val dirtyEntry = saved.copy(
7288 dirty = true ,
7389 updatedAt = System .currentTimeMillis(),
7490 lastSyncedAt = existing?.lastSyncedAt
7591 )
7692
93+ if (previousEntry != null && previousEntry.sourcePath != normalizedSourcePath) {
94+ markContentDeleted(previousEntry.sourcePath, previousEntry.lastSyncedAt, previousEntry.dirty)
95+ if (previousEntry.outputPath != saved.outputPath) {
96+ deleteGeneratedContent(previousEntry.outputPath)
97+ }
98+ }
99+
77100 repository.upsert(dirtyEntry)
78101 generator.regenerate(listOf (rootPath.resolve(saved.sourcePath)))
79102
@@ -252,6 +275,53 @@ class CmsService(
252275 )
253276 }
254277
278+ private fun markContentDeleted (sourcePath : String , lastSyncedAt : Long? , dirty : Boolean ) {
279+ if (dirty && lastSyncedAt == null ) {
280+ repository.deleteContent(sourcePath)
281+ return
282+ }
283+
284+ repository.deleteContent(sourcePath)
285+ repository.markContentDeleted(sourcePath)
286+ }
287+
288+ private fun deleteGeneratedContent (outputPath : String ) {
289+ val outputRoot = rootPath.resolve(config.theme.output).normalize()
290+ val target = if (outputPath.isBlank()) {
291+ outputRoot.resolve(" index.html" )
292+ } else {
293+ outputRoot.resolve(outputPath)
294+ }.normalize()
295+
296+ if (! target.startsWith(outputRoot) || ! Files .exists(target)) {
297+ return
298+ }
299+
300+ if (Files .isDirectory(target)) {
301+ Files .walk(target).use { stream ->
302+ stream.sorted(Comparator .reverseOrder()).forEach { candidate ->
303+ Files .deleteIfExists(candidate)
304+ }
305+ }
306+ pruneEmptyGeneratedDirectories(target.parent, outputRoot)
307+ } else {
308+ Files .deleteIfExists(target)
309+ pruneEmptyGeneratedDirectories(target.parent, outputRoot)
310+ }
311+ }
312+
313+ private fun pruneEmptyGeneratedDirectories (start : Path ? , stopAt : Path ) {
314+ var current = start
315+ while (current != null && current.startsWith(stopAt) && current != stopAt) {
316+ val isEmpty = Files .list(current).use { stream -> ! stream.findFirst().isPresent }
317+ if (! isEmpty) {
318+ return
319+ }
320+ Files .deleteIfExists(current)
321+ current = current.parent
322+ }
323+ }
324+
255325 private fun composeMediaPath (targetDirectory : String , fileName : String ): String {
256326 val directory = targetDirectory.trim().trimEnd(' /' ).removePrefix(" /" )
257327 return listOf (directory, fileName.trim().removePrefix(" /" ))
0 commit comments