From 7d58bf681a2afdad54ce9b81c7eae19501d12565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Ladst=C3=A4tter?= Date: Mon, 1 Jan 2024 10:26:33 +0100 Subject: [PATCH] #182: adds support for handling zip files --- .../main/scala/app/logorrr/LogoRRRApp.scala | 1 + .../app/logorrr/conf/LogoRRRGlobals.scala | 25 ++- .../scala/app/logorrr/conf/Settings.scala | 11 +- .../scala/app/logorrr/conf/SettingsIO.scala | 2 +- .../app/logorrr/conf/mut/MutSettings.scala | 33 +--- .../main/scala/app/logorrr/io/FEncoding.scala | 11 +- .../scala/app/logorrr/io/FileManager.scala | 55 ------- .../main/scala/app/logorrr/io/IoManager.scala | 151 ++++++++++++++++++ .../app/logorrr/io/LogEntryFileReader.scala | 28 ---- .../app/logorrr/model/LogFileSettings.scala | 35 +--- .../logorrr/views/logfiletab/LogFileTab.scala | 59 +++++-- .../app/logorrr/views/main/LogoRRRMain.scala | 52 ++++-- .../app/logorrr/views/main/LogoRRRStage.scala | 10 +- .../app/logorrr/views/main/MainTabPane.scala | 41 ++++- .../app/logorrr/views/search/OpsToolBar.scala | 2 +- .../logorrr/views/search/TimerButton.scala | 6 +- .../logorrr/io/ziputil-bit-more-complex.zip | Bin 0 -> 1241 bytes .../app/logorrr/io/ziputil-simple.zip | Bin 0 -> 345 bytes .../test/scala/app/logorrr/Issue139Spec.scala | 5 +- .../app/logorrr/LogEntryFileReaderSpec.scala | 4 +- .../conf/mut/LogFileSettingsSpec.scala | 4 +- .../logorrr/conf/mut/MutSettingsSpec.scala | 11 +- .../scala/app/logorrr/io/IoManagerSpec.scala | 21 +++ .../views/block/ChunkListTestApp.scala | 4 +- .../main/scala/app/logorrr/io/FileId.scala | 20 ++- core/src/main/scala/app/logorrr/io/Fs.scala | 4 +- .../scala/app/logorrr/io/FileIdSpec.scala | 23 +++ 27 files changed, 409 insertions(+), 209 deletions(-) delete mode 100644 app/src/main/scala/app/logorrr/io/FileManager.scala create mode 100644 app/src/main/scala/app/logorrr/io/IoManager.scala delete mode 100644 app/src/main/scala/app/logorrr/io/LogEntryFileReader.scala create mode 100644 app/src/test/resources/app/logorrr/io/ziputil-bit-more-complex.zip create mode 100644 app/src/test/resources/app/logorrr/io/ziputil-simple.zip create mode 100644 app/src/test/scala/app/logorrr/io/IoManagerSpec.scala create mode 100644 core/src/test/scala/app/logorrr/io/FileIdSpec.scala diff --git a/app/src/main/scala/app/logorrr/LogoRRRApp.scala b/app/src/main/scala/app/logorrr/LogoRRRApp.scala index 39f0a2f8..89110903 100644 --- a/app/src/main/scala/app/logorrr/LogoRRRApp.scala +++ b/app/src/main/scala/app/logorrr/LogoRRRApp.scala @@ -34,6 +34,7 @@ object LogoRRRApp { LogoRRRGlobals.set(settings, hostServices) val logoRRRMain = new LogoRRRMain(JfxUtils.closeStage(stage)) LogoRRRStage.init(stage, logoRRRMain) + logoRRRMain.initLogFilesFromConfig() LogoRRRStage.show(stage, logoRRRMain) } } diff --git a/app/src/main/scala/app/logorrr/conf/LogoRRRGlobals.scala b/app/src/main/scala/app/logorrr/conf/LogoRRRGlobals.scala index a271f70a..de11a6f6 100644 --- a/app/src/main/scala/app/logorrr/conf/LogoRRRGlobals.scala +++ b/app/src/main/scala/app/logorrr/conf/LogoRRRGlobals.scala @@ -20,7 +20,7 @@ import java.nio.file.Path */ object LogoRRRGlobals extends CanLog { - val mutSettings = new MutSettings + private val mutSettings = new MutSettings private val hostServicesProperty = new SimpleObjectProperty[HostServices]() @@ -60,16 +60,18 @@ object LogoRRRGlobals extends CanLog { def getHostServices: HostServices = hostServicesProperty.get() def set(settings: Settings, hostServices: HostServices): Unit = { - mutSettings.set(settings) + mutSettings.setStageSettings(settings.stageSettings) + mutSettings.setLogFileSettings(settings.fileSettings) + mutSettings.setSomeActive(settings.someActive) + mutSettings.setSomeLastUsedDirectory(settings.someLastUsedDirectory) + setHostServices(hostServices) } /** a case class representing current setting state */ def getSettings: Settings = mutSettings.petrify() - def setSomeActiveLogFile(sActive: Option[FileId]): Unit = { - mutSettings.setSomeActive(sActive) - } + def setSomeActiveLogFile(sActive: Option[FileId]): Unit = mutSettings.setSomeActive(sActive) def getSomeActiveLogFile: Option[FileId] = mutSettings.getSomeActiveLogFile @@ -85,15 +87,24 @@ object LogoRRRGlobals extends CanLog { }) if (OsUtil.enableSecurityBookmarks) { - OsxBridge.releasePath(fileId.absolutePathAsString) + if (fileId.isZip) { + // only release path if no other file is opened anymore for this particular zip file + val zipInQuestion = fileId.extractZipFileId + if (!LogoRRRGlobals.getOrderedLogFileSettings.map(_.fileId.extractZipFileId).contains(zipInQuestion)) { + OsxBridge.releasePath(fileId.extractZipFileId.absolutePathAsString) + } + } else { + OsxBridge.releasePath(fileId.absolutePathAsString) + } } }, s"Removed file $fileId ...") def clearLogFileSettings(): Unit = mutSettings.clearLogFileSettings() + def registerSettings(fs: LogFileSettings): Unit = mutSettings.putMutLogFileSetting(MutLogFileSettings(fs)) + def getLogFileSettings(fileId: FileId): MutLogFileSettings = mutSettings.getMutLogFileSetting(fileId) - def registerSettings(fs: LogFileSettings): Unit = mutSettings.putMutLogFileSetting(MutLogFileSettings(fs)) } diff --git a/app/src/main/scala/app/logorrr/conf/Settings.scala b/app/src/main/scala/app/logorrr/conf/Settings.scala index 9ca23a06..ea0db123 100644 --- a/app/src/main/scala/app/logorrr/conf/Settings.scala +++ b/app/src/main/scala/app/logorrr/conf/Settings.scala @@ -1,6 +1,6 @@ package app.logorrr.conf -import app.logorrr.io.FileId +import app.logorrr.io.{FileId, IoManager} import app.logorrr.model.LogFileSettings import pureconfig.generic.semiauto.{deriveReader, deriveWriter} import pureconfig.{ConfigReader, ConfigWriter} @@ -37,7 +37,14 @@ case class Settings(stageSettings: StageSettings copy(stageSettings, fileSettings + (logFileSetting.fileId.value -> logFileSetting)) } - def filterWithValidPaths(): Settings = copy(fileSettings = fileSettings.filter { case (_, d) => d.isPathValid }) + def filterWithValidPaths(): Settings = copy(fileSettings = fileSettings.filter { case (_, d) => + // if entry is part of a zip file, test the path of the zip file + if (d.fileId.isZip) { + IoManager.isPathValid(d.fileId.extractZipFileId.asPath) + } else { + IoManager.isPathValid(d.path) + } + }) } diff --git a/app/src/main/scala/app/logorrr/conf/SettingsIO.scala b/app/src/main/scala/app/logorrr/conf/SettingsIO.scala index dff4a41c..26a1b115 100644 --- a/app/src/main/scala/app/logorrr/conf/SettingsIO.scala +++ b/app/src/main/scala/app/logorrr/conf/SettingsIO.scala @@ -16,7 +16,7 @@ object SettingsIO extends CanLog { val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) /** read settings from default place and filter all paths which don't exist anymore */ - def fromFile(settingsFilePath : Path): Settings = { + def fromFile(settingsFilePath: Path): Settings = { Try(ConfigSource.file(settingsFilePath).loadOrThrow[Settings].filterWithValidPaths()) match { case Failure(_) => logWarn(s"Could not load $settingsFilePath, using default settings ...") diff --git a/app/src/main/scala/app/logorrr/conf/mut/MutSettings.scala b/app/src/main/scala/app/logorrr/conf/mut/MutSettings.scala index d1c09116..41b13907 100644 --- a/app/src/main/scala/app/logorrr/conf/mut/MutSettings.scala +++ b/app/src/main/scala/app/logorrr/conf/mut/MutSettings.scala @@ -11,31 +11,13 @@ import java.nio.file.Path import java.util import scala.jdk.CollectionConverters._ -// This avoids passing around references to settings in all classes. -// This approach is some sort of experiment and the current state of my knowledge to cope with -// this problem when doing this sort of stuff in JavaFX. Happy to get input on how to solve the global -// configuration problem any better. -object MutSettings { - - def apply(settings: Settings): MutSettings = { - val s = new MutSettings - s.setStageSettings(settings.stageSettings) - s.setLogFileSettings(settings.fileSettings) - s.setSomeActive(settings.someActive) - s - } - -} - class MutSettings { /** remembers last opened directory for the next execution */ val lastUsedDirectoryProperty = new SimpleObjectProperty[Option[Path]](None) - def getSomeLastUsedDirectory: Option[Path] = { - lastUsedDirectoryProperty.get() - } + def getSomeLastUsedDirectory: Option[Path] = lastUsedDirectoryProperty.get() def setSomeLastUsedDirectory(someDirectory: Option[Path]): Unit = { lastUsedDirectoryProperty.set(someDirectory) @@ -56,14 +38,7 @@ class MutSettings { mutLogFileSettingsMapProperty.put(mutLogFileSettings.getFileId, mutLogFileSettings) } - def removeLogFileSetting(pathAsString: FileId): Unit = mutLogFileSettingsMapProperty.remove(pathAsString) - - def set(settings: Settings): Unit = { - setStageSettings(settings.stageSettings) - setLogFileSettings(settings.fileSettings) - setSomeActive(settings.someActive) - setSomeLastUsedDirectory(settings.someLastUsedDirectory) - } + def removeLogFileSetting(fileId: FileId): Unit = mutLogFileSettingsMapProperty.remove(fileId) def setSomeActive(path: Option[FileId]): Unit = someActiveLogProperty.set(path) @@ -91,7 +66,6 @@ class MutSettings { } def clearLogFileSettings(): Unit = { - mutLogFileSettingsMapProperty.clear() setSomeActive(None) } @@ -119,7 +93,8 @@ class MutSettings { def getStageWidth: Int = mutStageSettings.getWidth() def getOrderedLogFileSettings: Seq[LogFileSettings] = { - mutLogFileSettingsMapProperty.get().values.asScala.toSeq.sortWith((lt, gt) => lt.getFirstOpened < gt.getFirstOpened).map(_.petrify()) + val seq = mutLogFileSettingsMapProperty.get().values.asScala.toSeq + seq.sortWith((lt, gt) => lt.getFirstOpened < gt.getFirstOpened).map(_.petrify()) } diff --git a/app/src/main/scala/app/logorrr/io/FEncoding.scala b/app/src/main/scala/app/logorrr/io/FEncoding.scala index a67e3e07..e3ea57e0 100644 --- a/app/src/main/scala/app/logorrr/io/FEncoding.scala +++ b/app/src/main/scala/app/logorrr/io/FEncoding.scala @@ -1,11 +1,20 @@ package app.logorrr.io +import java.io.{ByteArrayInputStream, InputStream} import java.nio.file.{Files, Path} object FEncoding { def apply(path: Path): FEncoding = { val is = Files.newInputStream(path) + apply(is) + } + + def apply(asBytes: Array[Byte]): FEncoding = { + apply(new ByteArrayInputStream(asBytes)) + } + + private def apply(is: InputStream): FEncoding = { try { val bom = Array.fill[Byte](3)(0) is.read(bom) @@ -24,7 +33,7 @@ object FEncoding { } } -class FEncoding(val asString: String) +abstract class FEncoding(val asString: String) extends Product case object UTF8 extends FEncoding("UTF-8") diff --git a/app/src/main/scala/app/logorrr/io/FileManager.scala b/app/src/main/scala/app/logorrr/io/FileManager.scala deleted file mode 100644 index f5ba2534..00000000 --- a/app/src/main/scala/app/logorrr/io/FileManager.scala +++ /dev/null @@ -1,55 +0,0 @@ -package app.logorrr.io - -import app.logorrr.OsxBridge -import app.logorrr.model.LogEntry -import app.logorrr.util.{CanLog, OsUtil} -import javafx.collections.{FXCollections, ObservableList} - -import java.io.{BufferedReader, FileInputStream, InputStreamReader} -import java.nio.file.{Files, Path} - - -object FileManager extends CanLog { - - private def openFileWithDetectedEncoding(path: Path): BufferedReader = { - val encoding = FEncoding(path) - if (encoding == Unknown) { - new BufferedReader(new InputStreamReader(new FileInputStream(path.toFile), UTF8.asString)) - } else { - new BufferedReader(new InputStreamReader(new FileInputStream(path.toFile), encoding.asString)) - } - } - - def fromPath(path: Path): Seq[String] = { - require(Files.exists(path)) - val reader = openFileWithDetectedEncoding(path) - try { - (for (line <- Iterator.continually(reader.readLine()).takeWhile(_ != null)) yield line).toSeq - } finally { - reader.close() - } - } - - def fromPathUsingSecurityBookmarks(logFile: Path): Seq[String] = { - if (OsUtil.enableSecurityBookmarks) { - logTrace(s"Registering security bookmark for '${logFile.toAbsolutePath.toString}'") - OsxBridge.registerPath(logFile.toAbsolutePath.toString) - } - val lines = FileManager.fromPath(logFile) - if (lines.isEmpty) { - logWarn(s"${logFile.toAbsolutePath.toString} was empty.") - } - lines - } - - def from(logFile: Path): ObservableList[LogEntry] = { - var lineNumber: Int = 0 - val arraylist = new java.util.ArrayList[LogEntry]() - fromPathUsingSecurityBookmarks(logFile).map(l => { - lineNumber = lineNumber + 1 - arraylist.add(LogEntry(lineNumber, l, None)) - }) - FXCollections.observableList(arraylist) - } - -} diff --git a/app/src/main/scala/app/logorrr/io/IoManager.scala b/app/src/main/scala/app/logorrr/io/IoManager.scala new file mode 100644 index 00000000..e948b61d --- /dev/null +++ b/app/src/main/scala/app/logorrr/io/IoManager.scala @@ -0,0 +1,151 @@ +package app.logorrr.io + +import app.logorrr.OsxBridge +import app.logorrr.model.{LogEntry, LogEntryInstantFormat} +import app.logorrr.util.{CanLog, OsUtil} +import javafx.collections.{FXCollections, ObservableList} + +import java.io.{BufferedReader, ByteArrayInputStream, FileInputStream, IOException, InputStreamReader} +import java.nio.file.{Files, Path} +import java.util +import java.util.zip.{ZipEntry, ZipInputStream} +import scala.util.{Failure, Success, Try} + + +object IoManager extends CanLog { + + private def mkReader(path: Path): BufferedReader = { + val encoding = FEncoding(path) + if (encoding == Unknown) { + new BufferedReader(new InputStreamReader(new FileInputStream(path.toFile), UTF8.asString)) + } else { + new BufferedReader(new InputStreamReader(new FileInputStream(path.toFile), encoding.asString)) + } + } + + private def mkReader(bytes: Array[Byte]): BufferedReader = { + val encoding = FEncoding(bytes) + if (encoding == Unknown) { + new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes), UTF8.asString)) + } else { + new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes), encoding.asString)) + } + } + + def fromPath(path: Path): Seq[String] = { + require(Files.exists(path)) + toSeq(mkReader(path)) + } + + private def toSeq(reader: BufferedReader): Seq[String] = { + try { + (for (line <- Iterator.continually(reader.readLine()).takeWhile(_ != null)) yield line).toSeq + } finally { + reader.close() + } + } + + def fromPathUsingSecurityBookmarks(logFile: Path): Seq[String] = { + registerPath(logFile) + val lines = IoManager.fromPath(logFile) + if (lines.isEmpty) { + logWarn(s"${logFile.toAbsolutePath.toString} was empty.") + } + lines + } + + def from(asBytes: Array[Byte]): ObservableList[LogEntry] = { + var lineNumber: Int = 0 + val arraylist = new java.util.ArrayList[LogEntry]() + toSeq(mkReader(asBytes)).map(l => { + lineNumber = lineNumber + 1 + arraylist.add(LogEntry(lineNumber, l, None)) + }) + FXCollections.observableList(arraylist) + + } + + def from(logFile: Path): ObservableList[LogEntry] = { + var lineNumber: Int = 0 + val arraylist = new java.util.ArrayList[LogEntry]() + fromPathUsingSecurityBookmarks(logFile).map(l => { + lineNumber = lineNumber + 1 + arraylist.add(LogEntry(lineNumber, l, None)) + }) + FXCollections.observableList(arraylist) + } + + def from(logFile: Path, logEntryTimeFormat: LogEntryInstantFormat): ObservableList[LogEntry] = { + var lineNumber: Int = 0 + val arraylist = new util.ArrayList[LogEntry]() + fromPathUsingSecurityBookmarks(logFile).map(l => { + lineNumber = lineNumber + 1 + arraylist.add(LogEntry(lineNumber, l, LogEntryInstantFormat.parseInstant(l, logEntryTimeFormat))) + }) + FXCollections.observableList(arraylist) + } + + def readEntries(path : Path, someLogEntryInstantFormat: Option[LogEntryInstantFormat]): ObservableList[LogEntry] = { + if (isPathValid(path)) { + Try(someLogEntryInstantFormat match { + case None => IoManager.from(path) + case Some(instantFormat) => from(path, instantFormat) + }) match { + case Success(logEntries) => logEntries + case Failure(ex) => + val msg = s"Could not load file ${path.toAbsolutePath.toString}" + logException(msg, ex) + FXCollections.observableArrayList() + } + } else { + logWarn(s"Could not read ${path.toAbsolutePath.toString} - does it exist?") + FXCollections.observableArrayList() + } + } + + def isPathValid(path : Path): Boolean = + if (OsUtil.isMac) { + Files.exists(path) + } else { + // without security bookmarks initialized, this returns false on mac + Files.isReadable(path) && Files.isRegularFile(path) + } + + /** + * Given a zip file, extract its contents recursively and return all files contained in a map. the key of this + * map represents the file name in the zip file, and the value is the contents of the file as string. + * + * @param zipFile the zip file + * @return + */ + def unzip(zipFile: Path, filters: Set[FileId]): Map[FileId, ObservableList[LogEntry]] = { + registerPath(zipFile) + var resultMap: Map[FileId, ObservableList[LogEntry]] = Map() + try { + val zipIn = new ZipInputStream(Files.newInputStream(zipFile)) + var entry: ZipEntry = zipIn.getNextEntry + while (entry != null) { + if (!entry.isDirectory) { + // by convention reference a file which is contained in a zip file like that + val id = FileId(zipFile.toAbsolutePath.toString + "@" + entry.getName) + if (filters.isEmpty || filters.contains(id)) { // do not read all entries if there is no need to do it + resultMap += (id -> IoManager.from(zipIn.readAllBytes())) + } + } + zipIn.closeEntry() + entry = zipIn.getNextEntry + } + zipIn.close() + } catch { + case e: IOException => logException("I/O error during unzip", e) + } + resultMap + } + + private def registerPath(zipFile: Path): Unit = { + if (OsUtil.enableSecurityBookmarks) { + logTrace(s"Registering security bookmark for '${zipFile.toAbsolutePath.toString}'") + OsxBridge.registerPath(zipFile.toAbsolutePath.toString) + } + } +} diff --git a/app/src/main/scala/app/logorrr/io/LogEntryFileReader.scala b/app/src/main/scala/app/logorrr/io/LogEntryFileReader.scala deleted file mode 100644 index d1343dae..00000000 --- a/app/src/main/scala/app/logorrr/io/LogEntryFileReader.scala +++ /dev/null @@ -1,28 +0,0 @@ -package app.logorrr.io - -import app.logorrr.model.{LogEntry, LogEntryInstantFormat} -import javafx.collections.{FXCollections, ObservableList} - -import java.nio.file.Path -import java.util - -/** - * Read a log file and parse time information from it - */ -object LogEntryFileReader { - - def from(logFile: Path, logEntryTimeFormat: LogEntryInstantFormat): ObservableList[LogEntry] = { - var lineNumber: Int = 0 - val arraylist = new util.ArrayList[LogEntry]() - FileManager.fromPathUsingSecurityBookmarks(logFile).map(l => { - lineNumber = lineNumber + 1 - arraylist.add(LogEntry(lineNumber, l, LogEntryInstantFormat.parseInstant(l, logEntryTimeFormat))) - }) - FXCollections.observableList(arraylist) - } - -} - - - - diff --git a/app/src/main/scala/app/logorrr/model/LogFileSettings.scala b/app/src/main/scala/app/logorrr/model/LogFileSettings.scala index c9c71560..5e0ba57a 100644 --- a/app/src/main/scala/app/logorrr/model/LogFileSettings.scala +++ b/app/src/main/scala/app/logorrr/model/LogFileSettings.scala @@ -1,17 +1,15 @@ package app.logorrr.model import app.logorrr.conf.BlockSettings -import app.logorrr.io.{FileId, FileManager, LogEntryFileReader} -import app.logorrr.util.{CanLog, OsUtil} +import app.logorrr.io.FileId +import app.logorrr.util.CanLog import app.logorrr.views.search.Filter -import javafx.collections.{FXCollections, ObservableList} import javafx.scene.paint.Color import pureconfig.generic.semiauto.{deriveReader, deriveWriter} import pureconfig.{ConfigReader, ConfigWriter} -import java.nio.file.{Files, Path, Paths} +import java.nio.file.{Path, Paths} import java.time.Instant -import scala.util.{Failure, Success, Try} object LogFileSettings { @@ -67,31 +65,4 @@ case class LogFileSettings(fileId: FileId val path: Path = Paths.get(fileId.value).toAbsolutePath - val isPathValid: Boolean = - if (OsUtil.isMac) { - Files.exists(path) - } else { - // without security bookmarks initialized, this returns false on mac - Files.isReadable(path) && Files.isRegularFile(path) - } - - def readEntries(): ObservableList[LogEntry] = { - if (isPathValid) { - Try(someLogEntryInstantFormat match { - case None => FileManager.from(path) - case Some(instantFormat) => LogEntryFileReader.from(path, instantFormat) - }) match { - case Success(logEntries) => - logEntries - case Failure(ex) => - val msg = s"Could not load file $fileId" - logException(msg, ex) - FXCollections.observableArrayList() - } - } else { - logWarn(s"Could not read $fileId - does it exist?") - FXCollections.observableArrayList() - } - } - } \ No newline at end of file diff --git a/app/src/main/scala/app/logorrr/views/logfiletab/LogFileTab.scala b/app/src/main/scala/app/logorrr/views/logfiletab/LogFileTab.scala index 7ac7aae7..afd6025d 100644 --- a/app/src/main/scala/app/logorrr/views/logfiletab/LogFileTab.scala +++ b/app/src/main/scala/app/logorrr/views/logfiletab/LogFileTab.scala @@ -21,19 +21,29 @@ import scala.concurrent.Future object LogFileTab { + /** background for file log tabs */ private val BackgroundStyle: String = - """ - |-fx-background-color: WHITE; - |-fx-border-width: 1px 1px 1px 0px; - |-fx-border-color: LIGHTGREY; - |""".stripMargin + """|-fx-background-color: white; + |-fx-border-width: 1px 1px 1px 0px; + |-fx-border-color: lightgrey""".stripMargin + /** background selected for file log tabs */ private val BackgroundSelectedStyle: String = - """ - |-fx-background-color: floralwhite; - |-fx-border-width: 1px 1px 1px 0px; - |-fx-border-color: LIGHTGREY; - |""".stripMargin + """|-fx-background-color: floralwhite; + |-fx-border-width: 1px 1px 1px 0px; + |-fx-border-color: lightgrey""".stripMargin + + /** background for zipfile log tabs */ + private val ZipBackgroundStyle: String = + """|-fx-background-color: white; + |-fx-border-width: 1px 1px 1px 0px; + |-fx-border-color: lightgrey""".stripMargin + + /** background selected for file log tabs */ + private val ZipBackgroundSelectedStyle: String = + """|-fx-background-color: floralwhite; + |-fx-border-width: 1px 1px 1px 0px; + |-fx-border-color: lightgrey""".stripMargin def apply(mutLogFileSettings: MutLogFileSettings , entries: ObservableList[LogEntry]): LogFileTab = { @@ -58,6 +68,12 @@ class LogFileTab(val fileId: FileId with TimerCode with CanLog { + if (fileId.isZip) { + setStyle(LogFileTab.ZipBackgroundStyle) + } else { + setStyle(LogFileTab.BackgroundStyle) + } + assert(fileId == mutLogFileSettings.getFileId) private lazy val logTailer = LogTailer(fileId, entries) @@ -97,13 +113,22 @@ class LogFileTab(val fileId: FileId private val selectedListener = JfxUtils.onNew[lang.Boolean](b => { if (b) { - setStyle(LogFileTab.BackgroundSelectedStyle) + if (fileId.isZip) { + setStyle(LogFileTab.ZipBackgroundSelectedStyle) + } else { + setStyle(LogFileTab.BackgroundSelectedStyle) + } + /* change active text field depending on visible tab */ LogoRRRAccelerators.setActiveSearchTextField(logFileTabContent.opsToolBar.searchTextField) LogoRRRAccelerators.setActiveRegexToggleButton(logFileTabContent.opsToolBar.regexToggleButton) recalculateChunkListViewAndScrollToActiveElement() } else { - setStyle(LogFileTab.BackgroundStyle) + if (fileId.isZip) { + setStyle(LogFileTab.ZipBackgroundStyle) + } else { + setStyle(LogFileTab.BackgroundStyle) + } } }) @@ -142,7 +167,7 @@ class LogFileTab(val fileId: FileId case java.lang.Boolean.FALSE => setContextMenu(null) }) - }, s"Init '$fileId'") + }, s"Init '${fileId.value}'") def initContextMenu(): Unit = setContextMenu(mkContextMenu()) @@ -191,7 +216,13 @@ class LogFileTab(val fileId: FileId mutLogFileSettings.filtersProperty.addListener(filterChangeListener) } - private def initBindings(): Unit = textProperty.bind(Bindings.concat(fileId.fileName)) + private def initBindings(): Unit = { + if (fileId.isZip) { + textProperty.bind(Bindings.concat(fileId.zipEntryPath)) + } else { + textProperty.bind(Bindings.concat(fileId.fileName)) + } + } private def removeListeners(): Unit = { diff --git a/app/src/main/scala/app/logorrr/views/main/LogoRRRMain.scala b/app/src/main/scala/app/logorrr/views/main/LogoRRRMain.scala index 19c41ae2..63b1cf64 100644 --- a/app/src/main/scala/app/logorrr/views/main/LogoRRRMain.scala +++ b/app/src/main/scala/app/logorrr/views/main/LogoRRRMain.scala @@ -1,7 +1,7 @@ package app.logorrr.views.main import app.logorrr.conf.LogoRRRGlobals -import app.logorrr.io.FileId +import app.logorrr.io.{FileId, IoManager} import app.logorrr.model.LogFileSettings import app.logorrr.util.CanLog import app.logorrr.views.logfiletab.LogFileTab @@ -23,41 +23,71 @@ class LogoRRRMain(closeStage: => Unit) extends BorderPane with CanLog { def init(): Unit = { setTop(bar) setCenter(mainTabPane) + mainTabPane.init() + } + + def initLogFilesFromConfig(): Unit = { val entries = LogoRRRGlobals.getOrderedLogFileSettings if (entries.nonEmpty) { loadLogFiles(LogoRRRGlobals.getOrderedLogFileSettings) } else { logTrace("No log files loaded.") } - mainTabPane.init() } private def loadLogFiles(settings: Seq[LogFileSettings]): Unit = { - val futures: Future[Seq[LogFileTab]] = Future.sequence { - settings.map(lfs => Future { + val (zipSettings, fileSettings) = settings.partition(p => p.fileId.isZip) + val zipSettingsMap: Map[FileId, LogFileSettings] = zipSettings.map(s => s.fileId -> s).toMap + // zips is a map which contains fileIds as keys which have to be loaded, and as values their corresponding + // settings. this is necessary as not to lose settings from previous runs + val zips: Map[FileId, Seq[FileId]] = FileId.reduceZipFiles(zipSettingsMap.keys.toSeq) + + val futures: Future[Seq[Option[LogFileTab]]] = Future.sequence { + + // load zip files also in parallel + val zipFutures: Seq[Future[Option[LogFileTab]]] = + zips.keys.toSeq.flatMap(f => { + timeR({ + IoManager.unzip(f.asPath, zips(f).toSet).map { + // only if settings contains given fileId - user could have removed it by closing the tab - load this file + case (fileId, entries) => + Future { + if (zipSettingsMap.contains(fileId)) { + Option(mainTabPane.addEntriesFromZip(zipSettingsMap(fileId), entries)) + } else None + } + } + }, s"Loaded zip file '${f.absolutePathAsString}'.") + }) + + val fileBasedSettings: Seq[Future[Option[LogFileTab]]] = fileSettings.map(lfs => Future { timeR({ - val entries = lfs.readEntries() + val entries = IoManager.readEntries(lfs.path, lfs.someLogEntryInstantFormat) val tab = LogFileTab(LogoRRRGlobals.getLogFileSettings(lfs.fileId), entries) mainTabPane.addLogFileTab(tab) - tab - }, s"Loaded '${lfs.fileId}'") + Option(tab) + }, s"Loaded '${lfs.fileId.absolutePathAsString}' from filesystem ...") }) + + zipFutures ++ fileBasedSettings } - val logFileTabs: Seq[LogFileTab] = Await.result(futures, Duration.Inf) + val logFileTabs = Await.result(futures, Duration.Inf).flatten logTrace("Loaded " + logFileTabs.size + " files ... ") - // only after loading all files we initialize the 'add' listener // otherwise we would overwrite the active log everytime mainTabPane.initSelectionListener() } + + def contains(fileId: FileId): Boolean = mainTabPane.contains(fileId) + /** called when 'Open File' is selected. */ def openLogFile(path: Path): Unit = { val fileId = FileId(path) - if (!mainTabPane.contains(fileId)) { + if (!contains(fileId)) { mainTabPane.addLogFile(path) } else { mainTabPane.selectLog(fileId).recalculateChunkListViewAndScrollToActiveElement() @@ -71,7 +101,7 @@ class LogoRRRMain(closeStage: => Unit) extends BorderPane with CanLog { LogoRRRGlobals.clearLogFileSettings() } - def selectLog(pathAsString: FileId): LogFileTab = mainTabPane.selectLog(pathAsString) + def selectLog(fileId: FileId): LogFileTab = mainTabPane.selectLog(fileId) def selectLastLogFile(): Unit = mainTabPane.selectLastLogFile() diff --git a/app/src/main/scala/app/logorrr/views/main/LogoRRRStage.scala b/app/src/main/scala/app/logorrr/views/main/LogoRRRStage.scala index 0d90ed12..2bdd06ae 100644 --- a/app/src/main/scala/app/logorrr/views/main/LogoRRRStage.scala +++ b/app/src/main/scala/app/logorrr/views/main/LogoRRRStage.scala @@ -55,9 +55,13 @@ object LogoRRRStage extends CanLog { def show(stage: Stage, logorrrMain: LogoRRRMain): Unit = { stage.show() LogoRRRGlobals.getSomeActiveLogFile match { - case Some(pathAsString) => - val tab = logorrrMain.selectLog(pathAsString) - tab.recalculateChunkListViewAndScrollToActiveElement() + case Some(fileId) => + if (logorrrMain.contains(fileId)) { + val tab = logorrrMain.selectLog(fileId) + tab.recalculateChunkListViewAndScrollToActiveElement() + } else { + logWarn(s"Not found: '${fileId.absolutePathAsString}'") + } case None => logorrrMain.selectLastLogFile() } diff --git a/app/src/main/scala/app/logorrr/views/main/MainTabPane.scala b/app/src/main/scala/app/logorrr/views/main/MainTabPane.scala index 5697cb46..76fd18b3 100644 --- a/app/src/main/scala/app/logorrr/views/main/MainTabPane.scala +++ b/app/src/main/scala/app/logorrr/views/main/MainTabPane.scala @@ -1,11 +1,12 @@ package app.logorrr.views.main import app.logorrr.conf.LogoRRRGlobals -import app.logorrr.io.FileId -import app.logorrr.model.LogFileSettings +import app.logorrr.io.{FileId, IoManager} +import app.logorrr.model.{LogEntry, LogFileSettings} import app.logorrr.util.{CanLog, JfxUtils} import app.logorrr.views.logfiletab.LogFileTab import javafx.beans.value.ChangeListener +import javafx.collections.ObservableList import javafx.scene.control.{Tab, TabPane} import javafx.scene.input.{DragEvent, TransferMode} @@ -32,7 +33,7 @@ class MainTabPane extends TabPane with CanLog { val selectedTabListener: ChangeListener[Tab] = JfxUtils.onNew { case logFileTab: LogFileTab => - logTrace(s"Selected: '${logFileTab.fileId}'") + logTrace(s"Selected: '${logFileTab.fileId.value}'") LogoRRRGlobals.setSomeActiveLogFile(Option(logFileTab.fileId)) // to set 'selected' property in Tab and to trigger repaint correctly (see issue #9) getSelectionModel.select(logFileTab) @@ -52,12 +53,32 @@ class MainTabPane extends TabPane with CanLog { for (f <- event.getDragboard.getFiles.asScala) { val path = f.toPath if (Files.isDirectory(path)) { - Files.list(path).filter((p: Path) => Files.isRegularFile(p)).forEach((t: Path) => dropLogFile(t)) - } else dropLogFile(path) + dropDirectory(path) + } else if (path.getFileName.toString.endsWith(".zip")) { + // by default read all files which are contained in the zip file + IoManager.unzip(path, Set()).foreach { + case (fileId, entries) => + if (!contains(fileId)) { + { + addEntriesFromZip(LogFileSettings(fileId), entries) + selectLog(fileId) + } + } else { + logTrace(s"${fileId.absolutePathAsString} is already opened, selecting tab ...") + selectLog(fileId) + } + } + } else { + dropLogFile(path) + } } }) } + private def dropDirectory(path: Path): Unit = { + Files.list(path).filter((p: Path) => Files.isRegularFile(p)).forEach((t: Path) => dropLogFile(t)) + } + /** * Defines what should happen when a tab is selected * */ @@ -115,11 +136,17 @@ class MainTabPane extends TabPane with CanLog { val fileId = FileId(path) val logFileSettings = LogFileSettings(fileId) LogoRRRGlobals.registerSettings(logFileSettings) - - addLogFileTab(LogFileTab(LogoRRRGlobals.getLogFileSettings(fileId), logFileSettings.readEntries())) + val entries = IoManager.readEntries(logFileSettings.path, logFileSettings.someLogEntryInstantFormat) + addLogFileTab(LogFileTab(LogoRRRGlobals.getLogFileSettings(fileId), entries)) selectLog(fileId) } + def addEntriesFromZip(logFileSettings: LogFileSettings, entries: ObservableList[LogEntry]): LogFileTab = timeR({ + LogoRRRGlobals.registerSettings(logFileSettings) + val tab = LogFileTab(LogoRRRGlobals.getLogFileSettings(logFileSettings.fileId), entries) + addLogFileTab(tab) + tab + }, s"Added ${logFileSettings.fileId.absolutePathAsString} to TabPane") /** Adds a new logfile to display and initializes bindings and listeners */ def addLogFileTab(tab: LogFileTab): Unit = { diff --git a/app/src/main/scala/app/logorrr/views/search/OpsToolBar.scala b/app/src/main/scala/app/logorrr/views/search/OpsToolBar.scala index ee563161..09473939 100644 --- a/app/src/main/scala/app/logorrr/views/search/OpsToolBar.scala +++ b/app/src/main/scala/app/logorrr/views/search/OpsToolBar.scala @@ -66,7 +66,7 @@ class OpsToolBar(fileId: FileId // val firstNEntries: ObservableList[LogEntry] = TimerSettingsLogView.mkEntriesToShow(logEntries) -// val timerButton = new TimerButton(pathAsString, firstNEntries) +// val timerButton = new TimerButton(fileId, firstNEntries) def execSearchOnHitEnter(event: KeyEvent): Unit = { if (event.getCode == KeyCode.ENTER) { diff --git a/app/src/main/scala/app/logorrr/views/search/TimerButton.scala b/app/src/main/scala/app/logorrr/views/search/TimerButton.scala index f8cf3309..a29edd45 100644 --- a/app/src/main/scala/app/logorrr/views/search/TimerButton.scala +++ b/app/src/main/scala/app/logorrr/views/search/TimerButton.scala @@ -14,12 +14,12 @@ import org.kordamp.ikonli.fontawesome5.FontAwesomeRegular import org.kordamp.ikonli.javafx.FontIcon -class TimerButton(pathAsString: FileId +class TimerButton(fileId: FileId , logEntriesToDisplay: ObservableList[LogEntry]) extends StackPane with CanLog { - def getSettings: MutLogFileSettings = LogoRRRGlobals.getLogFileSettings(pathAsString) + def getSettings: MutLogFileSettings = LogoRRRGlobals.getLogFileSettings(fileId) def updateLogEntrySetting(leif: LogEntryInstantFormat): Unit = { getSettings.setLogEntryInstantFormat(leif) @@ -43,7 +43,7 @@ class TimerButton(pathAsString: FileId new TimerSettingStage(getSettings, updateLogEntrySetting, logEntriesToDisplay).showAndWait() }) - private val binding: BooleanBinding = LogoRRRGlobals.getLogFileSettings(pathAsString).hasLogEntrySettingBinding.not() + private val binding: BooleanBinding = LogoRRRGlobals.getLogFileSettings(fileId).hasLogEntrySettingBinding.not() private val icon = new FontIcon() icon.setStyle("-fx-icon-code:fas-exclamation-circle;-fx-icon-color:rgba(255, 0, 0, 1);-fx-icon-size:8;") diff --git a/app/src/test/resources/app/logorrr/io/ziputil-bit-more-complex.zip b/app/src/test/resources/app/logorrr/io/ziputil-bit-more-complex.zip new file mode 100644 index 0000000000000000000000000000000000000000..d88e077173c07d93301812c7c503afeaa02ffbc2 GIT binary patch literal 1241 zcmWIWW@Zs#U|`^2(9WM5zSDDl(0U;6Fc7mc$S@e{<>aS_hHx@48y)IPH38z%3T_5Q zmamKq3}9`d-L~9^3`7{p>yO^v(wwxBb+V&I;KBvlwuEZhwWs%}-q4@4;>Y{7r*+G> zpHH6Q!LO$CVBv=cM(H*wcfS9;#J^tVQ%dxl#W~D3)$}(-h!hqznA|+N=t)74oBh6B zYZs{hT;g@GWLMM12NhdRMbz*fS@Hh*6OPS&vlLw?zPbD&AZg074{};}x2}kOy<*+R zb4@>+?gp4$U9+)@C*>l~5r@ zF)#=)ymfpBGzye#f#!gcEmm`|JBb~|Nm@XcCF(gD98hlX%6FiWoPO9kQ5 z3T_5QmamKq3}C&Y-L~9^3`7{p>yO^v(wwxBb+V&I;KBvlwuEZhwWs%}-q4@4;>Y{7 zr*+G>pHH6Q!LO$CVBv=cM(H*wcfS9;#J^tVQ%dxl#W~D3)$}(-h!hqznA|+N=t)74 zoBh6BYZs{hT;g@GWLMM12NhdRMbz*fS@Hh*6OPS&vlLw?zPbD&AZg074{};}x2}kO zy<*+Rb4@>+?gp4$U9+)@C*CYez0|0_WeVhOQ literal 0 HcmV?d00001 diff --git a/app/src/test/scala/app/logorrr/Issue139Spec.scala b/app/src/test/scala/app/logorrr/Issue139Spec.scala index 4262183b..71a75f13 100644 --- a/app/src/test/scala/app/logorrr/Issue139Spec.scala +++ b/app/src/test/scala/app/logorrr/Issue139Spec.scala @@ -1,6 +1,6 @@ package app.logorrr -import app.logorrr.io.FileManager +import app.logorrr.io.IoManager import org.scalatest.wordspec.AnyWordSpec import java.nio.file.{Files, Paths} @@ -12,9 +12,8 @@ class Issue139Spec extends AnyWordSpec { "Logfile" when { "encodedInUtf16" should { val p = Paths.get("src/test/resources/app/logorrr/issue-139.log") - //val p = Paths.get("src/test/resources/app/logorrr/util/orig.log") "exist" in assert(Files.exists(p)) - "can read file" in assert(FileManager.fromPath(p).nonEmpty) + "can read file" in assert(IoManager.fromPath(p).nonEmpty) } } } diff --git a/app/src/test/scala/app/logorrr/LogEntryFileReaderSpec.scala b/app/src/test/scala/app/logorrr/LogEntryFileReaderSpec.scala index dd9155ac..90911ea1 100644 --- a/app/src/test/scala/app/logorrr/LogEntryFileReaderSpec.scala +++ b/app/src/test/scala/app/logorrr/LogEntryFileReaderSpec.scala @@ -1,6 +1,6 @@ package app.logorrr -import app.logorrr.io.FileManager +import app.logorrr.io.IoManager import app.logorrr.util.OsUtil import org.scalatest.wordspec.AnyWordSpec @@ -15,7 +15,7 @@ class LogEntryFileReaderSpec extends AnyWordSpec { "exist" in assert(Files.exists(p)) "be readable" in { if (!OsUtil.isMac) { // fixme: currently this guard exists since otherwise we would have to fiddle around loading native libs on mac - val r = FileManager.from(p) + val r = IoManager.from(p) assert(!r.isEmpty) } } diff --git a/app/src/test/scala/app/logorrr/conf/mut/LogFileSettingsSpec.scala b/app/src/test/scala/app/logorrr/conf/mut/LogFileSettingsSpec.scala index b9f435c2..6029f292 100644 --- a/app/src/test/scala/app/logorrr/conf/mut/LogFileSettingsSpec.scala +++ b/app/src/test/scala/app/logorrr/conf/mut/LogFileSettingsSpec.scala @@ -9,7 +9,7 @@ import org.scalacheck.Gen object LogFileSettingsSpec { val gen: Gen[LogFileSettings] = for { - pathAsString <- Gen.identifier.map(FileId.apply) + fileId <- Gen.identifier.map(FileId.apply) selectedIndex <- Gen.posNum[Int] firstOpened <- Gen.posNum[Long] dPos <- Gen.posNum[Double] @@ -19,7 +19,7 @@ object LogFileSettingsSpec { blockSettings <- BlockSettingsSpec.gen fontSize <- Gen.posNum[Int] autoScroll <- CoreGen.booleanGen - } yield LogFileSettings(pathAsString + } yield LogFileSettings(fileId , selectedIndex , firstOpened , dPos diff --git a/app/src/test/scala/app/logorrr/conf/mut/MutSettingsSpec.scala b/app/src/test/scala/app/logorrr/conf/mut/MutSettingsSpec.scala index 6ce01112..a17bcfa9 100644 --- a/app/src/test/scala/app/logorrr/conf/mut/MutSettingsSpec.scala +++ b/app/src/test/scala/app/logorrr/conf/mut/MutSettingsSpec.scala @@ -7,14 +7,21 @@ import org.scalacheck.Prop class MutSettingsSpec extends LogoRRRSpec { + def mkMutSettings(settings: Settings): MutSettings = { + val s = new MutSettings + s.setStageSettings(settings.stageSettings) + s.setLogFileSettings(settings.fileSettings) + s.setSomeActive(settings.someActive) + s + } "MutSettings" should { "deserialize" in { val s = Settings(StageSettings(0.15142984837327833, 0.5216122226307276, 1, 1), Map(), None, None) - assert(s == MutSettings(s).petrify()) + assert(s == mkMutSettings(s).petrify()) } "de/serialize" in { check(Prop.forAll(SettingsSpec.gen) { - expected: Settings => expected == MutSettings(MutSettings(expected).petrify()).petrify() + expected: Settings => expected == mkMutSettings(mkMutSettings(expected).petrify()).petrify() }) } } diff --git a/app/src/test/scala/app/logorrr/io/IoManagerSpec.scala b/app/src/test/scala/app/logorrr/io/IoManagerSpec.scala new file mode 100644 index 00000000..f414f580 --- /dev/null +++ b/app/src/test/scala/app/logorrr/io/IoManagerSpec.scala @@ -0,0 +1,21 @@ +package app.logorrr.io + +import org.scalatest.wordspec.AnyWordSpec + +import java.nio.file.Paths + +class IoManagerSpec extends AnyWordSpec { + + "read ziputil-simple.zip" in { + val res = IoManager.unzip(Paths.get("src/test/resources/app/logorrr/io/ziputil-simple.zip"), Set()) + assert(res.size == 1) + val (fileId, entries) = res.toSeq.head + assert(fileId.fileName.endsWith("simple.log")) + assert(entries.size == 1) + assert(entries.get(0).value.startsWith("""MSI (c) (94:5C)""")) + } + "read ziputil-bit-more-complex.zip" in { + val res = IoManager.unzip(Paths.get("src/test/resources/app/logorrr/io/ziputil-bit-more-complex.zip"), Set()) + println(res.size == 3) + } +} diff --git a/app/src/test/scala/app/logorrr/views/block/ChunkListTestApp.scala b/app/src/test/scala/app/logorrr/views/block/ChunkListTestApp.scala index a0d2ba71..fb8c2312 100644 --- a/app/src/test/scala/app/logorrr/views/block/ChunkListTestApp.scala +++ b/app/src/test/scala/app/logorrr/views/block/ChunkListTestApp.scala @@ -1,7 +1,7 @@ package app.logorrr.views.block import app.logorrr.LogoRRRApp -import app.logorrr.io.FileManager +import app.logorrr.io.IoManager import app.logorrr.model.{LogEntry, LogFileSettings} import app.logorrr.util.{CanLog, JfxUtils} import app.logorrr.views.search.Filter @@ -33,7 +33,7 @@ object ChunkListTestApp { class ChunkListTestApp extends Application with CanLog { private def mkEntries(path: Path): java.util.List[LogEntry] = { - util.Arrays.asList((for ((l, i) <- FileManager.fromPathUsingSecurityBookmarks(path).zipWithIndex) yield LogEntry(i, l, None)): _*) + util.Arrays.asList((for ((l, i) <- IoManager.fromPathUsingSecurityBookmarks(path).zipWithIndex) yield LogEntry(i, l, None)): _*) } def start(stage: Stage): Unit = { diff --git a/core/src/main/scala/app/logorrr/io/FileId.scala b/core/src/main/scala/app/logorrr/io/FileId.scala index 3325f860..b9af5463 100644 --- a/core/src/main/scala/app/logorrr/io/FileId.scala +++ b/core/src/main/scala/app/logorrr/io/FileId.scala @@ -13,19 +13,33 @@ object FileId { FileId(p.toAbsolutePath.toString) } + def reduceZipFiles(fileIds: Seq[FileId]): Map[FileId, Seq[FileId]] = fileIds.groupBy(fileId => fileId.extractZipFileId) + } + /** - * Identifies a log file + * Identifies a log file or an entry in a zip file. + * + * If a value contains the string '.zip@', it is considered to be an zip file entry. * * @param value is an identifier which is used to discriminate between log files. typically it is a file path. */ case class FileId(value: String) { - def asPath : Path = Paths.get(value) + def extractZipFileId: FileId = FileId(value.substring(0, value.indexOf(".zip@") + 4)) // get filename of zip file + + // if fileId is a zip part, show this relative part + def zipEntryPath: String = { + extractZipFileId.fileName + value.substring(value.indexOf(".zip@") + 4, value.length) + } + + def isZip: Boolean = value.contains(".zip@") + + def asPath: Path = Paths.get(value) def fileName: String = asPath.getFileName.toString - def absolutePathAsString : String = value + def absolutePathAsString: String = value } diff --git a/core/src/main/scala/app/logorrr/io/Fs.scala b/core/src/main/scala/app/logorrr/io/Fs.scala index 94e9ab8a..25fe5fe1 100644 --- a/core/src/main/scala/app/logorrr/io/Fs.scala +++ b/core/src/main/scala/app/logorrr/io/Fs.scala @@ -2,8 +2,10 @@ package app.logorrr.io import app.logorrr.util.CanLog +import java.io.IOException import java.nio.charset.Charset -import java.nio.file.{Files, Path, Paths} +import java.nio.file._ +import java.nio.file.attribute.BasicFileAttributes /** * File related operations diff --git a/core/src/test/scala/app/logorrr/io/FileIdSpec.scala b/core/src/test/scala/app/logorrr/io/FileIdSpec.scala new file mode 100644 index 00000000..369c846b --- /dev/null +++ b/core/src/test/scala/app/logorrr/io/FileIdSpec.scala @@ -0,0 +1,23 @@ +package app.logorrr.io + +import org.scalatest.wordspec.AnyWordSpec + +class FileIdSpec extends AnyWordSpec { + + "reduceZipFiles" in { + val fileIds = Seq(FileId("a.zip@1"), FileId("a.zip@2"), FileId("a.zip@3"), FileId("b.zip@1")) + val res = FileId.reduceZipFiles(fileIds) + assert(res.size == 2) + assert(res(FileId("a.zip")) == Seq(FileId("a.zip@1"), FileId("a.zip@2"), FileId("a.zip@3"))) + assert(res(FileId("b.zip")) == Seq(FileId("b.zip@1"))) + } + "zipentrypath" in { + val id = FileId("real/path/a.zip@an/entry/file.log") + assert(id.isZip) + assert(id.zipEntryPath == "a.zip@an/entry/file.log") + } + "zippath" in { + val id = FileId("real/path/a.zip@an/entry/file.log") + assert(id.extractZipFileId.fileName == "a.zip") + } +}