@@ -44,7 +44,7 @@ class LogsRepo @Inject constructor(
4444 suspend fun postQuestion (email : String , message : String ): Result <Unit > = withContext(bgDispatcher) {
4545 runCatching {
4646 val logsBase64 = zipLogs(maxEncodedBytes = MAX_SUPPORT_UPLOAD_BASE64_BYTES ).getOrDefault(" " )
47- val logsFileName = createLogsArchiveFileName (SUPPORT_LOGS_ARCHIVE_PREFIX )
47+ val logsArchiveBaseName = currentLogsArchiveName (SUPPORT_LOGS_ARCHIVE_PREFIX ).baseName
4848
4949 chatwootHttpClient.postQuestion(
5050 message = ChatwootMessage (
@@ -53,7 +53,7 @@ class LogsRepo @Inject constructor(
5353 platform = Env .platform,
5454 version = Env .version,
5555 logs = logsBase64,
56- logsFileName = logsFileName ,
56+ logsFileName = logsArchiveBaseName ,
5757 )
5858 )
5959 }.onFailure {
@@ -108,7 +108,7 @@ class LogsRepo @Inject constructor(
108108 val file = withContext(ioDispatcher) {
109109 val tempDir = context.cacheDir.resolve(" logs" ).apply { mkdirs() }
110110
111- val zipFileName = createLogsArchiveFileName()
111+ val zipFileName = currentLogsArchiveName().fileName
112112 val tempFile = File (tempDir, zipFileName)
113113
114114 // Convert base64 back to bytes and write to file
@@ -143,74 +143,12 @@ class LogsRepo @Inject constructor(
143143 allLogs.take(limit)
144144 }
145145
146- return @runCatching createZipBase64(logsToZip, maxEncodedBytes)
146+ return @runCatching createZipBase64(logsToZip, maxEncodedBytes, ::createSupportSnapshot )
147147 }.onFailure {
148148 Logger .error(" Failed to zip logs" , it, context = TAG )
149149 }
150150 }
151151
152- @Suppress(" NestedBlockDepth" )
153- private fun createZipBase64 (logFiles : List <LogFile >, maxEncodedBytes : Int? ): String {
154- val selectedLogFiles = logFiles.toMutableList()
155-
156- while (true ) {
157- val encoded = createZipBytes(selectedLogFiles).toBase64()
158- if (maxEncodedBytes == null || encoded.length <= maxEncodedBytes || selectedLogFiles.isEmpty()) {
159- Logger .info(" Created support logs archive with '${selectedLogFiles.size} ' log file(s)" , context = TAG )
160- return encoded
161- }
162-
163- selectedLogFiles.removeAt(selectedLogFiles.lastIndex)
164- }
165- }
166-
167- @Suppress(" NestedBlockDepth" )
168- private fun createZipBytes (logFiles : List <LogFile >): ByteArray {
169- return ByteArrayOutputStream ().use { byteArrayOut ->
170- ZipOutputStream (byteArrayOut).use { zipOut ->
171- zipOut.putNextEntry(ZipEntry (SUPPORT_SNAPSHOT_FILE_NAME ))
172- zipOut.write(createSupportSnapshot().toByteArray())
173- zipOut.closeEntry()
174-
175- logFiles.forEach { logFile ->
176- if (logFile.file.exists()) {
177- val zipEntry = ZipEntry (" ${logFile.source.name.lowercase()} /${logFile.fileName} " )
178- zipOut.putNextEntry(zipEntry)
179-
180- FileInputStream (logFile.file).use { fileIn ->
181- fileIn.copyTo(zipOut)
182- }
183- zipOut.closeEntry()
184- }
185- }
186- }
187- byteArrayOut.toByteArray()
188- }
189- }
190-
191- private fun File.toLogFile (): LogFile {
192- val match = LOG_FILE_NAME_REGEX .matchEntire(name)
193- val serviceName = match
194- ?.groupValues
195- ?.getOrNull(1 )
196- ?.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
197- ? : LogSource .Unknown .name
198- val timestamp = match?.groupValues?.getOrNull(2 )?.replace(" _" , " " )
199- val part = match?.groupValues?.getOrNull(3 )?.ifBlank { null }
200- val partSuffix = part?.let { " part $it " }.orEmpty()
201- val displayName = if (timestamp != null ) {
202- " $serviceName Log: $timestamp$partSuffix "
203- } else {
204- " $serviceName Log: $name "
205- }
206-
207- return LogFile (
208- displayName = displayName,
209- file = this ,
210- source = getEnumValueOf<LogSource >(serviceName).getOrDefault(LogSource .Unknown ),
211- )
212- }
213-
214152 private fun createSupportSnapshot (): String {
215153 val state = lightningRepo.lightningState.value
216154 val snapshot = SupportSnapshot (
@@ -262,26 +200,85 @@ class LogsRepo @Inject constructor(
262200 return appJson.encodeToString(snapshot)
263201 }
264202
265- private fun createLogsArchiveFileName (prefix : String = LOGS_ARCHIVE_PREFIX ): String {
266- return " ${ prefix} _ ${ currentLogTimestamp()} .zip "
203+ private fun currentLogsArchiveName (prefix : String = LOGS_ARCHIVE_PREFIX ): LogsArchiveName {
204+ return createLogsArchiveName( prefix, currentLogTimestamp())
267205 }
268206
269207 private fun currentLogTimestamp (): String {
270208 return utcDateFormatterOf(DatePattern .LOG_FILE ).format(Date ())
271209 }
210+ }
272211
273- private companion object {
274- const val TAG = " SupportRepo"
275- const val LOGS_ARCHIVE_PREFIX = " bitkit_logs"
276- const val MAX_SUPPORT_UPLOAD_BASE64_BYTES = 900 * 1024
277- const val SUPPORT_LOGS_ARCHIVE_PREFIX = " bitkit_support_logs"
278- const val SUPPORT_SNAPSHOT_FILE_NAME = " support_snapshot.json"
279- val LOG_FILE_NAME_REGEX = Regex (
280- " ^([A-Za-z]+)_(\\ d{4}-\\ d{2}-\\ d{2}_\\ d{2}-\\ d{2}-\\ d{2})(?:\\ .part_(\\ d{3}))?\\ .log$"
281- )
212+ internal fun createZipBase64 (
213+ logFiles : List <LogFile >,
214+ maxEncodedBytes : Int? ,
215+ supportSnapshot : () -> String ,
216+ ): String {
217+ val selectedLogFiles = logFiles.toMutableList()
218+
219+ while (true ) {
220+ val encoded = createZipBytes(selectedLogFiles, supportSnapshot).toBase64()
221+ if (maxEncodedBytes == null || encoded.length <= maxEncodedBytes || selectedLogFiles.isEmpty()) {
222+ Logger .info(" Created support logs archive with '${selectedLogFiles.size} ' log file(s)" , context = TAG )
223+ return encoded
224+ }
225+
226+ selectedLogFiles.removeAt(selectedLogFiles.lastIndex)
282227 }
283228}
284229
230+ internal fun createZipBytes (
231+ logFiles : List <LogFile >,
232+ supportSnapshot : () -> String ,
233+ ): ByteArray {
234+ return ByteArrayOutputStream ().use { byteArrayOut ->
235+ ZipOutputStream (byteArrayOut).use { zipOut ->
236+ zipOut.writeSupportSnapshot(supportSnapshot())
237+ logFiles.filter { it.file.exists() }.forEach { logFile ->
238+ zipOut.writeLogFile(logFile)
239+ }
240+ }
241+ byteArrayOut.toByteArray()
242+ }
243+ }
244+
245+ private fun ZipOutputStream.writeSupportSnapshot (supportSnapshot : String ) {
246+ putNextEntry(ZipEntry (SUPPORT_SNAPSHOT_FILE_NAME ))
247+ write(supportSnapshot.toByteArray())
248+ closeEntry()
249+ }
250+
251+ private fun ZipOutputStream.writeLogFile (logFile : LogFile ) {
252+ putNextEntry(ZipEntry (" ${logFile.source.name.lowercase()} /${logFile.fileName} " ))
253+ FileInputStream (logFile.file).use { fileIn ->
254+ fileIn.copyTo(this )
255+ }
256+ closeEntry()
257+ }
258+
259+ internal fun File.toLogFile (): LogFile {
260+ val match = LOG_FILE_NAME_REGEX .matchEntire(name)
261+ val serviceName = match
262+ ?.groupValues
263+ ?.getOrNull(1 )
264+ ?.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
265+ ? : LogSource .Unknown .name
266+ val timestamp = match?.groupValues?.getOrNull(2 )?.replace(" _" , " " )
267+ val part = match?.groupValues?.getOrNull(3 )?.ifBlank { null }
268+ val partSuffix = part?.let { " part $it " }.orEmpty()
269+ val displayName = if (timestamp != null ) {
270+ " $serviceName Log: $timestamp$partSuffix "
271+ } else {
272+ " $serviceName Log: $name "
273+ }
274+
275+ return LogFile (
276+ displayName = displayName,
277+ file = this ,
278+ source = getEnumValueOf<LogSource >(serviceName).getOrDefault(LogSource .Unknown ),
279+ )
280+ }
281+
285282data class LogFile (
286283 val displayName : String ,
287284 val file : File ,
@@ -290,6 +287,24 @@ data class LogFile(
290287 val fileName: String get() = file.name
291288}
292289
290+ internal data class LogsArchiveName (
291+ val baseName : String ,
292+ ) {
293+ val fileName: String get() = " $baseName$ZIP_EXTENSION "
294+ }
295+
296+ internal fun createLogsArchiveName (prefix : String , timestamp : String ): LogsArchiveName {
297+ return LogsArchiveName (" ${prefix} _$timestamp " .withoutZipExtension())
298+ }
299+
300+ internal fun String.withoutZipExtension (): String {
301+ var name = this
302+ while (name.endsWith(ZIP_EXTENSION , ignoreCase = true )) {
303+ name = name.dropLast(ZIP_EXTENSION .length)
304+ }
305+ return name
306+ }
307+
293308private fun NodeLifecycleState.supportName (): String = when (this ) {
294309 is NodeLifecycleState .Stopped -> " Stopped"
295310 is NodeLifecycleState .Starting -> " Starting"
@@ -348,3 +363,13 @@ private data class SupportBalanceSnapshot(
348363 val lightningBalancesCount : Int ,
349364 val pendingChannelClosureBalancesCount : Int ,
350365)
366+
367+ private const val TAG = " LogsRepo"
368+ private const val LOGS_ARCHIVE_PREFIX = " bitkit_logs"
369+ private const val MAX_SUPPORT_UPLOAD_BASE64_BYTES = 900 * 1024
370+ private const val SUPPORT_LOGS_ARCHIVE_PREFIX = " bitkit_support_logs"
371+ private const val SUPPORT_SNAPSHOT_FILE_NAME = " support_snapshot.json"
372+ private const val ZIP_EXTENSION = " .zip"
373+ private val LOG_FILE_NAME_REGEX = Regex (
374+ " ^([A-Za-z]+)_(\\ d{4}-\\ d{2}-\\ d{2}_\\ d{2}-\\ d{2}-\\ d{2})(?:\\ .part_(\\ d{3}))?\\ .log$"
375+ )
0 commit comments