Skip to content

Commit cfcf942

Browse files
committed
Rename support
1 parent d6903fd commit cfcf942

7 files changed

Lines changed: 762 additions & 52 deletions

File tree

src/com/potomushto/statik/cms/CmsModels.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ data class CmsMediaListResponse(
105105
data class CmsSaveRequest(
106106
val type: CmsContentType,
107107
val sourcePath: String,
108+
val previousSourcePath: String? = null,
108109
val frontmatter: String = "",
109110
val body: String = "",
110111
val sync: Boolean = false,

src/com/potomushto/statik/cms/CmsRepository.kt

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ class CmsRepository(
4949
)
5050
""".trimIndent()
5151
)
52+
statement.execute(
53+
"""
54+
create table if not exists cms_content_deleted (
55+
source_path text primary key
56+
)
57+
""".trimIndent()
58+
)
5259
}
5360
}
5461
}
@@ -116,6 +123,30 @@ class CmsRepository(
116123
}
117124
}
118125

126+
fun deleteContent(sourcePath: String) {
127+
withConnection { connection ->
128+
connection.prepareStatement("delete from cms_content where source_path = ?").use { statement ->
129+
statement.setString(1, sourcePath)
130+
statement.executeUpdate()
131+
}
132+
}
133+
}
134+
135+
fun markContentDeleted(sourcePath: String) {
136+
withConnection { connection ->
137+
connection.prepareStatement(
138+
"""
139+
insert into cms_content_deleted (source_path)
140+
values (?)
141+
on conflict(source_path) do nothing
142+
""".trimIndent()
143+
).use { statement ->
144+
statement.setString(1, sourcePath)
145+
statement.executeUpdate()
146+
}
147+
}
148+
}
149+
119150
fun upsertMedia(entry: CmsMediaEntry) {
120151
withConnection { connection ->
121152
upsertMedia(connection, entry)
@@ -227,7 +258,7 @@ class CmsRepository(
227258

228259
fun dirtySourcePaths(): List<String> {
229260
return withConnection { connection ->
230-
connection.prepareStatement(
261+
val dirtyContent = connection.prepareStatement(
231262
"""
232263
select source_path
233264
from cms_content
@@ -243,6 +274,24 @@ class CmsRepository(
243274
}
244275
}
245276
}
277+
278+
val deletedContent = connection.prepareStatement(
279+
"""
280+
select source_path
281+
from cms_content_deleted
282+
order by source_path asc
283+
""".trimIndent()
284+
).use { statement ->
285+
statement.executeQuery().use { resultSet ->
286+
buildList {
287+
while (resultSet.next()) {
288+
add(resultSet.getString("source_path"))
289+
}
290+
}
291+
}
292+
}
293+
294+
(dirtyContent + deletedContent).distinct().sorted()
246295
}
247296
}
248297

@@ -287,6 +336,18 @@ class CmsRepository(
287336
}
288337
statement.executeBatch()
289338
}
339+
connection.prepareStatement(
340+
"""
341+
delete from cms_content_deleted
342+
where source_path = ?
343+
""".trimIndent()
344+
).use { statement ->
345+
sourcePaths.forEach { sourcePath ->
346+
statement.setString(1, sourcePath)
347+
statement.addBatch()
348+
}
349+
statement.executeBatch()
350+
}
290351
connection.commit()
291352
} catch (error: Exception) {
292353
connection.rollback()
@@ -360,12 +421,19 @@ class CmsRepository(
360421
}
361422

362423
fun dirtyCount(): Int = withConnection { connection ->
363-
connection.prepareStatement("select count(*) as total from cms_content where dirty = 1").use { statement ->
424+
val dirtyContent = connection.prepareStatement("select count(*) as total from cms_content where dirty = 1").use { statement ->
425+
statement.executeQuery().use { resultSet ->
426+
resultSet.next()
427+
resultSet.getInt("total")
428+
}
429+
}
430+
val deletedContent = connection.prepareStatement("select count(*) as total from cms_content_deleted").use { statement ->
364431
statement.executeQuery().use { resultSet ->
365432
resultSet.next()
366433
resultSet.getInt("total")
367434
}
368435
}
436+
dirtyContent + deletedContent
369437
}
370438

371439
fun mediaDirtyCount(): Int = withConnection { connection ->
@@ -434,6 +502,10 @@ class CmsRepository(
434502
}
435503
statement.executeUpdate()
436504
}
505+
connection.prepareStatement("delete from cms_content_deleted where source_path = ?").use { statement ->
506+
statement.setString(1, entry.sourcePath)
507+
statement.executeUpdate()
508+
}
437509
}
438510

439511
private fun upsertMedia(connection: Connection, entry: CmsMediaEntry) {

src/com/potomushto/statik/cms/CmsService.kt

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import com.potomushto.statik.config.BlogConfig
44
import com.potomushto.statik.generators.AssetManager
55
import com.potomushto.statik.generators.FileWalker
66
import com.potomushto.statik.generators.SiteGenerator
7+
import java.nio.file.Files
78
import java.nio.file.Path
89
import java.nio.file.Paths
910
import java.time.LocalDateTime
11+
import java.util.Comparator
1012

1113
class 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

Comments
 (0)