Stable per-branch report URLs and a reachable live view (v0.9.8)
A report directory holding a single page is now linked and served as a directory, so Gradle's --profile report has a stable permanent URL although its file name carries the build timestamp. The permanent link moves to the build it resolves to — the branch's latest green build — and appears on every build table instead of only the branches view. The Current tab gave way to a link in the artifacts column, shown while a build runs; /current itself stays routable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c1869ea427
commit
962836beaf
@@ -17,6 +17,7 @@ import java.net.URI
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.LinkOption
|
||||
import java.nio.file.Path
|
||||
import kotlin.streams.asSequence
|
||||
|
||||
/**
|
||||
* Streams stored build artifacts. Status pages, JSON, and logs are served with
|
||||
@@ -31,12 +32,15 @@ class ArtifactFileController(
|
||||
fun serve(
|
||||
@PathVariable artifactKey: String,
|
||||
@PathVariable path: String,
|
||||
request: HttpServletRequest,
|
||||
): ResponseEntity<Resource> {
|
||||
val artifactDir =
|
||||
artifactStore.artifactDir(artifactKey)
|
||||
?: return ResponseEntity.notFound().build()
|
||||
val relativePath = path.removePrefix("/").removeSuffix("/")
|
||||
directoryResponse(artifactDir, relativePath, request, noStore = true)?.let { return it }
|
||||
val file =
|
||||
resolveFile(artifactDir, path.removePrefix("/"))
|
||||
resolveFile(artifactDir, relativePath)
|
||||
?: return ResponseEntity.notFound().build()
|
||||
return respond(file, noStore = file.extension() in NO_CACHE_EXTENSIONS)
|
||||
}
|
||||
@@ -66,24 +70,53 @@ class ArtifactFileController(
|
||||
// the bare permanent URL is the artifact-index page rendered by the UI controller
|
||||
return redirect(request.requestURI.trimEnd('/'))
|
||||
}
|
||||
val target = artifactDir.resolve(relativePath).normalize()
|
||||
if (target.startsWith(artifactDir) &&
|
||||
Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS) &&
|
||||
Files.isRegularFile(target.resolve(INDEX_FILE), LinkOption.NOFOLLOW_LINKS)
|
||||
) {
|
||||
// relative links inside a report only resolve correctly under a trailing-slash URL
|
||||
return if (request.requestURI.endsWith("/")) {
|
||||
respond(target.resolve(INDEX_FILE), noStore = true)
|
||||
} else {
|
||||
redirect(request.requestURI + "/")
|
||||
}
|
||||
}
|
||||
directoryResponse(artifactDir, relativePath, request, noStore = true)?.let { return it }
|
||||
val file =
|
||||
resolveFile(artifactDir, relativePath)
|
||||
?: return ResponseEntity.notFound().build()
|
||||
return respond(file, noStore = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* The response for a directory URL, or null when [relativePath] is no servable directory.
|
||||
* A directory serves its `index.html`, or the single HTML page of a report directory without
|
||||
* one — that keeps Gradle's `--profile` report, whose file name carries the build timestamp,
|
||||
* reachable under a stable URL.
|
||||
*/
|
||||
private fun directoryResponse(
|
||||
artifactDir: Path,
|
||||
relativePath: String,
|
||||
request: HttpServletRequest,
|
||||
noStore: Boolean,
|
||||
): ResponseEntity<Resource>? {
|
||||
val target = artifactDir.resolve(relativePath).normalize()
|
||||
if (relativePath.isBlank() || !target.startsWith(artifactDir) || !Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS)) {
|
||||
return null
|
||||
}
|
||||
val page = directoryPage(target) ?: return null
|
||||
// relative links inside a report only resolve correctly under a trailing-slash URL
|
||||
return if (request.requestURI.endsWith("/")) {
|
||||
respond(page, noStore = noStore)
|
||||
} else {
|
||||
redirect(request.requestURI + "/")
|
||||
}
|
||||
}
|
||||
|
||||
private fun directoryPage(dir: Path): Path? {
|
||||
val index = dir.resolve(INDEX_FILE)
|
||||
if (Files.isRegularFile(index, LinkOption.NOFOLLOW_LINKS)) {
|
||||
return index
|
||||
}
|
||||
return Files
|
||||
.list(dir)
|
||||
.use { entries ->
|
||||
entries
|
||||
.asSequence()
|
||||
.filter { Files.isRegularFile(it, LinkOption.NOFOLLOW_LINKS) && it.extension() == "html" }
|
||||
.toList()
|
||||
}.singleOrNull()
|
||||
}
|
||||
|
||||
/** Resolves [relativePath] inside [artifactDir]; null when it escapes the directory or is no regular file. */
|
||||
private fun resolveFile(
|
||||
artifactDir: Path,
|
||||
|
||||
Reference in New Issue
Block a user