working version with new Kotlin frontend!!!
This commit is contained in:
29
build.gradle
29
build.gradle
@@ -60,6 +60,13 @@ configurations {
|
|||||||
visible = false
|
visible = false
|
||||||
canBeResolved = true
|
canBeResolved = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frontend {
|
||||||
|
transitive = false
|
||||||
|
canBeConsumed = false
|
||||||
|
visible = false
|
||||||
|
canBeResolved = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
@@ -106,23 +113,12 @@ dependencies {
|
|||||||
deployableArchives project
|
deployableArchives project
|
||||||
deployableArchives project(':jpacrepo-impl')
|
deployableArchives project(':jpacrepo-impl')
|
||||||
deployableArchives project(':jpacrepo-api')
|
deployableArchives project(':jpacrepo-api')
|
||||||
}
|
|
||||||
|
|
||||||
File nimDir = project.file('nim')
|
frontend project(path: ':jpacrepo-frontend', configuration: 'tar')
|
||||||
File srcDir = new File(nimDir, 'src')
|
|
||||||
File staticDir = new File(nimDir, 'static')
|
|
||||||
|
|
||||||
Provider<Exec> nimCompileTaskProvider = tasks.register("compileNim", Exec) {
|
|
||||||
inputs.files(project.fileTree(srcDir), project.fileTree(staticDir))
|
|
||||||
File outputFile = new File(temporaryDir, "jpacrepo.js")
|
|
||||||
outputs.file(outputFile)
|
|
||||||
commandLine 'nim', 'js', '-d:release', "-o:$outputFile", 'src/jpacrepo.nim'
|
|
||||||
workingDir(nimDir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Provider<War> warTaskProvider = tasks.named('war', War) { War it ->
|
Provider<War> warTaskProvider = tasks.named('war', War) { War it ->
|
||||||
from staticDir
|
from(configurations.frontend)
|
||||||
from nimCompileTaskProvider
|
|
||||||
archiveVersion = ''
|
archiveVersion = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,14 +136,7 @@ tasks.named("test", Test) {
|
|||||||
'--add-opens', 'java.naming/javax.naming.spi=ALL-UNNAMED',
|
'--add-opens', 'java.naming/javax.naming.spi=ALL-UNNAMED',
|
||||||
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
|
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
|
||||||
'--add-opens', 'java.base/java.io=ALL-UNNAMED',
|
'--add-opens', 'java.base/java.io=ALL-UNNAMED',
|
||||||
// '--add-opens', 'java.base/java.util=ALL-UNNAMED',
|
|
||||||
// '--add-opens', 'java.management/javax.management.openmbean=ALL-UNNAMED',
|
|
||||||
// '--add-opens', 'java.management/javax.management=ALL-UNNAMED',
|
|
||||||
]
|
]
|
||||||
// classpath = configurations.testRuntimeClasspath + sourceSets.test.output
|
|
||||||
// File warPath = tasks.named("war", War).get().archiveFile.get().getAsFile()
|
|
||||||
// classpath += warPath
|
|
||||||
// classpath += tasks.named("war", War).get().outputs.files
|
|
||||||
inputs.files(configurations.deployableArchives)
|
inputs.files(configurations.deployableArchives)
|
||||||
systemProperty('jpacrepo.jar.path', configurations.deployableArchives.asPath)
|
systemProperty('jpacrepo.jar.path', configurations.deployableArchives.asPath)
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
jpacrepo.version=2024.02.09
|
jpacrepo.version=2024.02.12
|
||||||
|
|
||||||
lys.version=2024.02.09
|
lys.version=2024.02.12
|
||||||
|
@@ -6,9 +6,12 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public enum CompressionFormat {
|
public enum CompressionFormat implements Comparable<CompressionFormat> {
|
||||||
XZ("xz"), GZIP("gz"), Z_STANDARD("zst");
|
XZ("xz"), GZIP("gz"), Z_STANDARD("zst");
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private final String value;
|
private final String value;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -84,6 +84,8 @@ public class PkgData implements Serializable {
|
|||||||
|
|
||||||
private long size;
|
private long size;
|
||||||
|
|
||||||
|
private long installedSize;
|
||||||
|
|
||||||
@Getter(onMethod_ = {
|
@Getter(onMethod_ = {
|
||||||
@ManyToMany,
|
@ManyToMany,
|
||||||
@JoinTable(name = "PkgData_license")
|
@JoinTable(name = "PkgData_license")
|
||||||
|
@@ -1,11 +1,54 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(catalog.plugins.kotlin.multiplatform)
|
alias(catalog.plugins.kotlin.multiplatform)
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization" version "1.9.22"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
tar {
|
||||||
|
canBeConsumed = true
|
||||||
|
canBeResolved = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
jsMain {
|
||||||
|
dependencies {
|
||||||
|
implementation catalog.klevtree
|
||||||
|
implementation group: 'org.jetbrains.kotlinx', name: 'kotlinx-serialization-json', version: '1.6.2'
|
||||||
|
// implementation(npm("bootstrap", "5.3.2"))
|
||||||
|
// implementation(npm("bootstrap-icons", "1.11.3"))
|
||||||
|
// implementation(npm("style-loader", "3.3.4"))
|
||||||
|
// implementation(npm("css-loader", "6.10.0"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
js(IR) {
|
js(IR) {
|
||||||
browser {
|
browser {
|
||||||
|
runTask {
|
||||||
|
sourceMaps = true
|
||||||
|
devServer.port = 8080
|
||||||
|
}
|
||||||
|
webpackTask {
|
||||||
|
sourceMaps = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
binaries.executable()
|
binaries.executable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Provider<Tar> distTarTaskProvider = tasks.register('distTar', Tar) { tar ->
|
||||||
|
from(tasks.named('jsBrowserDistribution'))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def distributionTaskProvider = tasks.named("jsBrowserDistribution")
|
||||||
|
|
||||||
|
artifacts {
|
||||||
|
tar(distributionTaskProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,15 @@
|
|||||||
|
package net.woggioni.jpacrepo.common
|
||||||
|
|
||||||
|
object JpacrepoCommons {
|
||||||
|
private val hexArray = "0123456789ABCDEF".toCharArray()
|
||||||
|
|
||||||
|
fun ByteArray.toHexString(): String {
|
||||||
|
val hexChars = CharArray(this.size * 2)
|
||||||
|
for (j in this.indices) {
|
||||||
|
val v = this[j].toInt() and 0xFF
|
||||||
|
hexChars[j * 2] = hexArray[v ushr 4]
|
||||||
|
hexChars[j * 2 + 1] = hexArray[v and 0x0F]
|
||||||
|
}
|
||||||
|
return hexChars.concatToString()
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,182 @@
|
|||||||
|
package net.woggioni.jpacrepo
|
||||||
|
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.coroutines.startCoroutine
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
import kotlin.js.Promise
|
||||||
|
import org.w3c.dom.Document
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
import org.w3c.dom.events.Event
|
||||||
|
|
||||||
|
object HtmlUtils {
|
||||||
|
fun Element.removeChildren() {
|
||||||
|
while (true) {
|
||||||
|
removeChild(firstChild ?: break)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Element.on(eventName : String, eventListener : (Event) -> Unit) {
|
||||||
|
addEventListener(eventName, eventListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launch(block: suspend () -> Unit) {
|
||||||
|
block.startCoroutine(object : Continuation<Unit> {
|
||||||
|
override val context: CoroutineContext get() = EmptyCoroutineContext
|
||||||
|
override fun resumeWith(result: Result<Unit>) {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> Promise<T>.await(): T = suspendCoroutine { cont ->
|
||||||
|
then({ cont.resume(it) }, { cont.resumeWithException(it) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HtmlBuilder private constructor(private val doc : Document, val el: Element) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <T> of(doc : Document, el: Element, cb : HtmlBuilder.(el : Element) -> T) : T {
|
||||||
|
return HtmlBuilder(doc, el).cb(el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T> dfd(
|
||||||
|
name : String,
|
||||||
|
attrs : Map<String, String>,
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T {
|
||||||
|
val child = doc.createElement(name)
|
||||||
|
for((key, value) in attrs) {
|
||||||
|
child.setAttribute(key, value)
|
||||||
|
}
|
||||||
|
el.appendChild(child)
|
||||||
|
return HtmlBuilder(doc, child).cb(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> html(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("html", attrs, cb)
|
||||||
|
fun <T> head(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("head", attrs, cb)
|
||||||
|
fun <T> body(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("body", attrs, cb)
|
||||||
|
|
||||||
|
fun <T> use(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("use", attrs, cb)
|
||||||
|
fun <T> svg(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("svg", attrs, cb)
|
||||||
|
fun <T> div(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("div", attrs, cb)
|
||||||
|
fun <T> header(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("header", attrs, cb)
|
||||||
|
fun <T> main(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("main", attrs, cb)
|
||||||
|
fun <T> footer(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("footer", attrs, cb)
|
||||||
|
fun <T> a(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("a", attrs, cb)
|
||||||
|
fun <T> meta(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("meta", attrs, cb)
|
||||||
|
fun <T> script(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("script", attrs, cb)
|
||||||
|
fun <T> link(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("link", attrs, cb)
|
||||||
|
fun <T> title(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("title", attrs, cb)
|
||||||
|
fun <T> p(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("p", attrs, cb)
|
||||||
|
fun <T> span(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("span", attrs, cb)
|
||||||
|
fun <T> i(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("i", attrs, cb)
|
||||||
|
fun <T> del(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("del", attrs, cb)
|
||||||
|
fun <T> s(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("s", attrs, cb)
|
||||||
|
fun <T> ins(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("ins", attrs, cb)
|
||||||
|
fun <T> u(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("u", attrs, cb)
|
||||||
|
fun <T> b(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("b", attrs, cb)
|
||||||
|
fun <T> small(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("small", attrs, cb)
|
||||||
|
fun <T> strong(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("strong", attrs, cb)
|
||||||
|
fun <T> em(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("em", attrs, cb)
|
||||||
|
fun <T> mark(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("mark", attrs, cb)
|
||||||
|
fun <T> obj(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("object", attrs, cb)
|
||||||
|
fun <T> h1(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("h1", attrs, cb)
|
||||||
|
fun <T> h2(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("h2", attrs, cb)
|
||||||
|
fun <T> h3(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("h3", attrs, cb)
|
||||||
|
fun <T> h4(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("h4", attrs, cb)
|
||||||
|
fun <T> h5(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("h5", attrs, cb)
|
||||||
|
fun <T> h6(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("h6", attrs, cb)
|
||||||
|
fun <T> table(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("table", attrs, cb)
|
||||||
|
fun <T> thead(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("thead", attrs, cb)
|
||||||
|
fun <T> tbody(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("tbody", attrs, cb)
|
||||||
|
fun <T> tfoot(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("tfoot", attrs, cb)
|
||||||
|
fun <T> tr(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("tr", attrs, cb)
|
||||||
|
fun <T> th(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("th", attrs, cb)
|
||||||
|
fun <T> td(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("td", attrs, cb)
|
||||||
|
fun <T> ol(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("ol", attrs, cb)
|
||||||
|
fun <T> ul(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("ul", attrs, cb)
|
||||||
|
fun <T> li(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("li", attrs, cb)
|
||||||
|
fun <T> img(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("img", attrs, cb)
|
||||||
|
fun <T> form(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("form", attrs, cb)
|
||||||
|
fun <T> label(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("label", attrs, cb)
|
||||||
|
fun <T> button(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("button", attrs, cb)
|
||||||
|
fun <T> input(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("input", attrs, cb)
|
||||||
|
fun <T> select(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("select", attrs, cb)
|
||||||
|
fun <T> option(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("option", attrs, cb)
|
||||||
|
fun <T> meter(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("meter", attrs, cb)
|
||||||
|
fun <T> nav(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("nav", attrs, cb)
|
||||||
|
fun <T> menu(attrs : Map<String, String> = emptyMap(),
|
||||||
|
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("menu", attrs, cb)
|
||||||
|
|
||||||
|
|
||||||
|
fun classes(vararg classes : String) {
|
||||||
|
for(cls in classes) el.classList.add(cls)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun attr(key: String, value : String) {
|
||||||
|
el.setAttribute(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun text(txt : String) {
|
||||||
|
el.appendChild(doc.createTextNode(txt))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun on(eventName : String, cb: (Event) -> Unit) {
|
||||||
|
el.addEventListener(eventName, cb)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,510 @@
|
|||||||
|
package net.woggioni.jpacrepo
|
||||||
|
|
||||||
|
import kotlin.js.Promise
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlinx.browser.document
|
||||||
|
import kotlinx.browser.window
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import net.woggioni.jpacrepo.HtmlUtils.await
|
||||||
|
import net.woggioni.jpacrepo.HtmlUtils.launch
|
||||||
|
import net.woggioni.jpacrepo.HtmlUtils.on
|
||||||
|
import net.woggioni.jpacrepo.HtmlUtils.removeChildren
|
||||||
|
import net.woggioni.jpacrepo.common.JpacrepoCommons.toHexString
|
||||||
|
import net.woggioni.klevtree.LevTrie
|
||||||
|
import org.w3c.dom.CustomEvent
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
import org.w3c.dom.HTMLElement
|
||||||
|
import org.w3c.dom.HTMLFormElement
|
||||||
|
import org.w3c.dom.HTMLInputElement
|
||||||
|
import org.w3c.dom.HTMLSelectElement
|
||||||
|
import org.w3c.dom.events.Event
|
||||||
|
import org.w3c.dom.get
|
||||||
|
|
||||||
|
private val UNITS_OF_MEASURE = arrayOf("B", "KiB", "MiB", "GiB", "TiB", "PiB")
|
||||||
|
fun Long.toHumanReadableByteSize(): String {
|
||||||
|
var i = 0
|
||||||
|
var multiple = 1
|
||||||
|
while (i < UNITS_OF_MEASURE.size) {
|
||||||
|
val label = UNITS_OF_MEASURE[i]
|
||||||
|
val newMultiple = multiple * 1024;
|
||||||
|
if (this > multiple && (i + 1 > UNITS_OF_MEASURE.size || this < newMultiple)) {
|
||||||
|
return (this.toDouble() / multiple).asDynamic().toPrecision(3) + " " + label;
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
multiple = newMultiple;
|
||||||
|
}
|
||||||
|
throw Error("This should never happen")
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias PkgMap = Map<String, Map<String, Map<String, Map<CompressionFormat, PkgTuple>>>>
|
||||||
|
|
||||||
|
private fun createPackageArchive(): Promise<PackageArchive> {
|
||||||
|
return window.fetch(SERVER_PREFIX + "api/pkg/map")
|
||||||
|
.then { it.text() }
|
||||||
|
.then { Json.decodeFromString<PkgMap>(it) }
|
||||||
|
.then(::PackageArchive)
|
||||||
|
}
|
||||||
|
|
||||||
|
class PackageArchive(val pkgMap: PkgMap) {
|
||||||
|
val tries: Map<String, LevTrie>
|
||||||
|
|
||||||
|
init {
|
||||||
|
tries = pkgMap.asSequence().map {
|
||||||
|
it.key to LevTrie().apply {
|
||||||
|
algorithm = LevTrie.Algorithm.DAMERAU_LEVENSHTEIN
|
||||||
|
for (pkgName in it.value.keys) {
|
||||||
|
add(pkgName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun searchByArch(arch: String, needle: String, maxResult: Int): List<String> {
|
||||||
|
return tries[arch]?.let {
|
||||||
|
it.fuzzySearch(needle, maxResult)
|
||||||
|
.asSequence()
|
||||||
|
.sortedBy { it.second }
|
||||||
|
.map { it.first }
|
||||||
|
.toList()
|
||||||
|
} ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private const val REFRESH_TABLE_EVENT_NAME = "refreshTable"
|
||||||
|
private const val REFRESH_TABLE_ROW_EVENT_NAME = "refreshTableRow"
|
||||||
|
private const val REFRESH_SELECTED_PACKAGES_EVENT_NAME = "refreshSelectedPackages"
|
||||||
|
//private const val SERVER_PREFIX = "https://woggioni.net/jpacrepo/"
|
||||||
|
private const val SERVER_PREFIX = ""
|
||||||
|
|
||||||
|
private fun fireTableRefreshEvent(el: Element) {
|
||||||
|
val evt = CustomEvent(
|
||||||
|
REFRESH_TABLE_EVENT_NAME
|
||||||
|
).apply {
|
||||||
|
initCustomEvent(
|
||||||
|
REFRESH_TABLE_EVENT_NAME,
|
||||||
|
bubbles = true,
|
||||||
|
cancelable = true,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
el.dispatchEvent(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fireTableRowRefreshEvent(el: Element) {
|
||||||
|
val evt = CustomEvent(
|
||||||
|
REFRESH_TABLE_ROW_EVENT_NAME
|
||||||
|
).apply {
|
||||||
|
initCustomEvent(
|
||||||
|
REFRESH_TABLE_ROW_EVENT_NAME,
|
||||||
|
bubbles = false,
|
||||||
|
cancelable = true,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
el.dispatchEvent(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fireSelectedPackagesRefreshEvent(el: Element) {
|
||||||
|
val evt = CustomEvent(
|
||||||
|
REFRESH_SELECTED_PACKAGES_EVENT_NAME
|
||||||
|
).apply {
|
||||||
|
initCustomEvent(
|
||||||
|
REFRESH_SELECTED_PACKAGES_EVENT_NAME,
|
||||||
|
bubbles = true,
|
||||||
|
cancelable = true,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
el.dispatchEvent(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pkgArchive: Promise<PackageArchive> = createPackageArchive()
|
||||||
|
var selectedArchitecture: String? = null
|
||||||
|
val selectedPackages: MutableSet<PkgTuple> = mutableSetOf()
|
||||||
|
|
||||||
|
suspend fun buildTable(root: Element, searchTerm: String, commonElement: Element) {
|
||||||
|
selectedArchitecture?.let { arch ->
|
||||||
|
val archive = pkgArchive.await()
|
||||||
|
val results = archive.searchByArch(arch, searchTerm, 10)
|
||||||
|
val children = root.children
|
||||||
|
while (children.length > 0) {
|
||||||
|
children[0]!!.remove()
|
||||||
|
}
|
||||||
|
HtmlBuilder.of(document, root) {
|
||||||
|
table {
|
||||||
|
classes("table", "table-striped", "pkgtable")
|
||||||
|
thead {
|
||||||
|
tr {
|
||||||
|
th {
|
||||||
|
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
text("Name")
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
text("Version")
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
text("Compression format")
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
text("Filename")
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
text("Size")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
for (pkgName in results) {
|
||||||
|
val packages = archive.pkgMap[arch]?.let { it[pkgName] }
|
||||||
|
var selectedPackage: Pair<PkgId, PkgTuple>? = let {
|
||||||
|
packages?.iterator()?.next()?.let {
|
||||||
|
val version = it.key
|
||||||
|
it.value.iterator().next().let {
|
||||||
|
val compressionFormat = it.key
|
||||||
|
PkgId(arch, pkgName, version, compressionFormat) to it.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var selectedVersion = packages?.keys?.first()
|
||||||
|
tr { row ->
|
||||||
|
td {
|
||||||
|
i {
|
||||||
|
classes("bi-plus-circle")
|
||||||
|
attr("style", "font-size: 1.5rem; color: grey;")
|
||||||
|
on("click") {
|
||||||
|
selectedPackage?.let {
|
||||||
|
selectedPackages.add(it.second)
|
||||||
|
}
|
||||||
|
fireSelectedPackagesRefreshEvent(commonElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
text(pkgName)
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
packages?.keys?.let { keys ->
|
||||||
|
select {
|
||||||
|
classes("form-select")
|
||||||
|
for (version in keys) {
|
||||||
|
option {
|
||||||
|
attr("value", version)
|
||||||
|
text(version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
on("input") { evt: Event ->
|
||||||
|
val htmlInputEvent = evt.target as? HTMLSelectElement
|
||||||
|
selectedVersion = htmlInputEvent?.value
|
||||||
|
fireTableRowRefreshEvent(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val getFirstAvailableCompressionFormat = {
|
||||||
|
val availablePackages = packages
|
||||||
|
?.let { it[selectedVersion] }
|
||||||
|
availablePackages?.keys?.first()
|
||||||
|
}
|
||||||
|
var selectedCompressionFormat = getFirstAvailableCompressionFormat()
|
||||||
|
|
||||||
|
fun HtmlBuilder.createCompressionFormatDropdown() {
|
||||||
|
val availablePackages = packages
|
||||||
|
?.let { it[selectedVersion] }
|
||||||
|
availablePackages?.keys?.let { compressionFormats ->
|
||||||
|
select {
|
||||||
|
classes("form-select")
|
||||||
|
for (compressionFormat in compressionFormats) {
|
||||||
|
option {
|
||||||
|
attr("value", compressionFormat.name)
|
||||||
|
text(compressionFormat.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
on("input") { evt: Event ->
|
||||||
|
val htmlInputEvent = evt.target as? HTMLSelectElement
|
||||||
|
selectedCompressionFormat = htmlInputEvent
|
||||||
|
?.value
|
||||||
|
?.let(CompressionFormat::valueOf)
|
||||||
|
fireTableRowRefreshEvent(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val compressionFormatCell = td {
|
||||||
|
createCompressionFormatDropdown()
|
||||||
|
el
|
||||||
|
}
|
||||||
|
val fileNameCell = td {
|
||||||
|
selectedPackage?.let {
|
||||||
|
text(it.second.fileName)
|
||||||
|
}
|
||||||
|
el
|
||||||
|
}
|
||||||
|
val sizeCell = td {
|
||||||
|
selectedPackage?.let {
|
||||||
|
text(it.second.size.toHumanReadableByteSize())
|
||||||
|
}
|
||||||
|
el
|
||||||
|
}
|
||||||
|
on(REFRESH_TABLE_ROW_EVENT_NAME) { evt ->
|
||||||
|
selectedCompressionFormat = getFirstAvailableCompressionFormat()
|
||||||
|
compressionFormatCell.removeChildren()
|
||||||
|
HtmlBuilder.of(document, compressionFormatCell) {
|
||||||
|
createCompressionFormatDropdown()
|
||||||
|
}
|
||||||
|
selectedPackage = archive.pkgMap[arch]?.let {
|
||||||
|
it[pkgName]
|
||||||
|
}?.let {
|
||||||
|
it[selectedVersion]
|
||||||
|
}?.let {
|
||||||
|
it[selectedCompressionFormat]
|
||||||
|
}?.let { pkgTuple ->
|
||||||
|
PkgId(arch, pkgName, selectedVersion!!, selectedCompressionFormat!!) to pkgTuple
|
||||||
|
}
|
||||||
|
selectedPackage?.let { pair ->
|
||||||
|
fileNameCell.textContent = pair.second.fileName
|
||||||
|
sizeCell.textContent = pair.second.size.toHumanReadableByteSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun HtmlBuilder.buildPackageSearchSection(commonElement: Element) {
|
||||||
|
val root = el
|
||||||
|
var selectedPackageName: String? = null
|
||||||
|
div {
|
||||||
|
classes("row", "g-5")
|
||||||
|
div {
|
||||||
|
classes("col-md12")
|
||||||
|
form {
|
||||||
|
classes("needs-validation")
|
||||||
|
div {
|
||||||
|
classes("row", "g-3")
|
||||||
|
label {
|
||||||
|
classes("col-sm-2", "col-form-label")
|
||||||
|
attr("for", "packageName")
|
||||||
|
text("Search package")
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
classes("col-sm-10", "col-form-label")
|
||||||
|
input { inputElement ->
|
||||||
|
attr("id", "packageName")
|
||||||
|
attr("type", "text")
|
||||||
|
attr("placeholder", "")
|
||||||
|
classes("form-control")
|
||||||
|
on("input") {
|
||||||
|
(it.target as? HTMLInputElement)?.value?.let { packageName ->
|
||||||
|
selectedPackageName = packageName
|
||||||
|
fireTableRefreshEvent(
|
||||||
|
inputElement
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
classes("d-flex")
|
||||||
|
text("Architecture:")
|
||||||
|
div {
|
||||||
|
classes("form-check")
|
||||||
|
pkgArchive.then { pkgArchive ->
|
||||||
|
for (arch in pkgArchive.pkgMap.keys) {
|
||||||
|
val id = Random.Default.nextBytes(4).toHexString()
|
||||||
|
div {
|
||||||
|
classes("form-check", "form-check-inline")
|
||||||
|
input { inputElement ->
|
||||||
|
attr("id", id)
|
||||||
|
attr("type", "radio")
|
||||||
|
attr("autocomplete", "off")
|
||||||
|
attr("name", "arch")
|
||||||
|
classes("form-check-input")
|
||||||
|
on("input") { inputEvent ->
|
||||||
|
(inputEvent.target as? HTMLInputElement)
|
||||||
|
?.takeIf(HTMLInputElement::checked)
|
||||||
|
?.let {
|
||||||
|
selectedArchitecture = arch
|
||||||
|
}
|
||||||
|
(inputEvent.target as? HTMLInputElement)?.value?.let {
|
||||||
|
fireTableRefreshEvent(inputElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
classes("form-check-label")
|
||||||
|
attr("for", id)
|
||||||
|
text(arch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div { table ->
|
||||||
|
classes("row", "align-items-md-stretch")
|
||||||
|
root.on(REFRESH_TABLE_EVENT_NAME) { evt ->
|
||||||
|
launch {
|
||||||
|
(evt as? CustomEvent)?.let { pnuv ->
|
||||||
|
table.let { t ->
|
||||||
|
selectedPackageName?.let { pkgName ->
|
||||||
|
t.removeChildren()
|
||||||
|
buildTable(t, pkgName, commonElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun main(vararg args: String) {
|
||||||
|
HtmlBuilder.of(document, document.body as HTMLElement) {
|
||||||
|
main { main ->
|
||||||
|
div {
|
||||||
|
classes("container", "py-4")
|
||||||
|
div {
|
||||||
|
classes("p-5", "mb-4", "bg-body-tertiary", "rounded-3")
|
||||||
|
div {
|
||||||
|
classes("container-fluid", "py-5")
|
||||||
|
h1 {
|
||||||
|
classes("display-5", "fw-bold")
|
||||||
|
text("Jpacrepo")
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
classes("col-md-8", "fs-4")
|
||||||
|
text("Personal archlinux package repository")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
classes("container")
|
||||||
|
div {
|
||||||
|
val commonElement = el
|
||||||
|
classes("row", "align-items-start")
|
||||||
|
div {
|
||||||
|
classes("col-3")
|
||||||
|
div {
|
||||||
|
classes("card")
|
||||||
|
div {
|
||||||
|
classes("card-header")
|
||||||
|
span {
|
||||||
|
classes("fs-5", "fw-semibold")
|
||||||
|
text("Package Cart")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun HtmlBuilder.createSelectedPackageList() {
|
||||||
|
if (selectedPackages.isNotEmpty()) {
|
||||||
|
div {
|
||||||
|
classes("card-body")
|
||||||
|
ul {
|
||||||
|
classes("list-group")
|
||||||
|
for (selectedPackage in selectedPackages) {
|
||||||
|
li {
|
||||||
|
classes(
|
||||||
|
"list-group-item",
|
||||||
|
"d-flex",
|
||||||
|
"justify-content-between",
|
||||||
|
"align-items-start"
|
||||||
|
)
|
||||||
|
text(selectedPackage.fileName)
|
||||||
|
i {
|
||||||
|
classes("bi-x-lg")
|
||||||
|
attr("style", "font-size: 1.5rem; color: grey;")
|
||||||
|
on("click") {
|
||||||
|
selectedPackages.remove(selectedPackage)
|
||||||
|
fireSelectedPackagesRefreshEvent(commonElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
classes("btn", "btn-primary", "w-100")
|
||||||
|
el.asDynamic().style.marginTop = "10px"
|
||||||
|
val totalSize = selectedPackages
|
||||||
|
.asSequence()
|
||||||
|
.map(PkgTuple::size)
|
||||||
|
.fold(0L, Long::plus)
|
||||||
|
text("Download (${totalSize.toHumanReadableByteSize()})")
|
||||||
|
on("click") {
|
||||||
|
val temporaryForm =
|
||||||
|
document.createElement("form") as HTMLFormElement
|
||||||
|
temporaryForm.method = "post"
|
||||||
|
temporaryForm.action =
|
||||||
|
SERVER_PREFIX + "api/pkg/downloadTar"
|
||||||
|
temporaryForm.asDynamic().style.display = "none"
|
||||||
|
val textField = document.createElement("input") as HTMLInputElement
|
||||||
|
textField.setAttribute("name", "pkgs")
|
||||||
|
textField.value = selectedPackages
|
||||||
|
.asSequence()
|
||||||
|
.map(PkgTuple::fileName)
|
||||||
|
.joinToString(" ")
|
||||||
|
temporaryForm.appendChild(textField)
|
||||||
|
el.appendChild(temporaryForm)
|
||||||
|
temporaryForm.submit()
|
||||||
|
el.removeChild(temporaryForm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
createSelectedPackageList()
|
||||||
|
commonElement.on(REFRESH_SELECTED_PACKAGES_EVENT_NAME) {
|
||||||
|
el.removeChildren()
|
||||||
|
createSelectedPackageList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
classes("col-9")
|
||||||
|
buildPackageSearchSection(commonElement)
|
||||||
|
}
|
||||||
|
el
|
||||||
|
}
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
classes("pt-3", "mt-4", "text-body-secondary", "border-top")
|
||||||
|
text("©2024")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class CompressionFormat(private val value: String) {
|
||||||
|
XZ("xz"), GZIP("gz"), Z_STANDARD("zst");
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PkgId(
|
||||||
|
var arch: String,
|
||||||
|
var name: String,
|
||||||
|
var version: String,
|
||||||
|
var compressionFormat: CompressionFormat,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PkgTuple(
|
||||||
|
var md5sum: String,
|
||||||
|
var fileName: String,
|
||||||
|
var size: Long,
|
||||||
|
)
|
||||||
|
|
11
jpacrepo-frontend/src/jsMain/resources/css/jpacrepo.css
Normal file
11
jpacrepo-frontend/src/jsMain/resources/css/jpacrepo.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
svg.bi-plus-circle {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table > tbody:nth-child(2) > tr > td:nth-child(1) > i {
|
||||||
|
visibility : hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table > tbody:nth-child(2) > tr:hover > td:nth-child(1) > i {
|
||||||
|
visibility : visible;
|
||||||
|
}
|
18
jpacrepo-frontend/src/jsMain/resources/index.html
Normal file
18
jpacrepo-frontend/src/jsMain/resources/index.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<link rel="stylesheet" href="css/jpacrepo.css"/>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
||||||
|
crossorigin="anonymous"/>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||||
|
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
<script src="jpacrepo-frontend.js" defer="true"></script>
|
||||||
|
<title>Hello World!</title>
|
||||||
|
</head>
|
||||||
|
</html>
|
@@ -1,70 +0,0 @@
|
|||||||
package net.woggioni.jpacrepo
|
|
||||||
|
|
||||||
import org.w3c.dom.Document
|
|
||||||
import org.w3c.dom.Element
|
|
||||||
|
|
||||||
|
|
||||||
class HtmlBuilder private constructor(private val doc : Document, val el: Element) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun <T> of(doc : Document, el: Element, cb : HtmlBuilder.(el : Element) -> T) : T {
|
|
||||||
return HtmlBuilder(doc, el).cb(el)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <T> dfd(
|
|
||||||
name : String,
|
|
||||||
attrs : Map<String, String>,
|
|
||||||
cb : HtmlBuilder.(el : Element) -> T) : T {
|
|
||||||
val child = doc.createElement(name)
|
|
||||||
for((key, value) in attrs) {
|
|
||||||
child.setAttribute(key, value)
|
|
||||||
}
|
|
||||||
el.appendChild(child)
|
|
||||||
return HtmlBuilder(doc, child).cb(child)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> html(attrs : Map<String, String> = emptyMap(),
|
|
||||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("html", attrs, cb)
|
|
||||||
fun <T> head(attrs : Map<String, String> = emptyMap(),
|
|
||||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("head", attrs, cb)
|
|
||||||
fun <T> body(attrs : Map<String, String> = emptyMap(),
|
|
||||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("body", attrs, cb)
|
|
||||||
|
|
||||||
fun <T> div(attrs : Map<String, String> = emptyMap(),
|
|
||||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("div", attrs, cb)
|
|
||||||
fun <T> header(attrs : Map<String, String> = emptyMap(),
|
|
||||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("header", attrs, cb)
|
|
||||||
fun <T> main(attrs : Map<String, String> = emptyMap(),
|
|
||||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("main", attrs, cb)
|
|
||||||
fun <T> footer(attrs : Map<String, String> = emptyMap(),
|
|
||||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("footer", attrs, cb)
|
|
||||||
fun <T> a(attrs : Map<String, String> = emptyMap(),
|
|
||||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("a", attrs, cb)
|
|
||||||
fun <T> meta(attrs : Map<String, String> = emptyMap(),
|
|
||||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("meta", attrs, cb)
|
|
||||||
fun <T> script(attrs : Map<String, String> = emptyMap(),
|
|
||||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("script", attrs, cb)
|
|
||||||
fun <T> link(attrs : Map<String, String> = emptyMap(),
|
|
||||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("link", attrs, cb)
|
|
||||||
fun <T> title(attrs : Map<String, String> = emptyMap(),
|
|
||||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("title", attrs, cb)
|
|
||||||
fun <T> p(attrs : Map<String, String> = emptyMap(),
|
|
||||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("p", attrs, cb)
|
|
||||||
|
|
||||||
fun <T> h1(attrs : Map<String, String> = emptyMap(),
|
|
||||||
cb : HtmlBuilder.(el : Element) -> T) : T = dfd("h1", attrs, cb)
|
|
||||||
|
|
||||||
|
|
||||||
fun classes(vararg classes : String) {
|
|
||||||
for(cls in classes) el.classList.add(cls)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun attr(key: String, value : String) {
|
|
||||||
el.setAttribute(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun text(txt : String) {
|
|
||||||
el.textContent = txt
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,61 +0,0 @@
|
|||||||
package net.woggioni.jpacrepo
|
|
||||||
|
|
||||||
import kotlinx.browser.document
|
|
||||||
import net.woggioni.jpacrepo.Component.container
|
|
||||||
import org.w3c.dom.HTMLElement
|
|
||||||
|
|
||||||
|
|
||||||
object Component {
|
|
||||||
val container = document.getElementById("container") as HTMLElement
|
|
||||||
|
|
||||||
fun sayHelloViaDom() {
|
|
||||||
container.textContent = "Hello, DOM! Kotlin is writing…"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun main(vararg args: String) {
|
|
||||||
// println("JavaScript generated through Kotlin")
|
|
||||||
//
|
|
||||||
// Component.sayHelloViaDom()
|
|
||||||
// sayHelloViaJsConsole()
|
|
||||||
// sayHelloViaInlinedJavaScript()
|
|
||||||
|
|
||||||
HtmlBuilder.of(document, document.body as HTMLElement) {
|
|
||||||
classes("d-flex", "h-100", "text-center", "text-white", "bg-dark")
|
|
||||||
div {
|
|
||||||
classes("cover-container", "d-flex", "w-100", "h-100", "p-3", "mx-auto", "flex-column")
|
|
||||||
|
|
||||||
}
|
|
||||||
header {
|
|
||||||
classes("mb-auto")
|
|
||||||
}
|
|
||||||
main {
|
|
||||||
classes("px-3")
|
|
||||||
h1 {
|
|
||||||
text("Cover your page.")
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
classes("lead")
|
|
||||||
text("Cover is a one-page template for building simple and beautiful home pages. Download, edit the text, and add your own fullscreen background photo to make it your own.")
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
classes("lead")
|
|
||||||
a {
|
|
||||||
classes("btn", "btn-lg", "btn-secondary", "fw-bold", "border-white", "bg-white")
|
|
||||||
text("Learn more")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
classes("mt-auto", "text-white-50")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sayHelloViaJsConsole() {
|
|
||||||
console.log("Hello from `console.log()`!")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sayHelloViaInlinedJavaScript() {
|
|
||||||
js("document.writeln('Hello, from inlined JavaScript in Kotlin!')")
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
|
||||||
<script src="jpacrepo-frontend.js" defer="true"></script>
|
|
||||||
<title>Hello World!</title>
|
|
||||||
</html>
|
|
@@ -310,7 +310,7 @@ public class PkgDataParser {
|
|||||||
List<String> value = entry.getValue();
|
List<String> value = entry.getValue();
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "size":
|
case "size":
|
||||||
data.setSize(Long.parseLong(value.get(0)));
|
data.setInstalledSize(Long.parseLong(value.get(0)));
|
||||||
break;
|
break;
|
||||||
case "arch":
|
case "arch":
|
||||||
data.getPkgId().setArch(value.get(0));
|
data.getPkgId().setArch(value.get(0));
|
||||||
@@ -395,6 +395,7 @@ public class PkgDataParser {
|
|||||||
data.setMd5sum(Hash.hash(Hash.Algorithm.MD5, fis).getBytes());
|
data.setMd5sum(Hash.hash(Hash.Algorithm.MD5, fis).getBytes());
|
||||||
}
|
}
|
||||||
data.setFileName(file.getFileName().toString());
|
data.setFileName(file.getFileName().toString());
|
||||||
|
data.setSize(Files.size(file));
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@@ -110,7 +110,6 @@ public class PackageSynchronizerEJB implements FileSystemSynchronizer {
|
|||||||
long[] count = new long[]{0};
|
long[] count = new long[]{0};
|
||||||
long totalPackages = fileListStreamSupplier.get().count();
|
long totalPackages = fileListStreamSupplier.get().count();
|
||||||
var parser = new PkgDataParser(em);
|
var parser = new PkgDataParser(em);
|
||||||
// List<PkgData> stack = new ArrayList<>();
|
|
||||||
Con<Boolean> persistPackages = (Boolean drain) -> {
|
Con<Boolean> persistPackages = (Boolean drain) -> {
|
||||||
while (inProgress.size() > maxInProgress || (drain && !inProgress.isEmpty())) {
|
while (inProgress.size() > maxInProgress || (drain && !inProgress.isEmpty())) {
|
||||||
Optional.ofNullable(completionService.poll(1, TimeUnit.SECONDS))
|
Optional.ofNullable(completionService.poll(1, TimeUnit.SECONDS))
|
||||||
@@ -123,16 +122,6 @@ public class PackageSynchronizerEJB implements FileSystemSynchronizer {
|
|||||||
throw ee.getCause();
|
throw ee.getCause();
|
||||||
}
|
}
|
||||||
persistPackage(em, parser, pkgData, ++count[0], totalPackages);
|
persistPackage(em, parser, pkgData, ++count[0], totalPackages);
|
||||||
// stack.add(pkgData);
|
|
||||||
// if(stack.size() >= 1000 || drain) {
|
|
||||||
// parser.addNewDependencies(parser.getNewDependencies(stack));
|
|
||||||
// parser.addNewPackagers(parser.getNewPackagers(stack));
|
|
||||||
// parser.addNewPkgBases(parser.getNewPkgBases(stack));
|
|
||||||
// parser.addNewLicenses(parser.getNewLicenses(stack));
|
|
||||||
// while(!stack.isEmpty()) {
|
|
||||||
// persistPackage(em, parser, stack.removeLast(), ++count[0], totalPackages);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -204,13 +204,16 @@ public class PacmanServiceEJB implements PacmanServiceLocal {
|
|||||||
else {
|
else {
|
||||||
try (OutputStream output = Files.newOutputStream(file)) {
|
try (OutputStream output = Files.newOutputStream(file)) {
|
||||||
JWO.copy(input, output, 0x10000);
|
JWO.copy(input, output, 0x10000);
|
||||||
|
var parser = new PkgDataParser(em);
|
||||||
PkgData pkg = PkgDataParser.parseFile(file,
|
PkgData pkg = PkgDataParser.parseFile(file,
|
||||||
CompressionFormatImpl.guess(Paths.get(fileName)));
|
CompressionFormatImpl.guess(Paths.get(fileName)));
|
||||||
pkg.setFileName(fileName);
|
pkg.setFileName(fileName);
|
||||||
|
pkg = parser.hydrateJPA(pkg);
|
||||||
getPackage(pkg.getPkgId()).ifPresent((Con<PkgData>) (pkgData -> {
|
getPackage(pkg.getPkgId()).ifPresent((Con<PkgData>) (pkgData -> {
|
||||||
em.remove(pkgData);
|
em.remove(pkgData);
|
||||||
Files.delete(ctx.getRepoFolder().resolve(pkgData.getFileName()));
|
Files.delete(ctx.getRepoFolder().resolve(pkgData.getFileName()));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
logger.info("Persisting package {}", pkg.getFileName());
|
logger.info("Persisting package {}", pkg.getFileName());
|
||||||
em.persist(pkg);
|
em.persist(pkg);
|
||||||
Files.move(file, ctx.getRepoFolder().resolve(fileName), StandardCopyOption.ATOMIC_MOVE);
|
Files.move(file, ctx.getRepoFolder().resolve(fileName), StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
@@ -45,6 +45,7 @@ import net.woggioni.jpacrepo.api.wire.StringList;
|
|||||||
import net.woggioni.jpacrepo.config.AppConfig;
|
import net.woggioni.jpacrepo.config.AppConfig;
|
||||||
import net.woggioni.jpacrepo.version.VersionComparator;
|
import net.woggioni.jpacrepo.version.VersionComparator;
|
||||||
import net.woggioni.jwo.CollectionUtils;
|
import net.woggioni.jwo.CollectionUtils;
|
||||||
|
import net.woggioni.jwo.Fun;
|
||||||
import net.woggioni.jwo.JWO;
|
import net.woggioni.jwo.JWO;
|
||||||
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
||||||
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
|
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
|
||||||
@@ -65,6 +66,8 @@ import java.util.NavigableMap;
|
|||||||
import java.util.NavigableSet;
|
import java.util.NavigableSet;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.ToLongFunction;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static net.woggioni.jpacrepo.api.security.Roles.WRITER;
|
import static net.woggioni.jpacrepo.api.security.Roles.WRITER;
|
||||||
@@ -140,19 +143,17 @@ public class PacmanWebService {
|
|||||||
EntityTag etag = new EntityTag(Integer.toString(cachedMap.hashCode()), true);
|
EntityTag etag = new EntityTag(Integer.toString(cachedMap.hashCode()), true);
|
||||||
Response.ResponseBuilder builder = request.evaluatePreconditions(etag);
|
Response.ResponseBuilder builder = request.evaluatePreconditions(etag);
|
||||||
if (builder == null) {
|
if (builder == null) {
|
||||||
|
TreeMap<String, TreeMap<String, Map<String, Map<CompressionFormat, PkgTuple>>>> result = cachedMap.entrySet().stream().collect(
|
||||||
TreeMap<String, TreeMap<String, Map<String, NavigableSet<PkgTuple>>>> result = cachedMap.entrySet().stream().collect(
|
|
||||||
Collectors.groupingBy((Map.Entry<PkgId, PkgTuple> entry) -> entry.getKey().getArch(),
|
Collectors.groupingBy((Map.Entry<PkgId, PkgTuple> entry) -> entry.getKey().getArch(),
|
||||||
TreeMap::new,
|
TreeMap::new,
|
||||||
Collectors.groupingBy((Map.Entry<PkgId, PkgTuple> entry) -> entry.getKey().getName(),
|
Collectors.groupingBy((Map.Entry<PkgId, PkgTuple> entry) -> entry.getKey().getName(),
|
||||||
TreeMap::new,
|
TreeMap::new,
|
||||||
Collectors.groupingBy((Map.Entry<PkgId, PkgTuple> entry) -> entry.getKey().getVersion(),
|
Collectors.groupingBy((Map.Entry<PkgId, PkgTuple> entry) -> entry.getKey().getVersion(),
|
||||||
() -> new TreeMap<>(VersionComparator.getInstance().reversed()),
|
() -> new TreeMap<>(VersionComparator.getInstance().reversed()),
|
||||||
Collectors.mapping(
|
CollectionUtils.toMap(
|
||||||
Map.Entry::getValue,
|
TreeMap::new,
|
||||||
CollectionUtils.toUnmodifiableTreeSet(
|
(Map.Entry<PkgId, PkgTuple> entry) -> entry.getKey().getCompressionFormat(),
|
||||||
Comparator.comparing(PkgTuple::getFileName)
|
Map.Entry::getValue
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -305,6 +306,11 @@ public class PacmanWebService {
|
|||||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
public Response downloadTar(@FormParam("pkgs") String formData) {
|
public Response downloadTar(@FormParam("pkgs") String formData) {
|
||||||
String[] files = URLDecoder.decode(formData, StandardCharsets.UTF_8).split(" ");
|
String[] files = URLDecoder.decode(formData, StandardCharsets.UTF_8).split(" ");
|
||||||
|
Fun<java.nio.file.Path, Long> fun = Files::size;
|
||||||
|
long estimatedSize = Arrays.stream(files)
|
||||||
|
.map(ctx::getFile)
|
||||||
|
.mapToLong(fun::apply)
|
||||||
|
.sum();
|
||||||
Arrays.stream(files)
|
Arrays.stream(files)
|
||||||
.filter(fileName -> !Files.exists(ctx.getFile(fileName)))
|
.filter(fileName -> !Files.exists(ctx.getFile(fileName)))
|
||||||
.forEach(fileName -> {
|
.forEach(fileName -> {
|
||||||
@@ -318,11 +324,11 @@ public class PacmanWebService {
|
|||||||
public void write(OutputStream output) {
|
public void write(OutputStream output) {
|
||||||
try(TarArchiveOutputStream taos = new TarArchiveOutputStream(output)) {
|
try(TarArchiveOutputStream taos = new TarArchiveOutputStream(output)) {
|
||||||
for (String fname : files) {
|
for (String fname : files) {
|
||||||
log.info(fname);
|
|
||||||
java.nio.file.Path file = ctx.getFile(fname);
|
java.nio.file.Path file = ctx.getFile(fname);
|
||||||
try(InputStream input = Files.newInputStream(file)) {
|
try(InputStream input = Files.newInputStream(file)) {
|
||||||
TarArchiveEntry entry = new TarArchiveEntry(fname);
|
TarArchiveEntry entry = new TarArchiveEntry(fname);
|
||||||
entry.setSize(Files.size(file));
|
long fileSize = Files.size(file);
|
||||||
|
entry.setSize(fileSize);
|
||||||
taos.putArchiveEntry(entry);
|
taos.putArchiveEntry(entry);
|
||||||
JWO.copy(input, taos, buffer);
|
JWO.copy(input, taos, buffer);
|
||||||
taos.closeArchiveEntry();
|
taos.closeArchiveEntry();
|
||||||
@@ -334,7 +340,10 @@ public class PacmanWebService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return Response.ok(stream).header("Content-Disposition", "attachment; filename=pkgs.tar").build();
|
return Response.ok(stream)
|
||||||
|
.header("Content-Length", estimatedSize)
|
||||||
|
.header("Content-Disposition", "attachment; filename=pkgs.tar")
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
@@ -151,7 +151,7 @@ public class Queries {
|
|||||||
public static TypedQuery<PkgData> getPackageById(EntityManager em, PkgId pkgId) {
|
public static TypedQuery<PkgData> getPackageById(EntityManager em, PkgId pkgId) {
|
||||||
var cb = em.getCriteriaBuilder();
|
var cb = em.getCriteriaBuilder();
|
||||||
var cq = cb.createQuery(PkgData.class);
|
var cq = cb.createQuery(PkgData.class);
|
||||||
var root = cq.from(PkgData_.class_);
|
var root = cq.from(PkgData.class);
|
||||||
cq.select(root).where(
|
cq.select(root).where(
|
||||||
cb.equal(
|
cb.equal(
|
||||||
root.get(PkgData_.pkgId),
|
root.get(PkgData_.pkgId),
|
||||||
|
@@ -12,6 +12,9 @@
|
|||||||
<column name="size" type="BIGINT">
|
<column name="size" type="BIGINT">
|
||||||
<constraints nullable="false"/>
|
<constraints nullable="false"/>
|
||||||
</column>
|
</column>
|
||||||
|
<column name="installedsize" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
<column name="updtimestamp" type="TIMESTAMP WITH TIME ZONE"/>
|
<column name="updtimestamp" type="TIMESTAMP WITH TIME ZONE"/>
|
||||||
<column name="arch" type="VARCHAR(16)"/>
|
<column name="arch" type="VARCHAR(16)"/>
|
||||||
<column name="md5sum" type="BYTEA"/>
|
<column name="md5sum" type="BYTEA"/>
|
||||||
|
@@ -10,7 +10,7 @@ import jakarta.ws.rs.core.Response;
|
|||||||
import jakarta.ws.rs.core.UriBuilder;
|
import jakarta.ws.rs.core.UriBuilder;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import net.woggioni.jpacrepo.api.model.PkgData;
|
import net.woggioni.jpacrepo.api.model.PkgData;
|
||||||
import net.woggioni.jpacrepo.api.service.PacmanServiceRemote;
|
import net.woggioni.jpacrepo.api.service.FileSystemSynchronizer;
|
||||||
import net.woggioni.jpacrepo.impl.model.CompressionFormatImpl;
|
import net.woggioni.jpacrepo.impl.model.CompressionFormatImpl;
|
||||||
import net.woggioni.jpacrepo.impl.model.PkgDataParser;
|
import net.woggioni.jpacrepo.impl.model.PkgDataParser;
|
||||||
import net.woggioni.jwo.Con;
|
import net.woggioni.jwo.Con;
|
||||||
@@ -137,12 +137,12 @@ public class ClientTest {
|
|||||||
Context ctx = new InitialContext(prop);
|
Context ctx = new InitialContext(prop);
|
||||||
traverseJndiNode("/", context);
|
traverseJndiNode("/", context);
|
||||||
// final PacmanService stateService = (PacmanService) ctx.lookup("/jpacrepo-1.0/remote/PacmanServiceEJB!service.PacmanService");
|
// final PacmanService stateService = (PacmanService) ctx.lookup("/jpacrepo-1.0/remote/PacmanServiceEJB!service.PacmanService");
|
||||||
final PacmanServiceRemote service = (PacmanServiceRemote) ctx.lookup(
|
final FileSystemSynchronizer service = (FileSystemSynchronizer) ctx.lookup(
|
||||||
"/jpacrepo/PacmanServiceEJB!net.woggioni.jpacrepo.api.service.PacmanServiceRemote"
|
"/jpacrepo/PackageSynchronizerEJB!net.woggioni.jpacrepo.api.service.FileSystemSynchronizer"
|
||||||
);
|
);
|
||||||
// List<PkgData> pkgs = service.searchPackage("google-earth", null, null, 1, 10);
|
// List<PkgData> pkgs = service.searchPackage("google-earth", null, null, 1, 10);
|
||||||
// System.out.println(new XStream().toXML(pkgs));
|
// System.out.println(new XStream().toXML(pkgs));
|
||||||
service.syncDB();
|
service.syncDb();
|
||||||
// service.searchPkgId("jre8-openjdk", null, null, null)
|
// service.searchPkgId("jre8-openjdk", null, null, null)
|
||||||
// .stream()
|
// .stream()
|
||||||
// .map(service::getPackage)
|
// .map(service::getPackage)
|
||||||
|
Reference in New Issue
Block a user