aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/moe/nea/firmament/repo/RepoDownloadManager.kt
blob: 18f8e49cf6db0bdf96443257e30e4eca890592cc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/*
 * SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

package moe.nea.firmament.repo

import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.utils.io.jvm.nio.*
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import moe.nea.firmament.Firmament
import moe.nea.firmament.Firmament.logger
import moe.nea.firmament.util.iterate
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.util.zip.ZipInputStream
import kotlin.io.path.*


object RepoDownloadManager {

    val repoSavedLocation = Firmament.DATA_DIR.resolve("repo-extracted")
    val repoMetadataLocation = Firmament.DATA_DIR.resolve("loaded-repo-sha.txt")

    private fun loadSavedVersionHash(): String? =
            if (repoSavedLocation.exists()) {
                if (repoMetadataLocation.exists()) {
                    try {
                        repoMetadataLocation.readText().trim()
                    } catch (e: IOException) {
                        null
                    }
                } else {
                    null
                }
            } else null

    private fun saveVersionHash(versionHash: String) {
        latestSavedVersionHash = versionHash
        repoMetadataLocation.writeText(versionHash)
    }

    var latestSavedVersionHash: String? = loadSavedVersionHash()
        private set

    @Serializable
    private class GithubCommitsResponse(val sha: String)

    private suspend fun requestLatestGithubSha(): String? {
        val response =
                Firmament.httpClient.get("https://api.github.com/repos/${RepoManager.Config.username}/${RepoManager.Config.reponame}/commits/${RepoManager.Config.branch}")
        if (response.status.value != 200) {
            return null
        }
        return response.body<GithubCommitsResponse>().sha
    }

    private suspend fun downloadGithubArchive(url: String): Path = withContext(IO) {
        val response = Firmament.httpClient.get(url)
        val targetFile = Files.createTempFile("firmament-repo", ".zip")
        val outputChannel = Files.newByteChannel(targetFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE)
        response.bodyAsChannel().copyTo(outputChannel)
        targetFile
    }

    /**
     * Downloads the latest repository from github, setting [latestSavedVersionHash].
     * @return true, if an update was performed, false, otherwise (no update needed, or wasn't able to complete update)
     */
    suspend fun downloadUpdate(force: Boolean): Boolean = withContext(CoroutineName("Repo Update Check")) {
        val latestSha = requestLatestGithubSha()
        if (latestSha == null) {
            logger.warn("Could not request github API to retrieve latest REPO sha.")
            return@withContext false
        }
        val currentSha = loadSavedVersionHash()
        if (latestSha != currentSha || force) {
            val requestUrl = "https://github.com/${RepoManager.Config.username}/${RepoManager.Config.reponame}/archive/$latestSha.zip"
            logger.info("Planning to upgrade repository from $currentSha to $latestSha from $requestUrl")
            val zipFile = downloadGithubArchive(requestUrl)
            logger.info("Download repository zip file to $zipFile. Deleting old repository")
            withContext(IO) { repoSavedLocation.toFile().deleteRecursively() }
            logger.info("Extracting new repository")
            withContext(IO) { extractNewRepository(zipFile) }
            logger.info("Repository loaded on disk.")
            saveVersionHash(latestSha)
            return@withContext true
        } else {
            logger.debug("Repository on latest sha $currentSha. Not performing update")
            return@withContext false
        }
    }

    private fun extractNewRepository(zipFile: Path) {
        repoSavedLocation.createDirectories()
        ZipInputStream(zipFile.inputStream()).use { cis ->
            while (true) {
                val entry = cis.nextEntry ?: break
                if (entry.isDirectory) continue
                val extractedLocation =
                        repoSavedLocation.resolve(
                                entry.name.substringAfter('/', missingDelimiterValue = "")
                        )
                if (repoSavedLocation !in extractedLocation.iterate { it.parent }) {
                    logger.error("Not Enough Updates detected an invalid zip file. This is a potential security risk, please report this in the Moulberry discord.")
                    throw RuntimeException("Not Enough Updates detected an invalid zip file. This is a potential security risk, please report this in the Moulberry discord.")
                }
                extractedLocation.parent.createDirectories()
                cis.copyTo(extractedLocation.outputStream())
                cis.closeEntry()
            }
        }
    }


}