GitHub.kt 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. /*
  2. * Copyright 2020 Mamoe Technologies and contributors.
  3. *
  4. * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  5. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
  6. *
  7. * https://github.com/mamoe/mirai/blob/master/LICENSE
  8. */
  9. @file:Suppress("EXPERIMENTAL_API_USAGE")
  10. package upload
  11. import com.google.gson.JsonObject
  12. import com.google.gson.JsonParser
  13. import io.ktor.client.HttpClient
  14. import io.ktor.client.engine.cio.CIO
  15. import io.ktor.client.features.HttpTimeout
  16. import io.ktor.client.request.put
  17. import kotlinx.coroutines.Dispatchers
  18. import kotlinx.coroutines.delay
  19. import kotlinx.coroutines.runBlocking
  20. import kotlinx.coroutines.withContext
  21. import org.gradle.api.Project
  22. import org.gradle.kotlin.dsl.provideDelegate
  23. import org.jsoup.Connection
  24. import org.jsoup.Jsoup
  25. import java.io.File
  26. import java.util.*
  27. internal val Http = HttpClient(CIO) {
  28. engine {
  29. requestTimeout = 600_000
  30. }
  31. install(HttpTimeout) {
  32. socketTimeoutMillis = 600_000
  33. requestTimeoutMillis = 600_000
  34. connectTimeoutMillis = 600_000
  35. }
  36. }
  37. object GitHub {
  38. private fun getGithubToken(project: Project): String {
  39. kotlin.runCatching {
  40. @Suppress("UNUSED_VARIABLE", "LocalVariableName")
  41. val github_token: String by project
  42. return github_token
  43. }
  44. System.getProperty("github_token", null)?.let {
  45. return it.trim()
  46. }
  47. File(File(System.getProperty("user.dir")).parent, "/token.txt").let { local ->
  48. if (local.exists()) {
  49. return local.readText().trim()
  50. }
  51. }
  52. File(File(System.getProperty("user.dir")), "/token.txt").let { local ->
  53. if (local.exists()) {
  54. return local.readText().trim()
  55. }
  56. }
  57. error(
  58. "Cannot find github token, " +
  59. "please specify by creating a file token.txt in project dir, " +
  60. "or by providing JVM parameter 'github_token'"
  61. )
  62. }
  63. fun upload(file: File, project: Project, repo: String, targetFilePath: String) = runBlocking {
  64. val token = getGithubToken(project)
  65. println("token.length=${token.length}")
  66. val url = "https://api.github.com/repos/project-mirai/$repo/contents/$targetFilePath"
  67. retryCatching(100, onFailure = { delay(30_000) }) { // 403 forbidden?
  68. Http.put<String>("$url?access_token=$token") {
  69. val sha = retryCatching(3, onFailure = { delay(30_000) }) {
  70. getGithubSha(
  71. repo,
  72. targetFilePath,
  73. "master",
  74. project
  75. )
  76. }.getOrNull()
  77. println("sha=$sha")
  78. val content = String(Base64.getEncoder().encode(file.readBytes()))
  79. body = """
  80. {
  81. "message": "automatically upload on release",
  82. "content": "$content"
  83. ${if (sha == null) "" else """, "sha": "$sha" """}
  84. }
  85. """.trimIndent()
  86. }.let {
  87. println("Upload response: $it")
  88. }
  89. delay(1000)
  90. }.getOrThrow()
  91. }
  92. private suspend fun getGithubSha(
  93. repo: String,
  94. filePath: String,
  95. branch: String,
  96. project: Project
  97. ): String? {
  98. fun String.asJson(): JsonObject {
  99. return JsonParser.parseString(this).asJsonObject
  100. }
  101. /*
  102. * 只能获取1M以内/branch为master的sha
  103. * */
  104. class TargetTooLargeException : Exception("Target TOO Large")
  105. suspend fun getShaSmart(repo: String, filePath: String, project: Project): String? {
  106. return withContext(Dispatchers.IO) {
  107. val response = Jsoup
  108. .connect(
  109. "https://api.github.com/repos/project-mirai/$repo/contents/$filePath?access_token=" + getGithubToken(
  110. project
  111. )
  112. )
  113. .ignoreContentType(true)
  114. .ignoreHttpErrors(true)
  115. .method(Connection.Method.GET)
  116. .execute()
  117. if (response.statusCode() == 404) {
  118. null
  119. } else {
  120. val p = response.body().asJson()
  121. if (p.has("message") && p["message"].asString == "This API returns blobs up to 1 MB in size. The requested blob is too large to fetch via the API, but you can use the Git Data API to request blobs up to 100 MB in size.") {
  122. throw TargetTooLargeException()
  123. }
  124. p.get("sha").asString
  125. }
  126. }
  127. }
  128. suspend fun getShaStupid(
  129. repo: String,
  130. filePath: String,
  131. branch: String,
  132. project: Project
  133. ): String? {
  134. val resp = withContext(Dispatchers.IO) {
  135. Jsoup
  136. .connect(
  137. "https://api.github.com/repos/project-mirai/$repo/git/ref/heads/$branch?access_token=" + getGithubToken(
  138. project
  139. )
  140. )
  141. .ignoreContentType(true)
  142. .ignoreHttpErrors(true)
  143. .method(Connection.Method.GET)
  144. .execute()
  145. }
  146. if (resp.statusCode() == 404) {
  147. println("Branch Not Found")
  148. return null
  149. }
  150. val info = resp.body().asJson().get("object").asJsonObject.get("url").asString
  151. var parentNode = withContext(Dispatchers.IO) {
  152. Jsoup.connect(info + "?access_token=" + getGithubToken(project)).ignoreContentType(true)
  153. .method(Connection.Method.GET)
  154. .execute().body().asJson().get("tree").asJsonObject.get("url").asString
  155. }
  156. filePath.split("/").forEach { subPath ->
  157. withContext(Dispatchers.IO) {
  158. Jsoup.connect(parentNode + "?access_token=" + getGithubToken(project)).ignoreContentType(true)
  159. .method(Connection.Method.GET).execute().body().asJson().get("tree").asJsonArray
  160. }.forEach list@{
  161. with(it.asJsonObject) {
  162. if (this.get("path").asString == subPath) {
  163. parentNode = this.get("url").asString
  164. return@list
  165. }
  166. }
  167. }
  168. }
  169. check(parentNode.contains("/blobs/"))
  170. return parentNode.substringAfterLast("/")
  171. }
  172. return if (branch == "master") {
  173. try {
  174. getShaSmart(repo, filePath, project)
  175. } catch (e: TargetTooLargeException) {
  176. getShaStupid(repo, filePath, branch, project)
  177. }
  178. } else {
  179. getShaStupid(repo, filePath, branch, project)
  180. }
  181. }
  182. }