diff --git a/.gitignore b/.gitignore index 73dc4a8..f4c7f96 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,5 @@ kotlin-js-store/ tailwind/ backend/src/main/resources/static/css/tailwind.css + +output/ diff --git a/backend/src/main/kotlin/api/ApiRouting.kt b/backend/src/main/kotlin/api/ApiRouting.kt index 7fe29ef..704903e 100644 --- a/backend/src/main/kotlin/api/ApiRouting.kt +++ b/backend/src/main/kotlin/api/ApiRouting.kt @@ -1,11 +1,16 @@ package dev.triumphteam.backend.api +import dev.triumphteam.backend.DATA_FOLDER import dev.triumphteam.website.api.Api import dev.triumphteam.website.project.Repository import io.ktor.http.HttpStatusCode +import io.ktor.http.content.PartData +import io.ktor.http.content.forEachPart +import io.ktor.http.content.streamProvider import io.ktor.server.application.call import io.ktor.server.auth.authenticate import io.ktor.server.request.receive +import io.ktor.server.request.receiveMultipart import io.ktor.server.resources.post import io.ktor.server.response.respond import io.ktor.server.routing.Routing @@ -19,11 +24,22 @@ public fun Routing.apiRoutes() { authenticate("bearer") { post { runCatching { - call.receive() + call.receiveMultipart() }.fold( - onSuccess = { (projects) -> + onSuccess = { multipartData -> // Handle parsing - setupRepository(projects) + multipartData.forEachPart { part -> + when (part) { + is PartData.FileItem -> { + val fileBytes = part.streamProvider().readBytes() + DATA_FOLDER.resolve("downloads/projects.zip").writeBytes(fileBytes) + } + + else -> {} + } + part.dispose() + } + call.respond(HttpStatusCode.Accepted) }, onFailure = { diff --git a/backend/src/main/kotlin/api/ProjectSetup.kt b/backend/src/main/kotlin/api/ProjectSetup.kt index 3cb7d12..45202ae 100644 --- a/backend/src/main/kotlin/api/ProjectSetup.kt +++ b/backend/src/main/kotlin/api/ProjectSetup.kt @@ -22,12 +22,11 @@ public fun setupRepository(projects: List) { val projectEntity = ProjectEntity.new(project.id) { this.name = project.name - this.icon = project.icon this.color = project.color this.github = project.projectHome } - val projectIcon = ImageIO.read(URL(project.icon)) + val projectIcon = ImageIO.read(URL("")) project.versions.forEach { version -> diff --git a/common/src/main/kotlin/project/SerialRepresentation.kt b/common/src/main/kotlin/project/SerialRepresentation.kt index 50cd0c9..d01c450 100644 --- a/common/src/main/kotlin/project/SerialRepresentation.kt +++ b/common/src/main/kotlin/project/SerialRepresentation.kt @@ -11,7 +11,6 @@ public data class Repository( public data class Project( public val id: String, public val name: String, - public val icon: String, public val color: String, public val projectHome: String, public val versions: List, diff --git a/docs/build.gradle.kts b/docs/build.gradle.kts index 56ee2ae..aa8b64a 100644 --- a/docs/build.gradle.kts +++ b/docs/build.gradle.kts @@ -10,4 +10,6 @@ dependencies { implementation(libs.bundles.logger) implementation(libs.bundles.commonmark) implementation(libs.commons.cli) + + implementation("net.lingala.zip4j:zip4j:2.11.5") } diff --git a/docs/src/main/kotlin/Application.kt b/docs/src/main/kotlin/Application.kt index 86508cc..b1ca89b 100644 --- a/docs/src/main/kotlin/Application.kt +++ b/docs/src/main/kotlin/Application.kt @@ -23,14 +23,20 @@ import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest +import io.ktor.client.plugins.onUpload import io.ktor.client.plugins.resources.Resources import io.ktor.client.plugins.resources.post import io.ktor.client.request.bearerAuth +import io.ktor.client.request.forms.MultiPartFormDataContent +import io.ktor.client.request.forms.formData import io.ktor.client.request.setBody import io.ktor.http.ContentType +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode -import io.ktor.http.contentType import io.ktor.serialization.kotlinx.json.json +import io.ktor.util.InternalAPI +import net.lingala.zip4j.ZipFile import org.apache.commons.cli.DefaultParser import org.apache.commons.cli.Option import org.apache.commons.cli.Options @@ -63,6 +69,7 @@ private val mdParser = Parser.builder().extensions(DEFAULT_EXTENSIONS).build() private val logger: Logger = LoggerFactory.getLogger("docs") +@OptIn(InternalAPI::class) public suspend fun main(args: Array) { val options = DefaultParser().parse( @@ -90,33 +97,56 @@ public suspend fun main(args: Array) { HoconSerializer.from(requireNotNull(inputFiles.find { it.name == "settings.conf" })) // Navigate through file structure and parse all projects - val repo = Repository(projects = inputFiles.mapNotNull { projectDir -> - // Ignore non-directory files - if (!projectDir.isDirectory) return@mapNotNull null + val projects = Projects( + projects = inputFiles.mapNotNull { projectDir -> + // Ignore non-directory files + if (!projectDir.isDirectory) return@mapNotNull null + + val files = projectDir.listFiles() ?: emptyArray() + val projectConfig = files.findFile("project.conf") { + "Found project folder without a 'project.conf' file, skipping it!" + } ?: return@mapNotNull null - val files = projectDir.listFiles() ?: emptyArray() - val projectConfig = files.find("project.conf") { - "Found project folder without a 'project.conf' file, skipping it!" - } ?: return@mapNotNull null + val parsedProjectConfig = HoconSerializer.from(projectConfig) + + ProjectWithIcon( + project = Project( + id = parsedProjectConfig.id, + name = parsedProjectConfig.name, + color = parsedProjectConfig.color, + projectHome = parsedProjectConfig.projectHome, + versions = parseVersions(files.filter(File::isDirectory), inputPath, repoSettings), + ), + icon = requireNotNull(files.findFile("icon.png")) { + "Found project folder without a 'project.conf' file, skipping it!" + } + ).also { + logger.info("Parsed project '${it.project.id}', with versions: ${it.project.versions.map(DocVersion::reference)}!") + } + }, + ) - val parsedProjectConfig = HoconSerializer.from(projectConfig) - - Project( - id = parsedProjectConfig.id, - name = parsedProjectConfig.name, - icon = parsedProjectConfig.icon, - color = parsedProjectConfig.color, - projectHome = parsedProjectConfig.projectHome, - versions = parseVersions(files.filter(File::isDirectory), inputPath, repoSettings), - ).also { - logger.info("Parsed project '$${it.id}', with versions: ${it.versions.map(DocVersion::reference)}!") + val outputDir = File("output").also(File::mkdirs) + + val repository = outputDir.resolve("repository.json").also { + it.writeText(JsonSerializer.encode(projects.toRepository())) + } + + val icons = projects.projects.map { + outputDir.resolve(it.project.id).also { dir -> + dir.mkdirs() + it.icon.copyTo(dir.resolve("icon.png"), overwrite = true) } - }) + } - println(JsonSerializer.encode(repo)) - return + val zip = ZipFile("projects.zip").also { + icons.forEach { dir -> + it.addFolder(dir) + } + it.addFile(repository) + } - logger.info("Paring complete!") + logger.info("Parsing complete!") logger.info("Uploading..") val client = HttpClient(CIO) { @@ -128,12 +158,24 @@ public suspend fun main(args: Array) { defaultRequest { url(url) bearerAuth(bearer) - contentType(ContentType.Application.Json) } } val response = client.post(Api.Setup()) { - setBody(repo) + setBody( + MultiPartFormDataContent( + formData { + append("zip", zip.file.readBytes(), Headers.build { + append(HttpHeaders.ContentType, ContentType.Application.Zip) + append(HttpHeaders.ContentDisposition, "filename=\"projects.zip\"") + }) + }, + boundary = "WebAppBoundary" + ) + ) + onUpload { bytesSentTotal, contentLength -> + logger.info("Sent $bytesSentTotal bytes from $contentLength") + } } if (response.status != HttpStatusCode.Accepted) { @@ -148,7 +190,7 @@ private fun parseVersions(versionDirs: List, parentDir: File, repoSettings return versionDirs.mapNotNull { versionDir -> val files = versionDir.listFiles() ?: emptyArray() - val versionConfig = files.find("version.conf") { + val versionConfig = files.findFile("version.conf") { "Found version folder without a 'version.conf' file, skipping it!" } ?: return@mapNotNull null @@ -159,7 +201,7 @@ private fun parseVersions(versionDirs: List, parentDir: File, repoSettings files.filter(File::isDirectory).sortedBy(File::getName).forEach { groupDir -> val groupFiles = groupDir.listFiles() ?: emptyArray() - val groupConfig = groupFiles.find("group.conf") { + val groupConfig = groupFiles.findFile("group.conf") { "Found group folder without a 'group.conf' file, skipping it!" } ?: return@mapNotNull null @@ -212,13 +254,26 @@ private fun parseVersions(versionDirs: List, parentDir: File, repoSettings } } -private fun Array.find(fileName: String, log: () -> String): File? { +private fun Array.findFile(fileName: String, log: (() -> String)? = null): File? { val file = find { it.name == fileName } if (file == null) { - logger.warn(log()) + log?.invoke()?.let { logger.warn(it) } return null } return file } + +private data class ProjectWithIcon(val project: Project, val icon: File) + +private data class Projects(val projects: List) { + + fun toRepository(): Repository { + return Repository(projects.map(ProjectWithIcon::project)) + } + + fun icons(): List { + return projects.map(ProjectWithIcon::icon) + } +} diff --git a/docs/src/main/kotlin/serialization/FileRepresentation.kt b/docs/src/main/kotlin/serialization/FileRepresentation.kt index 6b49add..ea19b15 100644 --- a/docs/src/main/kotlin/serialization/FileRepresentation.kt +++ b/docs/src/main/kotlin/serialization/FileRepresentation.kt @@ -10,7 +10,6 @@ public data class RepoSettings(public val editPath: String) public data class ProjectConfig( public val id: String, public val name: String, - public val icon: String, public val color: String, public val projectHome: String, )