pepejik

Untitled

Feb 21st, 2025
44
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Kotlin 7.62 KB | None | 0 0
  1. package com.pepej.truncator.indexer
  2.  
  3. import com.pepej.truncator.database.ProcessedLogs
  4. import io.ktor.client.HttpClient
  5. import io.ktor.client.engine.cio.CIO
  6. import io.ktor.client.plugins.HttpTimeout
  7. import io.ktor.client.plugins.auth.Auth
  8. import io.ktor.client.plugins.auth.providers.BasicAuthCredentials
  9. import io.ktor.client.plugins.auth.providers.basic
  10. import io.ktor.client.request.get
  11. import io.ktor.client.statement.bodyAsBytes
  12. import io.ktor.client.statement.bodyAsText
  13. import io.ktor.http.isSuccess
  14. import kotlinx.coroutines.Dispatchers
  15. import kotlinx.coroutines.async
  16. import kotlinx.coroutines.awaitAll
  17. import kotlinx.coroutines.coroutineScope
  18. import kotlinx.coroutines.delay
  19. import kotlinx.coroutines.withContext
  20. import kotlinx.datetime.Clock.System.now
  21. import kotlinx.datetime.LocalDate
  22. import kotlinx.datetime.TimeZone
  23. import kotlinx.datetime.toLocalDateTime
  24. import kotlinx.io.IOException
  25. import okio.buffer
  26. import okio.gzip
  27. import okio.sink
  28. import okio.source
  29. import org.jetbrains.exposed.sql.and
  30. import org.jetbrains.exposed.sql.insert
  31. import org.jetbrains.exposed.sql.selectAll
  32. import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
  33. import org.jsoup.Jsoup
  34. import org.slf4j.LoggerFactory
  35. import java.io.File
  36. import java.net.URL
  37.  
  38. private const val BASE_URL = "https://privatelogs.mcskill.net"
  39. private const val USERNAME = "Truncator"
  40. private const val PASSWORD = "PASSWORD"
  41.  
  42. private val log = LoggerFactory.getLogger("LogsDownloader")
  43.  
  44.  
  45. fun parseFilenameDate(filename: String): LocalDate? {
  46.     val regex = Regex("(\\d{4})[.\\-_](\\d{2})[.\\-_](\\d{2})")
  47.     val match = regex.find(filename) ?: return null
  48.     return try {
  49.         val (yyyy, mm, dd) = match.destructured
  50.         LocalDate(yyyy.toInt(), mm.toInt(), dd.toInt())
  51.     } catch (e: Exception) {
  52.         null
  53.     }
  54. }
  55.  
  56. suspend fun fetchAndIndexFile(
  57.     client: HttpClient,
  58.     fileUrl: String,
  59.     filename: String,
  60.     serverId: Int,
  61.     saveFolder: String = "mclogs",
  62.     force: Boolean = false
  63. ) {
  64.  
  65.     withContext(Dispatchers.IO) {
  66.         val folder = File(saveFolder)
  67.         if (!folder.exists()) folder.mkdirs()
  68.  
  69.         val gzFile = File(folder, filename)
  70.         val baseName = filename.removeSuffix(".gz")
  71.         val logFile = File(folder, baseName)
  72.  
  73.         if (!force && logFile.exists()) {
  74.             processFile(baseName, logFile, filename, serverId)
  75.             return@withContext
  76.         }
  77.  
  78.         val bytes = client.downloadGzWithRetries(fileUrl) ?: run {
  79.             log.error("Failed to download gzip file $fileUrl")
  80.             return@withContext
  81.         }
  82.  
  83.         gzFile.writeBytes(bytes)
  84.  
  85.         try {
  86.             gzFile.source().gzip().use { unzipSource ->
  87.                 val bufferedUnzip = unzipSource.buffer()
  88.                 logFile.sink().buffer().use { out ->
  89.                     bufferedUnzip.readAll(out)
  90.                 }
  91.             }
  92.         } catch (ex: IOException) {
  93.             log.error("Файл $fileUrl не является валидным gzip: ${ex.message}")
  94.             return@withContext
  95.         }
  96.  
  97.  
  98.         gzFile.delete()
  99.  
  100.         processFile(baseName, logFile, filename, serverId)
  101.     }
  102. }
  103.  
  104. private suspend fun processFile(baseName: String, logFile: File, filename: String, serverId: Int) {
  105.     parseFilenameDate(baseName)?.let { date ->
  106.         indexLogFile(logFile, date, serverId)
  107.         newSuspendedTransaction {
  108.             ProcessedLogs.insert {
  109.                 it[ProcessedLogs.filename] = filename
  110.                 it[ProcessedLogs.serverId] = serverId
  111.                 it[processedAt] = now().toLocalDateTime(TimeZone.currentSystemDefault())
  112.             }
  113.         }
  114.     }
  115. }
  116.  
  117. suspend fun HttpClient.downloadGzWithRetries(
  118.     url: String,
  119.     maxAttempts: Int = 3,
  120.     delayBetweenAttemptsMs: Long = 2000
  121. ): ByteArray? {
  122.     repeat(maxAttempts) { attemptIndex ->
  123.         try {
  124.             val response = get(url)
  125.             if (!response.status.isSuccess()) {
  126.                 log.warn("Attempt #${attemptIndex + 1}: Code ${response.status} for $url")
  127.             } else {
  128.                 val bytes = response.bodyAsBytes()
  129.                 if (bytes.size >= 2 && bytes[0] == 0x1F.toByte() && bytes[1] == 0x8B.toByte()) {
  130.                     return bytes
  131.                 } else {
  132.                     log.warn("Attempt #${attemptIndex + 1}: Not GZIP bytes. " +
  133.                         "First bytes=${bytes.take(2)} at $url")
  134.                 }
  135.             }
  136.         } catch (e: Exception) {
  137.             log.warn("Attempt #${attemptIndex + 1} failed: ${e.message}")
  138.         }
  139.         if (attemptIndex < maxAttempts - 1) {
  140.             delay(delayBetweenAttemptsMs)
  141.         }
  142.     }
  143.     log.error("All $maxAttempts attempts to download '$url' have failed or not GZIP.")
  144.     return null
  145. }
  146.  
  147.  
  148. suspend fun updateLogs(
  149.     baseFolder: String = "mclogs",
  150.     force: Boolean = false
  151. ) {
  152.     val client = HttpClient(CIO) {
  153.         install(Auth) {
  154.             basic {
  155.                 credentials {
  156.                     BasicAuthCredentials(USERNAME, PASSWORD)
  157.                 }
  158.             }
  159.         }
  160.         install(HttpTimeout) {
  161.             connectTimeoutMillis = 60_000
  162.             requestTimeoutMillis = 60_000
  163.             socketTimeoutMillis = 60_000
  164.         }
  165.     }
  166.  
  167.     client.use { c ->
  168.         for (server in Servers.entries) {
  169.             val serverId = server.id
  170.             val serverName = server.name.lowercase()
  171.             val serverFolder = "$baseFolder/$serverName"
  172.             val serverUrl = "$BASE_URL/?serverid=$serverId"
  173.  
  174.             log.info("Обновление логов для сервера $serverName ($serverId)")
  175.  
  176.             try {
  177.                 val html = c.get(serverUrl).bodyAsText()
  178.                 val doc = Jsoup.parse(html)
  179.  
  180.                 val links = doc.select("a")
  181.                     .toList()
  182.                     .filter { it.attr("href").contains(".log.gz") }
  183.  
  184.                 val tasks = links
  185.                     .filterNot { link ->
  186.                         val text = link.text().trim()
  187.                         val fileName = if (text.isNotEmpty()) File(text).name else return@filterNot false
  188.                         checkFileAlreadyProcessed(serverId, fileName)
  189.                     }
  190.                     .map { link ->
  191.                         val href = link.attr("href")
  192.                         val fullUrl = URL(URL(serverUrl), href).toString()
  193.                         val text = link.text().trim()
  194.                         val name = if (text.isNotEmpty()) File(text).name else File(fullUrl).name
  195.                         fullUrl to name
  196.                     }
  197.  
  198.                 log.info("Будет обработано файлов: ${tasks.size} для $serverName")
  199.  
  200.                 coroutineScope {
  201.                     tasks.map { (url, fname) ->
  202.                         async {
  203.                             fetchAndIndexFile(c, url, fname, serverId, serverFolder, force)
  204.                         }
  205.                     }.awaitAll()
  206.                 }
  207.  
  208.                 log.info("Готово: скачаны и проиндексированы ${tasks.size} лог-файлов для $serverName.")
  209.  
  210.             } catch (e: Exception) {
  211.                 log.error("Ошибка загрузки логов для сервера $serverName: ${e.message}", e)
  212.             }
  213.         }
  214.     }
  215. }
  216.  
  217. suspend fun checkFileAlreadyProcessed(serverId: Int, fileName: String): Boolean {
  218.     return newSuspendedTransaction {
  219.         ProcessedLogs.selectAll()
  220.             .where { (ProcessedLogs.filename eq fileName) and (ProcessedLogs.serverId eq serverId) }
  221.             .any()
  222.     }
  223. }
  224.  
Add Comment
Please, Sign In to add comment