Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@
## 2025-01-24 - 단일 readAttributes 호출로 파일 속성 조회 최적화
**학습:** `isDirectory`, `!it.isDirectory()`, `isSymbolicLink` 3개의 개별적인 파일 시스템 I/O 호출을 수행하면 성능 저하가 큽니다. 이를 단일 `Files.readAttributes` 호출로 변경하여 메타데이터를 한 번에 조회함으로써 I/O 오버헤드를 대폭 줄일 수 있음을 확인했습니다.
**조치:** 디렉토리 순회 시 파일의 여러 속성을 확인할 때는 개별적인 stat 호출보다 `Files.readAttributes`를 사용하여 필요한 모든 속성을 한 번에 가져오는 방식을 우선적으로 고려해야 합니다.
## 2026-07-27 - 정적 변수 호이스팅을 통한 불필요한 연산 제거
**학습:** `process_dir` 함수 내부에서 매번 생성되고 해시가 계산되는 CSS 콘텐츠 문자열은 성능을 저하시키는 불필요한 할당과 연산을 유발합니다.
**조치:** 불변의 정적 문자열(CSS 콘텐츠)과 비싼 연산(SHA-256 해싱)을 루프나 반복 호출되는 함수 외부의 파일 레벨 `private val`로 호이스팅하여 중복 할당과 연산 오버헤드를 방지합니다.
152 changes: 77 additions & 75 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,81 @@ import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.types.int

// ⚡ Bolt Performance Optimization: Hoist invariant static strings and expensive SHA-256 computations
// out of process_dir to prevent redundant allocations and processing overhead per directory.
private val CSS_CONTENT = """
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.5;
padding: 1rem;
color: #1f2328;
}
main {
max-width: 800px;
margin: 0 auto;
}
ul {
list-style-type: none;
padding-left: 0;
}
a.dir-link {
display: flex;
align-items: flex-start;
gap: 0.5rem;
width: 100%;
overflow-wrap: anywhere;
box-sizing: border-box;
}
.icon {
flex-shrink: 0;
width: 1.25rem;
text-align: center;
}
a {
padding: 0.5rem;
text-decoration: none;
color: #0969da;
border-radius: 4px;
transition: background-color 0.2s ease, outline-color 0.2s ease;
}
a:hover, a:focus-visible {
background-color: #f6f8fa;
text-decoration: underline;
outline: 2px solid #0969da;
outline-offset: -2px;
}
@media (prefers-reduced-motion: reduce) {
a {
transition: none;
}
}
@media (prefers-color-scheme: dark) {
body {
background-color: #0d1117;
color: #c9d1d9;
}
a {
color: #58a6ff;
}
a:hover, a:focus-visible {
background-color: #161b22;
outline-color: #58a6ff;
}
}
.empty-dir {
padding: 0.5rem;
opacity: 0.7;
font-style: italic;
}
"""

private val STYLE_HASH = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(CSS_CONTENT.toByteArray(Charsets.UTF_8)))

private val CSS_TAG = """
<style>
${CSS_CONTENT} </style>
"""
Comment on lines +86 to +89

class Html4tree : CliktCommand() {
val maxLevel:Int by option(help="Number of levels deep for which to generate an index.html file", hidden = false).int().default(-1)
val topDir: String by argument(help="Top directory to crawl")
Expand Down Expand Up @@ -244,91 +319,18 @@ fun process_dir(curr_dir: File, excludeSet: Set<String>? = null, dirFiles: Array

val exclude: Set<String> = excludeSet ?: process_ignore_file(curr_dir)

val cssContent = """
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.5;
padding: 1rem;
color: #1f2328;
}
main {
max-width: 800px;
margin: 0 auto;
}
ul {
list-style-type: none;
padding-left: 0;
}
a.dir-link {
display: flex;
align-items: flex-start;
gap: 0.5rem;
width: 100%;
overflow-wrap: anywhere;
box-sizing: border-box;
}
.icon {
flex-shrink: 0;
width: 1.25rem;
text-align: center;
}
a {
padding: 0.5rem;
text-decoration: none;
color: #0969da;
border-radius: 4px;
transition: background-color 0.2s ease, outline-color 0.2s ease;
}
a:hover, a:focus-visible {
background-color: #f6f8fa;
text-decoration: underline;
outline: 2px solid #0969da;
outline-offset: -2px;
}
@media (prefers-reduced-motion: reduce) {
a {
transition: none;
}
}
@media (prefers-color-scheme: dark) {
body {
background-color: #0d1117;
color: #c9d1d9;
}
a {
color: #58a6ff;
}
a:hover, a:focus-visible {
background-color: #161b22;
outline-color: #58a6ff;
}
}
.empty-dir {
padding: 0.5rem;
opacity: 0.7;
font-style: italic;
}
"""

val styleHash = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(cssContent.toByteArray(Charsets.UTF_8)))

val css = """
<style>
${cssContent} </style>
"""

val index_top = """<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<!-- 보안 향상: 인라인 스크립트 실행 방지 -->
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src '${styleHash}'; base-uri 'none'; form-action 'none';">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src '${STYLE_HASH}'; base-uri 'none'; form-action 'none';">
<!-- 보안 향상: 리퍼러를 통한 디렉토리 경로 노출 방지 -->
<meta name="referrer" content="no-referrer">
<title>${curr_dir.getName().escapeHtml()}</title>
${css}
${CSS_TAG}
</head>
<body>
<main>
Expand Down
Loading