Convert to Java (domain, config, storage-aws and filesystem) (#446)
* Java rewrite domain (#438) * domain.Bucket: convert to Java * domain.LastModified: convert to Java * domain.QuoteStripper: convert to Java * domain.HexEncoder: convert to Java * domain.MD5Hash: convert to Java * remove unused import * domain.RemoteKey: convert to Java * domain.Action: convert to Java * domain.Counters: convert to Java * domain.HashType: convert to Java * domain.Hashes: convert to Java * domain.MD5HashData: convert to Java * domain.Filter: convert to Java * domain.LocalFile: convert to Java * domain: make immutable field public * domain.SizeTranslation: convert to Java * domain.HashType: restrict access to contstructor * domain.RemoteObjects: convert to Java Introduce MapView and Tuple. * domain.Sources: convert to Java * domain.StorageEvent: convert to Java * domain.Terminal: convert to Java * domain => config: move SimpleLens to only module that uses it * domain => filesystem: move TemporaryFolder * domain.Implicits: removed * parent: make junit, et al available * domain: add testing dependencies * domain.HexEncoder: convert test to Java and fix bugs * domain.HexEncoderTest: replace with Java version * domain.MD5HashTest: convert to Java * domain.RemoteKeyTest: convert to Java * domain.SizeTranslationTest: convert to Java * domain.TerminalTest: convert to Java * domain: remove unused dependencies * parent: rollback zio-streams to match zio and pin them together * storage-aws: resolve transitive dependency conflicts * Java rewrite storage aws (#445) * storage-aws.AmazonS3: convert to Java as AmazonS3Client * storage-aws.S3Copier: convert to Java * storage-aws.S3Uploader: convert to Java * storage-aws.S3Deleter: convert to Java * storage-aws.S3Lister: convert to Java * filesystem: write cache data correctly (as supplied) * domain,filesystem: fix MD5Hash generation * filesystem: convert to Java (#450) * remove legacy * Rewrite config module in Java (#461) * config.ParseConfigFile: convert to Java * config.ParseConfigFile: convert to Java * config.SourceConfigLoader: convert to Java * WIP config.Configuration: convert to Java * config.ConfigOption: convert to Java * config.ConfigOptions: convert to Java * config.ConfigValidation: convert to Java * config.ConfigQuery: convert to Java * config: move classes to correct location * config.ConfigValidationException: convert to Java * config.ConfigValidator: convert to Java * config.ConfigurationBuilder: convert to Java * config.SimpleLens: removed * config.Config: remove environment * config.ConfigOptionTest: convert to Java * config.ConfigQueryTest: convert to Java * config.ConfigurationBuilderTest: convert to Java * config.ParseConfigFileTest: convert to Java * config.ParseConfigLinesTest: convert to Java * config: remove scala dependencies and plugin
This commit is contained in:
parent
823f50d75c
commit
319c46f403
170 changed files with 4602 additions and 4472 deletions
37
.travis.yml
37
.travis.yml
|
@ -1,37 +0,0 @@
|
||||||
language: scala
|
|
||||||
scala:
|
|
||||||
- 2.13.0
|
|
||||||
jdk:
|
|
||||||
- openjdk8
|
|
||||||
- openjdk11
|
|
||||||
env:
|
|
||||||
- AWS_REGION=eu-west-1
|
|
||||||
before_install:
|
|
||||||
- git fetch --tags
|
|
||||||
stages:
|
|
||||||
- name: test
|
|
||||||
- name: release
|
|
||||||
if: ((branch = master AND type = push) OR (tag IS present)) AND NOT fork
|
|
||||||
jobs:
|
|
||||||
include:
|
|
||||||
- stage: test
|
|
||||||
script: sbt ++$TRAVIS_SCALA_VERSION test
|
|
||||||
- stage: coverage
|
|
||||||
script:
|
|
||||||
- sbt clean coverage test coverageAggregate
|
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
|
||||||
- stage: release
|
|
||||||
script: sbt ++$TRAVIS_SCALA_VERSION ci-release
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- $HOME/.sbt/1.0/dependency
|
|
||||||
- $HOME/.sbt/boot/scala*
|
|
||||||
- $HOME/.sbt/launchers
|
|
||||||
- $HOME/.ivy2/cache
|
|
||||||
- $HOME/.coursier
|
|
||||||
before_cache:
|
|
||||||
- du -h -d 1 $HOME/.ivy2/cache
|
|
||||||
- du -h -d 2 $HOME/.sbt/
|
|
||||||
- find $HOME/.sbt -name "*.lock" -type f -delete
|
|
||||||
- find $HOME/.ivy2/cache -name "ivydata-*.properties" -type f -delete
|
|
||||||
- rm -rf $HOME/.ivy2/local
|
|
|
@ -46,12 +46,6 @@
|
||||||
<artifactId>thorp-uishell</artifactId>
|
<artifactId>thorp-uishell</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- command line parsing -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.scopt</groupId>
|
|
||||||
<artifactId>scopt_2.13</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- scala -->
|
<!-- scala -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.scala-lang</groupId>
|
<groupId>org.scala-lang</groupId>
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
net.kemitix.thorp.filesystem.MD5HashGenerator
|
||||||
|
net.kemitix.thorp.storage.aws.S3ETagGenerator
|
|
@ -1,11 +1,8 @@
|
||||||
package net.kemitix.thorp
|
package net.kemitix.thorp
|
||||||
|
|
||||||
import net.kemitix.thorp.config.Config
|
|
||||||
import net.kemitix.thorp.console.Console
|
import net.kemitix.thorp.console.Console
|
||||||
import net.kemitix.thorp.filesystem.FileSystem
|
|
||||||
import net.kemitix.thorp.lib.FileScanner
|
import net.kemitix.thorp.lib.FileScanner
|
||||||
import net.kemitix.thorp.storage.aws.S3Storage
|
import net.kemitix.thorp.storage.aws.S3Storage
|
||||||
import net.kemitix.thorp.storage.aws.hasher.S3Hasher
|
|
||||||
import zio.clock.Clock
|
import zio.clock.Clock
|
||||||
import zio.{App, ZEnv, ZIO}
|
import zio.{App, ZEnv, ZIO}
|
||||||
|
|
||||||
|
@ -15,9 +12,6 @@ object Main extends App {
|
||||||
extends S3Storage.Live
|
extends S3Storage.Live
|
||||||
with Console.Live
|
with Console.Live
|
||||||
with Clock.Live
|
with Clock.Live
|
||||||
with Config.Live
|
|
||||||
with FileSystem.Live
|
|
||||||
with S3Hasher.Live
|
|
||||||
with FileScanner.Live
|
with FileScanner.Live
|
||||||
|
|
||||||
override def run(args: List[String]): ZIO[ZEnv, Nothing, Int] =
|
override def run(args: List[String]): ZIO[ZEnv, Nothing, Int] =
|
||||||
|
|
|
@ -5,89 +5,100 @@ import net.kemitix.eip.zio.{Message, MessageChannel}
|
||||||
import net.kemitix.thorp.cli.CliArgs
|
import net.kemitix.thorp.cli.CliArgs
|
||||||
import net.kemitix.thorp.config._
|
import net.kemitix.thorp.config._
|
||||||
import net.kemitix.thorp.console._
|
import net.kemitix.thorp.console._
|
||||||
import net.kemitix.thorp.domain.{Counters, SimpleLens, StorageEvent}
|
|
||||||
import net.kemitix.thorp.domain.StorageEvent.{
|
import net.kemitix.thorp.domain.StorageEvent.{
|
||||||
CopyEvent,
|
CopyEvent,
|
||||||
DeleteEvent,
|
DeleteEvent,
|
||||||
ErrorEvent,
|
ErrorEvent,
|
||||||
UploadEvent
|
UploadEvent
|
||||||
}
|
}
|
||||||
import net.kemitix.thorp.filesystem.{FileSystem, Hasher}
|
import net.kemitix.thorp.domain.{Counters, RemoteObjects, StorageEvent}
|
||||||
import net.kemitix.thorp.lib._
|
import net.kemitix.thorp.lib._
|
||||||
import net.kemitix.thorp.storage.Storage
|
import net.kemitix.thorp.storage.Storage
|
||||||
import net.kemitix.thorp.uishell.{UIEvent, UIShell}
|
import net.kemitix.thorp.uishell.{UIEvent, UIShell}
|
||||||
import zio.clock.Clock
|
import zio.clock.Clock
|
||||||
import zio.{RIO, UIO, ZIO}
|
import zio.{IO, RIO, UIO, ZIO}
|
||||||
import scala.io.AnsiColor.{WHITE, RESET}
|
|
||||||
|
import scala.io.AnsiColor.{RESET, WHITE}
|
||||||
|
import scala.jdk.CollectionConverters._
|
||||||
|
|
||||||
trait Program {
|
trait Program {
|
||||||
|
|
||||||
val version = "0.11.0"
|
val version = "0.11.0"
|
||||||
lazy val versionLabel = s"${WHITE}Thorp v${version}$RESET"
|
lazy val versionLabel = s"${WHITE}Thorp v$version$RESET"
|
||||||
|
|
||||||
def run(args: List[String]): ZIO[
|
def run(args: List[String])
|
||||||
Storage with Console with Config with Clock with FileSystem with Hasher with FileScanner,
|
: ZIO[Storage with Console with Clock with FileScanner, Nothing, Unit] = {
|
||||||
Throwable,
|
(for {
|
||||||
Unit] = {
|
|
||||||
for {
|
|
||||||
cli <- CliArgs.parse(args)
|
cli <- CliArgs.parse(args)
|
||||||
config <- ConfigurationBuilder.buildConfig(cli)
|
config <- IO(ConfigurationBuilder.buildConfig(cli))
|
||||||
_ <- Config.set(config)
|
|
||||||
_ <- Console.putStrLn(versionLabel)
|
_ <- Console.putStrLn(versionLabel)
|
||||||
_ <- ZIO.when(!showVersion(cli))(executeWithUI.catchAll(handleErrors))
|
_ <- ZIO.when(!showVersion(cli))(
|
||||||
} yield ()
|
executeWithUI(config).catchAll(handleErrors))
|
||||||
|
} yield ())
|
||||||
|
.catchAll(e => {
|
||||||
|
Console.putStrLn("An ERROR occurred:")
|
||||||
|
Console.putStrLn(e.getMessage)
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def showVersion: ConfigOptions => Boolean =
|
private def showVersion: ConfigOptions => Boolean =
|
||||||
cli => ConfigQuery.showVersion(cli)
|
cli => ConfigQuery.showVersion(cli)
|
||||||
|
|
||||||
private def executeWithUI =
|
private def executeWithUI(configuration: Configuration) =
|
||||||
for {
|
for {
|
||||||
uiEventSender <- execute
|
uiEventSender <- execute(configuration)
|
||||||
uiEventReceiver <- UIShell.receiver
|
uiEventReceiver <- UIShell.receiver(configuration)
|
||||||
_ <- MessageChannel.pointToPoint(uiEventSender)(uiEventReceiver).runDrain
|
_ <- MessageChannel.pointToPoint(uiEventSender)(uiEventReceiver).runDrain
|
||||||
} yield ()
|
} yield ()
|
||||||
|
|
||||||
type UIChannel = UChannel[Any, UIEvent]
|
type UIChannel = UChannel[Any, UIEvent]
|
||||||
|
|
||||||
private def execute
|
private def execute(configuration: Configuration): ZIO[
|
||||||
: ZIO[Any,
|
Any,
|
||||||
Nothing,
|
Nothing,
|
||||||
MessageChannel.ESender[
|
MessageChannel.ESender[Storage with Clock with FileScanner with Console,
|
||||||
Storage with Config with FileSystem with Hasher with Clock with FileScanner with Console,
|
Throwable,
|
||||||
Throwable,
|
UIEvent]] = UIO { uiChannel =>
|
||||||
UIEvent]] = UIO { uiChannel =>
|
|
||||||
(for {
|
(for {
|
||||||
_ <- showValidConfig(uiChannel)
|
_ <- showValidConfig(uiChannel)
|
||||||
remoteData <- fetchRemoteData(uiChannel)
|
remoteData <- fetchRemoteData(configuration, uiChannel)
|
||||||
archive <- UIO(UnversionedMirrorArchive)
|
archive <- UIO(UnversionedMirrorArchive)
|
||||||
copyUploadEvents <- LocalFileSystem.scanCopyUpload(uiChannel,
|
copyUploadEvents <- LocalFileSystem.scanCopyUpload(configuration,
|
||||||
|
uiChannel,
|
||||||
remoteData,
|
remoteData,
|
||||||
archive)
|
archive)
|
||||||
deleteEvents <- LocalFileSystem.scanDelete(uiChannel, remoteData, archive)
|
deleteEvents <- LocalFileSystem.scanDelete(configuration,
|
||||||
_ <- showSummary(uiChannel)(copyUploadEvents ++ deleteEvents)
|
uiChannel,
|
||||||
|
remoteData,
|
||||||
|
archive)
|
||||||
|
_ <- showSummary(uiChannel)(copyUploadEvents ++ deleteEvents)
|
||||||
} yield ()) <* MessageChannel.endChannel(uiChannel)
|
} yield ()) <* MessageChannel.endChannel(uiChannel)
|
||||||
}
|
}
|
||||||
|
|
||||||
private def showValidConfig(uiChannel: UIChannel) =
|
private def showValidConfig(uiChannel: UIChannel) =
|
||||||
Message.create(UIEvent.ShowValidConfig) >>= MessageChannel.send(uiChannel)
|
Message.create(UIEvent.ShowValidConfig) >>= MessageChannel.send(uiChannel)
|
||||||
|
|
||||||
private def fetchRemoteData(uiChannel: UIChannel) =
|
private def fetchRemoteData(configuration: Configuration,
|
||||||
|
uiChannel: UIChannel)
|
||||||
|
: ZIO[Clock with Storage with Console, Throwable, RemoteObjects] = {
|
||||||
|
val bucket = configuration.bucket
|
||||||
|
val prefix = configuration.prefix
|
||||||
for {
|
for {
|
||||||
bucket <- Config.bucket
|
|
||||||
prefix <- Config.prefix
|
|
||||||
objects <- Storage.list(bucket, prefix)
|
objects <- Storage.list(bucket, prefix)
|
||||||
_ <- Message.create(UIEvent.RemoteDataFetched(objects.byKey.size)) >>= MessageChannel
|
_ <- Message.create(UIEvent.RemoteDataFetched(objects.byKey.size)) >>= MessageChannel
|
||||||
.send(uiChannel)
|
.send(uiChannel)
|
||||||
} yield objects
|
} yield objects
|
||||||
|
}
|
||||||
|
|
||||||
private def handleErrors(throwable: Throwable) =
|
private def handleErrors(throwable: Throwable) =
|
||||||
Console.putStrLn("There were errors:") *> logValidationErrors(throwable)
|
Console.putStrLn("There were errors:") *> logValidationErrors(throwable)
|
||||||
|
|
||||||
private def logValidationErrors(throwable: Throwable) =
|
private def logValidationErrors(throwable: Throwable) =
|
||||||
throwable match {
|
throwable match {
|
||||||
case ConfigValidationException(errors) =>
|
case validateError: ConfigValidationException =>
|
||||||
ZIO.foreach_(errors)(error => Console.putStrLn(s"- $error"))
|
ZIO.foreach_(validateError.getErrors.asScala)(error =>
|
||||||
|
Console.putStrLn(s"- $error"))
|
||||||
}
|
}
|
||||||
|
|
||||||
private def showSummary(uiChannel: UIChannel)(
|
private def showSummary(uiChannel: UIChannel)(
|
||||||
|
@ -99,13 +110,11 @@ trait Program {
|
||||||
|
|
||||||
private def countActivities: (Counters, StorageEvent) => Counters =
|
private def countActivities: (Counters, StorageEvent) => Counters =
|
||||||
(counters: Counters, s3Action: StorageEvent) => {
|
(counters: Counters, s3Action: StorageEvent) => {
|
||||||
def increment: SimpleLens[Counters, Int] => Counters =
|
|
||||||
_.modify(_ + 1)(counters)
|
|
||||||
s3Action match {
|
s3Action match {
|
||||||
case _: UploadEvent => increment(Counters.uploaded)
|
case _: UploadEvent => counters.incrementUploaded()
|
||||||
case _: CopyEvent => increment(Counters.copied)
|
case _: CopyEvent => counters.incrementCopied()
|
||||||
case _: DeleteEvent => increment(Counters.deleted)
|
case _: DeleteEvent => counters.incrementDeleted()
|
||||||
case _: ErrorEvent => increment(Counters.errors)
|
case _: ErrorEvent => counters.incrementErrors()
|
||||||
case _ => counters
|
case _ => counters
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
172
build.sbt
172
build.sbt
|
@ -1,172 +0,0 @@
|
||||||
import sbtassembly.AssemblyPlugin.defaultShellScript
|
|
||||||
|
|
||||||
inThisBuild(List(
|
|
||||||
organization := "net.kemitix.thorp",
|
|
||||||
homepage := Some(url("https://github.com/kemitix/thorp")),
|
|
||||||
licenses := List("mit" -> url("https://opensource.org/licenses/MIT")),
|
|
||||||
developers := List(
|
|
||||||
Developer(
|
|
||||||
"kemitix",
|
|
||||||
"Paul Campbell",
|
|
||||||
"pcampbell@kemitix.net",
|
|
||||||
url("https://github.kemitix.net")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
))
|
|
||||||
|
|
||||||
val commonSettings = Seq(
|
|
||||||
sonatypeProfileName := "net.kemitix",
|
|
||||||
scalaVersion := "2.13.0",
|
|
||||||
scalacOptions ++= Seq(
|
|
||||||
"-Ywarn-unused:imports",
|
|
||||||
"-Xfatal-warnings",
|
|
||||||
"-feature",
|
|
||||||
"-deprecation",
|
|
||||||
"-unchecked",
|
|
||||||
"-language:postfixOps",
|
|
||||||
"-language:higherKinds"),
|
|
||||||
wartremoverErrors ++= Warts.unsafe.filterNot(wart => List(
|
|
||||||
Wart.Any,
|
|
||||||
Wart.Nothing,
|
|
||||||
Wart.Serializable,
|
|
||||||
Wart.NonUnitStatements,
|
|
||||||
Wart.StringPlusAny
|
|
||||||
).contains(wart)),
|
|
||||||
test in assembly := {},
|
|
||||||
assemblyMergeStrategy in assembly := {
|
|
||||||
case PathList("META-INF", xs @ _*) => MergeStrategy.discard
|
|
||||||
case x => MergeStrategy.first
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val applicationSettings = Seq(
|
|
||||||
name := "thorp",
|
|
||||||
)
|
|
||||||
val testDependencies = Seq(
|
|
||||||
libraryDependencies ++= Seq(
|
|
||||||
"org.scalatest" %% "scalatest" % "3.0.8" % Test,
|
|
||||||
"org.scalamock" %% "scalamock" % "4.4.0" % Test
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val commandLineParsing = Seq(
|
|
||||||
libraryDependencies ++= Seq(
|
|
||||||
"com.github.scopt" %% "scopt" % "4.0.0-RC2"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val awsSdkDependencies = Seq(
|
|
||||||
libraryDependencies ++= Seq(
|
|
||||||
"com.amazonaws" % "aws-java-sdk-s3" % "1.11.797",
|
|
||||||
// override the versions AWS uses, which is they do to preserve Java 6 compatibility
|
|
||||||
"com.fasterxml.jackson.core" % "jackson-databind" % "2.10.4",
|
|
||||||
"com.fasterxml.jackson.dataformat" % "jackson-dataformat-cbor" % "2.10.4",
|
|
||||||
"javax.xml.bind" % "jaxb-api" % "2.3.1"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val zioDependencies = Seq(
|
|
||||||
libraryDependencies ++= Seq (
|
|
||||||
"dev.zio" %% "zio" % "1.0.0-RC16",
|
|
||||||
"dev.zio" %% "zio-streams" % "1.0.0-RC16"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val eipDependencies = Seq(
|
|
||||||
libraryDependencies ++= Seq(
|
|
||||||
"net.kemitix" %% "eip-zio" % "0.3.2"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
lazy val thorp = (project in file("."))
|
|
||||||
.settings(commonSettings)
|
|
||||||
.aggregate(app, cli, config, console, domain, filesystem, lib, storage, `storage-aws`, uishell)
|
|
||||||
|
|
||||||
lazy val app = (project in file("app"))
|
|
||||||
.settings(commonSettings)
|
|
||||||
.settings(mainClass in assembly := Some("net.kemitix.thorp.Main"))
|
|
||||||
.settings(applicationSettings)
|
|
||||||
.settings(eipDependencies)
|
|
||||||
.settings(Seq(
|
|
||||||
assemblyOption in assembly := (
|
|
||||||
assemblyOption in assembly).value
|
|
||||||
.copy(prependShellScript =
|
|
||||||
Some(defaultShellScript)),
|
|
||||||
assemblyJarName in assembly := "thorp"
|
|
||||||
))
|
|
||||||
.dependsOn(cli)
|
|
||||||
.dependsOn(lib)
|
|
||||||
.dependsOn(`storage-aws`)
|
|
||||||
|
|
||||||
lazy val cli = (project in file("cli"))
|
|
||||||
.settings(commonSettings)
|
|
||||||
.settings(testDependencies)
|
|
||||||
.dependsOn(config)
|
|
||||||
.dependsOn(filesystem % "test->test")
|
|
||||||
|
|
||||||
lazy val `storage-aws` = (project in file("storage-aws"))
|
|
||||||
.settings(commonSettings)
|
|
||||||
.settings(assemblyJarName in assembly := "storage-aws.jar")
|
|
||||||
.settings(awsSdkDependencies)
|
|
||||||
.settings(testDependencies)
|
|
||||||
.dependsOn(storage)
|
|
||||||
.dependsOn(filesystem % "compile->compile;test->test")
|
|
||||||
.dependsOn(console)
|
|
||||||
.dependsOn(lib)
|
|
||||||
|
|
||||||
lazy val lib = (project in file("lib"))
|
|
||||||
.settings(commonSettings)
|
|
||||||
.settings(assemblyJarName in assembly := "lib.jar")
|
|
||||||
.settings(testDependencies)
|
|
||||||
.enablePlugins(BuildInfoPlugin)
|
|
||||||
.settings(
|
|
||||||
buildInfoKeys := Seq[BuildInfoKey](name, version),
|
|
||||||
buildInfoPackage := "thorp"
|
|
||||||
)
|
|
||||||
.dependsOn(storage)
|
|
||||||
.dependsOn(console)
|
|
||||||
.dependsOn(config)
|
|
||||||
.dependsOn(domain % "compile->compile;test->test")
|
|
||||||
.dependsOn(filesystem % "compile->compile;test->test")
|
|
||||||
|
|
||||||
lazy val storage = (project in file("storage"))
|
|
||||||
.settings(commonSettings)
|
|
||||||
.settings(zioDependencies)
|
|
||||||
.settings(assemblyJarName in assembly := "storage.jar")
|
|
||||||
.dependsOn(uishell)
|
|
||||||
.dependsOn(domain)
|
|
||||||
|
|
||||||
lazy val uishell = (project in file("uishell"))
|
|
||||||
.settings(commonSettings)
|
|
||||||
.settings(zioDependencies)
|
|
||||||
.settings(eipDependencies)
|
|
||||||
.settings(assemblyJarName in assembly := "uishell.jar")
|
|
||||||
.dependsOn(config)
|
|
||||||
.dependsOn(console)
|
|
||||||
.dependsOn(filesystem)
|
|
||||||
|
|
||||||
lazy val console = (project in file("console"))
|
|
||||||
.settings(commonSettings)
|
|
||||||
.settings(zioDependencies)
|
|
||||||
.settings(assemblyJarName in assembly := "console.jar")
|
|
||||||
.dependsOn(domain)
|
|
||||||
|
|
||||||
lazy val config = (project in file("config"))
|
|
||||||
.settings(commonSettings)
|
|
||||||
.settings(zioDependencies)
|
|
||||||
.settings(testDependencies)
|
|
||||||
.settings(commandLineParsing)
|
|
||||||
.settings(assemblyJarName in assembly := "config.jar")
|
|
||||||
.dependsOn(domain % "compile->compile;test->test")
|
|
||||||
.dependsOn(filesystem)
|
|
||||||
|
|
||||||
lazy val filesystem = (project in file("filesystem"))
|
|
||||||
.settings(commonSettings)
|
|
||||||
.settings(zioDependencies)
|
|
||||||
.settings(testDependencies)
|
|
||||||
.settings(assemblyJarName in assembly := "filesystem.jar")
|
|
||||||
.dependsOn(domain % "compile->compile;test->test")
|
|
||||||
|
|
||||||
lazy val domain = (project in file("domain"))
|
|
||||||
.settings(commonSettings)
|
|
||||||
.settings(assemblyJarName in assembly := "domain.jar")
|
|
||||||
.settings(testDependencies)
|
|
||||||
.settings(zioDependencies)
|
|
||||||
.settings(eipDependencies)
|
|
17
cli/pom.xml
17
cli/pom.xml
|
@ -22,23 +22,30 @@
|
||||||
<artifactId>thorp-filesystem</artifactId>
|
<artifactId>thorp-filesystem</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- command line parsing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.scopt</groupId>
|
||||||
|
<artifactId>scopt_2.13</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- scala -->
|
<!-- scala -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.scala-lang</groupId>
|
<groupId>org.scala-lang</groupId>
|
||||||
<artifactId>scala-library</artifactId>
|
<artifactId>scala-library</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- zio -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.zio</groupId>
|
||||||
|
<artifactId>zio_2.13</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- scala - testing -->
|
<!-- scala - testing -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.scalatest</groupId>
|
<groupId>org.scalatest</groupId>
|
||||||
<artifactId>scalatest_2.13</artifactId>
|
<artifactId>scalatest_2.13</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.scalamock</groupId>
|
|
||||||
<artifactId>scalamock_2.13</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -2,6 +2,8 @@ package net.kemitix.thorp.cli
|
||||||
|
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
import scala.jdk.CollectionConverters._
|
||||||
|
|
||||||
import net.kemitix.thorp.config.{ConfigOption, ConfigOptions}
|
import net.kemitix.thorp.config.{ConfigOption, ConfigOptions}
|
||||||
import scopt.OParser
|
import scopt.OParser
|
||||||
import zio.Task
|
import zio.Task
|
||||||
|
@ -11,7 +13,7 @@ object CliArgs {
|
||||||
def parse(args: List[String]): Task[ConfigOptions] = Task {
|
def parse(args: List[String]): Task[ConfigOptions] = Task {
|
||||||
OParser
|
OParser
|
||||||
.parse(configParser, args, List())
|
.parse(configParser, args, List())
|
||||||
.map(ConfigOptions(_))
|
.map(options => ConfigOptions.create(options.asJava))
|
||||||
.getOrElse(ConfigOptions.empty)
|
.getOrElse(ConfigOptions.empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,40 +24,40 @@ object CliArgs {
|
||||||
programName("thorp"),
|
programName("thorp"),
|
||||||
head("thorp"),
|
head("thorp"),
|
||||||
opt[Unit]('V', "version")
|
opt[Unit]('V', "version")
|
||||||
.action((_, cos) => ConfigOption.Version :: cos)
|
.action((_, cos) => ConfigOption.version() :: cos)
|
||||||
.text("Show version"),
|
.text("Show version"),
|
||||||
opt[Unit]('B', "batch")
|
opt[Unit]('B', "batch")
|
||||||
.action((_, cos) => ConfigOption.BatchMode :: cos)
|
.action((_, cos) => ConfigOption.batchMode() :: cos)
|
||||||
.text("Enable batch-mode"),
|
.text("Enable batch-mode"),
|
||||||
opt[String]('s', "source")
|
opt[String]('s', "source")
|
||||||
.unbounded()
|
.unbounded()
|
||||||
.action((str, cos) => ConfigOption.Source(Paths.get(str)) :: cos)
|
.action((str, cos) => ConfigOption.source(Paths.get(str)) :: cos)
|
||||||
.text("Source directory to sync to destination"),
|
.text("Source directory to sync to destination"),
|
||||||
opt[String]('b', "bucket")
|
opt[String]('b', "bucket")
|
||||||
.action((str, cos) => ConfigOption.Bucket(str) :: cos)
|
.action((str, cos) => ConfigOption.bucket(str) :: cos)
|
||||||
.text("S3 bucket name"),
|
.text("S3 bucket name"),
|
||||||
opt[String]('p', "prefix")
|
opt[String]('p', "prefix")
|
||||||
.action((str, cos) => ConfigOption.Prefix(str) :: cos)
|
.action((str, cos) => ConfigOption.prefix(str) :: cos)
|
||||||
.text("Prefix within the S3 Bucket"),
|
.text("Prefix within the S3 Bucket"),
|
||||||
opt[Int]('P', "parallel")
|
opt[Int]('P', "parallel")
|
||||||
.action((int, cos) => ConfigOption.Parallel(int) :: cos)
|
.action((int, cos) => ConfigOption.parallel(int) :: cos)
|
||||||
.text("Maximum Parallel uploads"),
|
.text("Maximum Parallel uploads"),
|
||||||
opt[String]('i', "include")
|
opt[String]('i', "include")
|
||||||
.unbounded()
|
.unbounded()
|
||||||
.action((str, cos) => ConfigOption.Include(str) :: cos)
|
.action((str, cos) => ConfigOption.include(str) :: cos)
|
||||||
.text("Include only matching paths"),
|
.text("Include only matching paths"),
|
||||||
opt[String]('x', "exclude")
|
opt[String]('x', "exclude")
|
||||||
.unbounded()
|
.unbounded()
|
||||||
.action((str, cos) => ConfigOption.Exclude(str) :: cos)
|
.action((str, cos) => ConfigOption.exclude(str) :: cos)
|
||||||
.text("Exclude matching paths"),
|
.text("Exclude matching paths"),
|
||||||
opt[Unit]('d', "debug")
|
opt[Unit]('d', "debug")
|
||||||
.action((_, cos) => ConfigOption.Debug() :: cos)
|
.action((_, cos) => ConfigOption.debug() :: cos)
|
||||||
.text("Enable debug logging"),
|
.text("Enable debug logging"),
|
||||||
opt[Unit]("no-global")
|
opt[Unit]("no-global")
|
||||||
.action((_, cos) => ConfigOption.IgnoreGlobalOptions :: cos)
|
.action((_, cos) => ConfigOption.ignoreGlobalOptions() :: cos)
|
||||||
.text("Ignore global configuration"),
|
.text("Ignore global configuration"),
|
||||||
opt[Unit]("no-user")
|
opt[Unit]("no-user")
|
||||||
.action((_, cos) => ConfigOption.IgnoreUserOptions :: cos)
|
.action((_, cos) => ConfigOption.ignoreUserOptions() :: cos)
|
||||||
.text("Ignore user configuration")
|
.text("Ignore user configuration")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,19 +2,19 @@ package net.kemitix.thorp.cli
|
||||||
|
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
|
||||||
import net.kemitix.thorp.config.ConfigOption.Debug
|
import net.kemitix.thorp.config.{ConfigOption, ConfigOptions, ConfigQuery}
|
||||||
import net.kemitix.thorp.config.{ConfigOptions, ConfigQuery}
|
|
||||||
import net.kemitix.thorp.filesystem.Resource
|
import net.kemitix.thorp.filesystem.Resource
|
||||||
import org.scalatest.FunSpec
|
import org.scalatest.FunSpec
|
||||||
import zio.DefaultRuntime
|
import zio.DefaultRuntime
|
||||||
|
|
||||||
|
import scala.jdk.CollectionConverters._
|
||||||
import scala.util.Try
|
import scala.util.Try
|
||||||
|
|
||||||
class CliArgsTest extends FunSpec {
|
class CliArgsTest extends FunSpec {
|
||||||
|
|
||||||
private val runtime = new DefaultRuntime {}
|
private val runtime = new DefaultRuntime {}
|
||||||
|
|
||||||
val source = Resource(this, "")
|
val source = Resource.select(this, "")
|
||||||
|
|
||||||
describe("parse - source") {
|
describe("parse - source") {
|
||||||
def invokeWithSource(path: String) =
|
def invokeWithSource(path: String) =
|
||||||
|
@ -36,7 +36,8 @@ class CliArgsTest extends FunSpec {
|
||||||
it("should get multiple sources") {
|
it("should get multiple sources") {
|
||||||
val expected = Some(Set("path1", "path2").map(Paths.get(_)))
|
val expected = Some(Set("path1", "path2").map(Paths.get(_)))
|
||||||
val configOptions = invoke(args)
|
val configOptions = invoke(args)
|
||||||
val result = configOptions.map(ConfigQuery.sources(_).paths.toSet)
|
val result =
|
||||||
|
configOptions.map(ConfigQuery.sources(_).paths.asScala.toSet)
|
||||||
assertResult(expected)(result)
|
assertResult(expected)(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +51,8 @@ class CliArgsTest extends FunSpec {
|
||||||
maybeOptions.getOrElse(ConfigOptions.empty)
|
maybeOptions.getOrElse(ConfigOptions.empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
val containsDebug = ConfigOptions.contains(Debug())(_)
|
val containsDebug = (options: ConfigOptions) =>
|
||||||
|
options.options.stream().anyMatch(_.isInstanceOf[ConfigOption.Debug])
|
||||||
|
|
||||||
describe("when no debug flag") {
|
describe("when no debug flag") {
|
||||||
val configOptions = invokeWithArgument("")
|
val configOptions = invokeWithArgument("")
|
||||||
|
@ -96,7 +98,7 @@ class CliArgsTest extends FunSpec {
|
||||||
}
|
}
|
||||||
|
|
||||||
private def pathTo(value: String): String =
|
private def pathTo(value: String): String =
|
||||||
Try(Resource(this, value))
|
Try(Resource.select(this, value))
|
||||||
.map(_.getCanonicalPath)
|
.map(_.getCanonicalPath)
|
||||||
.getOrElse("[not-found]")
|
.getOrElse("[not-found]")
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,19 @@
|
||||||
<name>config</name>
|
<name>config</name>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<!-- mon -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.kemitix</groupId>
|
||||||
|
<artifactId>mon</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- lombok -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- thorp -->
|
<!-- thorp -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.kemitix.thorp</groupId>
|
<groupId>net.kemitix.thorp</groupId>
|
||||||
|
@ -22,48 +35,16 @@
|
||||||
<artifactId>thorp-filesystem</artifactId>
|
<artifactId>thorp-filesystem</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- command line parsing -->
|
<!-- testing -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.scopt</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>scopt_2.13</artifactId>
|
<artifactId>junit-jupiter</artifactId>
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- scala -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.scala-lang</groupId>
|
|
||||||
<artifactId>scala-library</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- zio -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>dev.zio</groupId>
|
|
||||||
<artifactId>zio_2.13</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>dev.zio</groupId>
|
|
||||||
<artifactId>zio-streams_2.13</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- scala - testing -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.scalatest</groupId>
|
|
||||||
<artifactId>scalatest_2.13</artifactId>
|
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.scalamock</groupId>
|
<groupId>org.assertj</groupId>
|
||||||
<artifactId>scalamock_2.13</artifactId>
|
<artifactId>assertj-core</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>net.alchim31.maven</groupId>
|
|
||||||
<artifactId>scala-maven-plugin</artifactId>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
|
|
||||||
</project>
|
</project>
|
178
config/src/main/java/net/kemitix/thorp/config/ConfigOption.java
Normal file
178
config/src/main/java/net/kemitix/thorp/config/ConfigOption.java
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
package net.kemitix.thorp.config;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import net.kemitix.mon.TypeAlias;
|
||||||
|
import net.kemitix.thorp.domain.Filter;
|
||||||
|
import net.kemitix.thorp.domain.RemoteKey;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ConfigOption {
|
||||||
|
Configuration update(Configuration config);
|
||||||
|
|
||||||
|
static ConfigOption source(Path path) {
|
||||||
|
return new Source(path);
|
||||||
|
}
|
||||||
|
class Source extends TypeAlias<Path> implements ConfigOption {
|
||||||
|
private Source(Path value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public Configuration update(Configuration config) {
|
||||||
|
return config.withSources(config.sources.append(getValue()));
|
||||||
|
}
|
||||||
|
public Path path() {
|
||||||
|
return getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConfigOption bucket(String name) {
|
||||||
|
return new Bucket(name);
|
||||||
|
}
|
||||||
|
class Bucket extends TypeAlias<String> implements ConfigOption {
|
||||||
|
private Bucket(String value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public Configuration update(Configuration config) {
|
||||||
|
return config.withBucket(
|
||||||
|
net.kemitix.thorp.domain.Bucket.named(getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConfigOption prefix(String path) {
|
||||||
|
return new Prefix(path);
|
||||||
|
}
|
||||||
|
class Prefix extends TypeAlias<String> implements ConfigOption {
|
||||||
|
private Prefix(String value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public Configuration update(Configuration config) {
|
||||||
|
return config.withPrefix(RemoteKey.create(getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConfigOption include(String pattern) {
|
||||||
|
return new Include(pattern);
|
||||||
|
}
|
||||||
|
class Include extends TypeAlias<String> implements ConfigOption {
|
||||||
|
private Include(String value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public Configuration update(Configuration config) {
|
||||||
|
List<Filter> filters = new ArrayList<>(config.filters);
|
||||||
|
filters.add(net.kemitix.thorp.domain.Filter.include(getValue()));
|
||||||
|
return config.withFilters(filters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConfigOption exclude(String pattern) {
|
||||||
|
return new Exclude(pattern);
|
||||||
|
}
|
||||||
|
class Exclude extends TypeAlias<String> implements ConfigOption {
|
||||||
|
private Exclude(String value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public Configuration update(Configuration config) {
|
||||||
|
List<Filter> filters = new ArrayList<>(config.filters);
|
||||||
|
filters.add(net.kemitix.thorp.domain.Filter.exclude(getValue()));
|
||||||
|
return config.withFilters(filters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConfigOption debug() {
|
||||||
|
return new Debug();
|
||||||
|
}
|
||||||
|
@EqualsAndHashCode
|
||||||
|
class Debug implements ConfigOption {
|
||||||
|
@Override
|
||||||
|
public Configuration update(Configuration config) {
|
||||||
|
return config.withDebug(true);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Debug";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConfigOption batchMode() {
|
||||||
|
return new BatchMode();
|
||||||
|
}
|
||||||
|
class BatchMode implements ConfigOption {
|
||||||
|
@Override
|
||||||
|
public Configuration update(Configuration config) {
|
||||||
|
return config.withBatchMode(true);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "BatchMode";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConfigOption version() {
|
||||||
|
return new Version();
|
||||||
|
}
|
||||||
|
class Version implements ConfigOption {
|
||||||
|
@Override
|
||||||
|
public Configuration update(Configuration config) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Version";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConfigOption ignoreUserOptions() {
|
||||||
|
return new IgnoreUserOptions();
|
||||||
|
}
|
||||||
|
class IgnoreUserOptions implements ConfigOption {
|
||||||
|
@Override
|
||||||
|
public Configuration update(Configuration config) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Ignore User Options";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConfigOption ignoreGlobalOptions() {
|
||||||
|
return new IgnoreGlobalOptions();
|
||||||
|
}
|
||||||
|
class IgnoreGlobalOptions implements ConfigOption {
|
||||||
|
@Override
|
||||||
|
public Configuration update(Configuration config) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Ignore Global Options";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConfigOption parallel(int factor) {
|
||||||
|
return new Parallel(factor);
|
||||||
|
}
|
||||||
|
class Parallel extends TypeAlias<Integer> implements ConfigOption {
|
||||||
|
protected Parallel(Integer value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public Configuration update(Configuration config) {
|
||||||
|
return config.withParallel(getValue());
|
||||||
|
}
|
||||||
|
public int factor() {
|
||||||
|
return getValue();
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Parallel: " + getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package net.kemitix.thorp.config;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public interface ConfigOptions {
|
||||||
|
List<ConfigOption> options();
|
||||||
|
ConfigOptions merge(ConfigOptions other);
|
||||||
|
ConfigOptions prepend(ConfigOption configOption);
|
||||||
|
boolean containsInstanceOf(Class<? extends ConfigOption> type);
|
||||||
|
static int parallel(ConfigOptions configOptions) {
|
||||||
|
return configOptions.options()
|
||||||
|
.stream()
|
||||||
|
.filter(option -> option instanceof ConfigOption.Parallel)
|
||||||
|
.map(ConfigOption.Parallel.class::cast)
|
||||||
|
.findFirst()
|
||||||
|
.map(ConfigOption.Parallel::factor)
|
||||||
|
.orElse(1);
|
||||||
|
}
|
||||||
|
static ConfigOptions empty() {
|
||||||
|
return create(Collections.emptyList());
|
||||||
|
}
|
||||||
|
static ConfigOptions create(List<ConfigOption> options) {
|
||||||
|
return new ConfigOptionsImpl(options);
|
||||||
|
}
|
||||||
|
@EqualsAndHashCode
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
class ConfigOptionsImpl implements ConfigOptions {
|
||||||
|
private final List<ConfigOption> options;
|
||||||
|
@Override
|
||||||
|
public List<ConfigOption> options() {
|
||||||
|
return new ArrayList<>(options);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public ConfigOptions merge(ConfigOptions other) {
|
||||||
|
List<ConfigOption> optionList = options();
|
||||||
|
other.options().stream()
|
||||||
|
.filter(o -> !optionList.contains(o))
|
||||||
|
.forEach(optionList::add);
|
||||||
|
return ConfigOptions.create(optionList);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public ConfigOptions prepend(ConfigOption configOption) {
|
||||||
|
List<ConfigOption> optionList = new ArrayList<>();
|
||||||
|
optionList.add(configOption);
|
||||||
|
options().stream()
|
||||||
|
.filter(o -> !optionList.contains(0))
|
||||||
|
.forEach(optionList::add);
|
||||||
|
return ConfigOptions.create(optionList);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean containsInstanceOf(Class<? extends ConfigOption> type) {
|
||||||
|
return options.stream()
|
||||||
|
.anyMatch(option ->
|
||||||
|
type.isAssignableFrom(option.getClass()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package net.kemitix.thorp.config;
|
||||||
|
|
||||||
|
import net.kemitix.thorp.domain.Sources;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public interface ConfigQuery {
|
||||||
|
|
||||||
|
static boolean showVersion(ConfigOptions configOptions) {
|
||||||
|
return configOptions.options().stream()
|
||||||
|
.anyMatch(configOption ->
|
||||||
|
configOption instanceof ConfigOption.Version);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean batchMode(ConfigOptions configOptions) {
|
||||||
|
return configOptions.options().stream()
|
||||||
|
.anyMatch(configOption ->
|
||||||
|
configOption instanceof ConfigOption.BatchMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean ignoreUserOptions(ConfigOptions configOptions) {
|
||||||
|
return configOptions.options().stream()
|
||||||
|
.anyMatch(configOption ->
|
||||||
|
configOption instanceof ConfigOption.IgnoreUserOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean ignoreGlobalOptions(ConfigOptions configOptions) {
|
||||||
|
return configOptions.options().stream()
|
||||||
|
.anyMatch(configOption ->
|
||||||
|
configOption instanceof ConfigOption.IgnoreGlobalOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Sources sources(ConfigOptions configOptions) {
|
||||||
|
List<Path> explicitPaths = configOptions.options().stream()
|
||||||
|
.filter(configOption ->
|
||||||
|
configOption instanceof ConfigOption.Source)
|
||||||
|
.map(ConfigOption.Source.class::cast)
|
||||||
|
.map(ConfigOption.Source::path)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (explicitPaths.isEmpty()) {
|
||||||
|
return Sources.create(Collections.singletonList(Paths.get(System.getenv("PWD"))));
|
||||||
|
}
|
||||||
|
return Sources.create(explicitPaths);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package net.kemitix.thorp.config;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ConfigValidation {
|
||||||
|
String errorMessage();
|
||||||
|
|
||||||
|
static ConfigValidation sourceIsNotADirectory(File file) {
|
||||||
|
return () -> "Source must be a directory: " + file;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConfigValidation sourceIsNotReadable(File file) {
|
||||||
|
return () -> "Source must be readable: " + file;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConfigValidation bucketNameIsMissing() {
|
||||||
|
return () -> "Bucket name is missing";
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConfigValidation errorReadingFile(File file, String message) {
|
||||||
|
return () -> String.format(
|
||||||
|
"Error reading file '%s': %s",
|
||||||
|
file, message);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package net.kemitix.thorp.config;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ConfigValidationException extends Exception {
|
||||||
|
private final List<ConfigValidation> errors;
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package net.kemitix.thorp.config;
|
||||||
|
|
||||||
|
import net.kemitix.thorp.domain.Bucket;
|
||||||
|
import net.kemitix.thorp.domain.Sources;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ConfigValidator {
|
||||||
|
|
||||||
|
static Configuration validateConfig(Configuration config) throws ConfigValidationException {
|
||||||
|
validateSources(config.sources);
|
||||||
|
validateBucket(config.bucket);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void validateBucket(Bucket bucket) throws ConfigValidationException {
|
||||||
|
if (bucket.name().isEmpty()) {
|
||||||
|
System.out.println("Bucket name is missing: " + bucket);
|
||||||
|
throw new ConfigValidationException(
|
||||||
|
Collections.singletonList(
|
||||||
|
ConfigValidation.bucketNameIsMissing()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void validateSources(Sources sources) throws ConfigValidationException {
|
||||||
|
List<ConfigValidation> errors = new ArrayList<>();
|
||||||
|
sources.paths().forEach(path ->
|
||||||
|
errors.addAll(validateAsSource(path.toFile())));
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
throw new ConfigValidationException(errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Collection<? extends ConfigValidation> validateAsSource(File file) {
|
||||||
|
if (!file.isDirectory())
|
||||||
|
return Collections.singletonList(
|
||||||
|
ConfigValidation.sourceIsNotADirectory(file));
|
||||||
|
if (!file.canRead())
|
||||||
|
return Collections.singletonList(
|
||||||
|
ConfigValidation.sourceIsNotReadable(file));
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package net.kemitix.thorp.config;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.With;
|
||||||
|
import net.kemitix.thorp.domain.Bucket;
|
||||||
|
import net.kemitix.thorp.domain.Filter;
|
||||||
|
import net.kemitix.thorp.domain.RemoteKey;
|
||||||
|
import net.kemitix.thorp.domain.Sources;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@With
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class Configuration {
|
||||||
|
public final Bucket bucket;
|
||||||
|
public final RemoteKey prefix;
|
||||||
|
public final List<Filter> filters;
|
||||||
|
public final boolean debug;
|
||||||
|
public final boolean batchMode;
|
||||||
|
public final int parallel;
|
||||||
|
public final Sources sources;
|
||||||
|
static Configuration create() {
|
||||||
|
return new Configuration(
|
||||||
|
Bucket.named(""),
|
||||||
|
RemoteKey.create(""),
|
||||||
|
Collections.emptyList(),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
Sources.emptySources
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package net.kemitix.thorp.config;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface ConfigurationBuilder {
|
||||||
|
static Configuration buildConfig(ConfigOptions priorityOpts) throws IOException, ConfigValidationException {
|
||||||
|
return new ConfigurationBuilderImpl().buildConfig(priorityOpts);
|
||||||
|
}
|
||||||
|
class ConfigurationBuilderImpl implements ConfigurationBuilder {
|
||||||
|
private static final String userConfigFile = ".config/thorp.conf";
|
||||||
|
private static final File globalConfig = new File("/etc/thorp.conf");
|
||||||
|
private static final File userHome = new File(System.getProperty("user.home"));
|
||||||
|
Configuration buildConfig(ConfigOptions priorityOpts) throws IOException, ConfigValidationException {
|
||||||
|
return ConfigValidator.validateConfig(
|
||||||
|
collateOptions(getConfigOptions(priorityOpts)));
|
||||||
|
}
|
||||||
|
private ConfigOptions getConfigOptions(ConfigOptions priorityOpts) throws IOException {
|
||||||
|
ConfigOptions sourceOpts = SourceConfigLoader.loadSourceConfigs(ConfigQuery.sources(priorityOpts));
|
||||||
|
ConfigOptions userOpts = userOptions(priorityOpts.merge(sourceOpts));
|
||||||
|
ConfigOptions globalOpts = globalOptions(priorityOpts.merge(sourceOpts.merge(userOpts)));
|
||||||
|
return priorityOpts.merge(sourceOpts.merge(userOpts.merge(globalOpts)));
|
||||||
|
}
|
||||||
|
private ConfigOptions userOptions(ConfigOptions priorityOpts) throws IOException {
|
||||||
|
if (ConfigQuery.ignoreUserOptions(priorityOpts)) {
|
||||||
|
return ConfigOptions.empty();
|
||||||
|
}
|
||||||
|
return ParseConfigFile.parseFile(
|
||||||
|
new File(userHome, userConfigFile));
|
||||||
|
}
|
||||||
|
private ConfigOptions globalOptions(ConfigOptions priorityOpts) throws IOException {
|
||||||
|
if (ConfigQuery.ignoreGlobalOptions(priorityOpts)) {
|
||||||
|
return ConfigOptions.empty();
|
||||||
|
}
|
||||||
|
return ParseConfigFile.parseFile(globalConfig);
|
||||||
|
}
|
||||||
|
private Configuration collateOptions(ConfigOptions configOptions) {
|
||||||
|
Configuration config = Configuration.create();
|
||||||
|
for (ConfigOption configOption : configOptions.options()) {
|
||||||
|
config = configOption.update(config);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package net.kemitix.thorp.config;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ParseConfigFile {
|
||||||
|
static ConfigOptions parseFile(File file) throws IOException {
|
||||||
|
if (file.exists()) {
|
||||||
|
System.out.println("Reading config: " + file);
|
||||||
|
ConfigOptions configOptions = new ParseConfigLines()
|
||||||
|
.parseLines(Files.readAllLines(file.toPath()));
|
||||||
|
return configOptions;
|
||||||
|
}
|
||||||
|
return ConfigOptions.empty();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package net.kemitix.thorp.config;
|
||||||
|
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class ParseConfigLines {
|
||||||
|
private static final String pattern = "^\\s*(?<key>\\S*)\\s*=\\s*(?<value>\\S*)\\s*$";
|
||||||
|
private static final Pattern format = Pattern.compile(pattern);
|
||||||
|
|
||||||
|
ConfigOptions parseLines(List<String> lines) {
|
||||||
|
return ConfigOptions.create(
|
||||||
|
lines.stream()
|
||||||
|
.flatMap(this::parseLine)
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stream<ConfigOption> parseLine(String str) {
|
||||||
|
Matcher m = format.matcher(str);
|
||||||
|
if (m.matches()) {
|
||||||
|
return parseKeyValue(m.group("key"), m.group("value"));
|
||||||
|
}
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stream<ConfigOption> parseKeyValue(String key, String value) {
|
||||||
|
switch (key.toLowerCase()) {
|
||||||
|
case "parallel":
|
||||||
|
return parseInt(value).map(ConfigOption::parallel);
|
||||||
|
case "source":
|
||||||
|
return Stream.of(ConfigOption.source(Paths.get(value)));
|
||||||
|
case "bucket":
|
||||||
|
return Stream.of(ConfigOption.bucket(value));
|
||||||
|
case "prefix":
|
||||||
|
return Stream.of(ConfigOption.prefix(value));
|
||||||
|
case "include":
|
||||||
|
return Stream.of(ConfigOption.include(value));
|
||||||
|
case "exclude":
|
||||||
|
return Stream.of(ConfigOption.exclude(value));
|
||||||
|
case "debug":
|
||||||
|
if (truthy(value))
|
||||||
|
return Stream.of(ConfigOption.debug());
|
||||||
|
// fall through to default
|
||||||
|
default:
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stream<Integer> parseInt(String value) {
|
||||||
|
try {
|
||||||
|
return Stream.of(Integer.parseInt(value));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean truthy(String value) {
|
||||||
|
switch (value.toLowerCase()) {
|
||||||
|
case "true":
|
||||||
|
case "yes":
|
||||||
|
case "enabled":
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package net.kemitix.thorp.config;
|
||||||
|
|
||||||
|
import net.kemitix.thorp.domain.Sources;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public interface SourceConfigLoader {
|
||||||
|
|
||||||
|
static ConfigOptions loadSourceConfigs(Sources sources) throws IOException {
|
||||||
|
// add each source as an option
|
||||||
|
ConfigOptions configOptions =
|
||||||
|
ConfigOptions.create(
|
||||||
|
sources.paths()
|
||||||
|
.stream()
|
||||||
|
.peek(path -> {
|
||||||
|
System.out.println("Using source: " + path);
|
||||||
|
})
|
||||||
|
.map(ConfigOption::source)
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
// add settings from each source as options
|
||||||
|
for (Path path : sources.paths()) {
|
||||||
|
configOptions = configOptions.merge(
|
||||||
|
ParseConfigFile.parseFile(
|
||||||
|
new File(path.toFile(), ".thorp.conf")));
|
||||||
|
}
|
||||||
|
return configOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,73 +0,0 @@
|
||||||
package net.kemitix.thorp.config
|
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.{Bucket, Filter, RemoteKey, Sources}
|
|
||||||
import zio.{UIO, ZIO}
|
|
||||||
|
|
||||||
trait Config {
|
|
||||||
val config: Config.Service
|
|
||||||
}
|
|
||||||
|
|
||||||
object Config {
|
|
||||||
|
|
||||||
trait Service {
|
|
||||||
def setConfiguration(config: Configuration): ZIO[Config, Nothing, Unit]
|
|
||||||
def isBatchMode: ZIO[Config, Nothing, Boolean]
|
|
||||||
def bucket: ZIO[Config, Nothing, Bucket]
|
|
||||||
def prefix: ZIO[Config, Nothing, RemoteKey]
|
|
||||||
def sources: ZIO[Config, Nothing, Sources]
|
|
||||||
def filters: ZIO[Config, Nothing, List[Filter]]
|
|
||||||
def parallel: UIO[Int]
|
|
||||||
}
|
|
||||||
|
|
||||||
trait Live extends Config {
|
|
||||||
|
|
||||||
val config: Service = new Service {
|
|
||||||
private val configRef = new AtomicReference(Configuration.empty)
|
|
||||||
override def setConfiguration(
|
|
||||||
config: Configuration): ZIO[Config, Nothing, Unit] =
|
|
||||||
UIO(configRef.set(config))
|
|
||||||
|
|
||||||
override def bucket: ZIO[Config, Nothing, Bucket] =
|
|
||||||
UIO(configRef.get).map(_.bucket)
|
|
||||||
|
|
||||||
override def sources: ZIO[Config, Nothing, Sources] =
|
|
||||||
UIO(configRef.get).map(_.sources)
|
|
||||||
|
|
||||||
override def prefix: ZIO[Config, Nothing, RemoteKey] =
|
|
||||||
UIO(configRef.get).map(_.prefix)
|
|
||||||
|
|
||||||
override def isBatchMode: ZIO[Config, Nothing, Boolean] =
|
|
||||||
UIO(configRef.get).map(_.batchMode)
|
|
||||||
|
|
||||||
override def filters: ZIO[Config, Nothing, List[Filter]] =
|
|
||||||
UIO(configRef.get).map(_.filters)
|
|
||||||
|
|
||||||
override def parallel: UIO[Int] = UIO(configRef.get).map(_.parallel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object Live extends Live
|
|
||||||
|
|
||||||
final def set(config: Configuration): ZIO[Config, Nothing, Unit] =
|
|
||||||
ZIO.accessM(_.config setConfiguration config)
|
|
||||||
|
|
||||||
final def batchMode: ZIO[Config, Nothing, Boolean] =
|
|
||||||
ZIO.accessM(_.config isBatchMode)
|
|
||||||
|
|
||||||
final def bucket: ZIO[Config, Nothing, Bucket] =
|
|
||||||
ZIO.accessM(_.config bucket)
|
|
||||||
|
|
||||||
final def prefix: ZIO[Config, Nothing, RemoteKey] =
|
|
||||||
ZIO.accessM(_.config prefix)
|
|
||||||
|
|
||||||
final def sources: ZIO[Config, Nothing, Sources] =
|
|
||||||
ZIO.accessM(_.config sources)
|
|
||||||
|
|
||||||
final def filters: ZIO[Config, Nothing, List[Filter]] =
|
|
||||||
ZIO.accessM(_.config filters)
|
|
||||||
|
|
||||||
final def parallel: ZIO[Config, Nothing, Int] =
|
|
||||||
ZIO.accessM(_.config parallel)
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
package net.kemitix.thorp.config
|
|
||||||
|
|
||||||
import java.nio.file.Path
|
|
||||||
|
|
||||||
import net.kemitix.thorp.config.Configuration._
|
|
||||||
import net.kemitix.thorp.domain
|
|
||||||
import net.kemitix.thorp.domain.RemoteKey
|
|
||||||
|
|
||||||
sealed trait ConfigOption {
|
|
||||||
def update(config: Configuration): Configuration
|
|
||||||
}
|
|
||||||
|
|
||||||
object ConfigOption {
|
|
||||||
|
|
||||||
final case class Source(path: Path) extends ConfigOption {
|
|
||||||
override def update(config: Configuration): Configuration =
|
|
||||||
sources.modify(_ + path)(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class Bucket(name: String) extends ConfigOption {
|
|
||||||
override def update(config: Configuration): Configuration =
|
|
||||||
if (config.bucket.name.isEmpty)
|
|
||||||
bucket.set(domain.Bucket(name))(config)
|
|
||||||
else
|
|
||||||
config
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class Prefix(path: String) extends ConfigOption {
|
|
||||||
override def update(config: Configuration): Configuration =
|
|
||||||
if (config.prefix.key.isEmpty)
|
|
||||||
prefix.set(RemoteKey(path))(config)
|
|
||||||
else
|
|
||||||
config
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class Include(pattern: String) extends ConfigOption {
|
|
||||||
override def update(config: Configuration): Configuration =
|
|
||||||
filters.modify(domain.Filter.Include(pattern) :: _)(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class Exclude(pattern: String) extends ConfigOption {
|
|
||||||
override def update(config: Configuration): Configuration =
|
|
||||||
filters.modify(domain.Filter.Exclude(pattern) :: _)(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class Debug() extends ConfigOption {
|
|
||||||
override def update(config: Configuration): Configuration =
|
|
||||||
debug.set(true)(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
case object Version extends ConfigOption {
|
|
||||||
override def update(config: Configuration): Configuration = config
|
|
||||||
}
|
|
||||||
|
|
||||||
case object BatchMode extends ConfigOption {
|
|
||||||
override def update(config: Configuration): Configuration =
|
|
||||||
batchMode.set(true)(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
case object IgnoreUserOptions extends ConfigOption {
|
|
||||||
override def update(config: Configuration): Configuration = config
|
|
||||||
}
|
|
||||||
|
|
||||||
case object IgnoreGlobalOptions extends ConfigOption {
|
|
||||||
override def update(config: Configuration): Configuration = config
|
|
||||||
}
|
|
||||||
|
|
||||||
case class Parallel(factor: Int) extends ConfigOption {
|
|
||||||
override def update(config: Configuration): Configuration =
|
|
||||||
parallel.set(factor)(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
package net.kemitix.thorp.config
|
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.SimpleLens
|
|
||||||
|
|
||||||
final case class ConfigOptions(options: List[ConfigOption]) {
|
|
||||||
|
|
||||||
def ++(other: ConfigOptions): ConfigOptions =
|
|
||||||
ConfigOptions.combine(this, other)
|
|
||||||
|
|
||||||
def ::(head: ConfigOption): ConfigOptions =
|
|
||||||
ConfigOptions(head :: options)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object ConfigOptions {
|
|
||||||
val defaultParallel = 1
|
|
||||||
def parallel(configOptions: ConfigOptions): Int = {
|
|
||||||
configOptions.options
|
|
||||||
.collectFirst {
|
|
||||||
case ConfigOption.Parallel(factor) => factor
|
|
||||||
}
|
|
||||||
.getOrElse(defaultParallel)
|
|
||||||
}
|
|
||||||
|
|
||||||
val empty: ConfigOptions = ConfigOptions(List.empty)
|
|
||||||
val options: SimpleLens[ConfigOptions, List[ConfigOption]] =
|
|
||||||
SimpleLens[ConfigOptions, List[ConfigOption]](_.options,
|
|
||||||
c => a => c.copy(options = a))
|
|
||||||
def combine(
|
|
||||||
x: ConfigOptions,
|
|
||||||
y: ConfigOptions
|
|
||||||
): ConfigOptions = ConfigOptions(x.options ++ y.options)
|
|
||||||
|
|
||||||
def contains[A1 >: ConfigOption](elem: A1)(
|
|
||||||
configOptions: ConfigOptions): Boolean =
|
|
||||||
configOptions.options.contains(elem)
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
package net.kemitix.thorp.config
|
|
||||||
|
|
||||||
import java.nio.file.Paths
|
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.Sources
|
|
||||||
|
|
||||||
trait ConfigQuery {
|
|
||||||
|
|
||||||
def showVersion(configOptions: ConfigOptions): Boolean =
|
|
||||||
ConfigOptions.contains(ConfigOption.Version)(configOptions)
|
|
||||||
|
|
||||||
def batchMode(configOptions: ConfigOptions): Boolean =
|
|
||||||
ConfigOptions.contains(ConfigOption.BatchMode)(configOptions)
|
|
||||||
|
|
||||||
def ignoreUserOptions(configOptions: ConfigOptions): Boolean =
|
|
||||||
ConfigOptions.contains(ConfigOption.IgnoreUserOptions)(configOptions)
|
|
||||||
|
|
||||||
def ignoreGlobalOptions(configOptions: ConfigOptions): Boolean =
|
|
||||||
ConfigOptions.contains(ConfigOption.IgnoreGlobalOptions)(configOptions)
|
|
||||||
|
|
||||||
def sources(configOptions: ConfigOptions): Sources = {
|
|
||||||
val explicitPaths = configOptions.options.flatMap {
|
|
||||||
case ConfigOption.Source(sourcePath) => List(sourcePath)
|
|
||||||
case _ => List.empty
|
|
||||||
}
|
|
||||||
val paths = explicitPaths match {
|
|
||||||
case List() => List(Paths.get(System.getenv("PWD")))
|
|
||||||
case _ => explicitPaths
|
|
||||||
}
|
|
||||||
Sources(paths)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object ConfigQuery extends ConfigQuery
|
|
|
@ -1,31 +0,0 @@
|
||||||
package net.kemitix.thorp.config
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
sealed trait ConfigValidation {
|
|
||||||
|
|
||||||
def errorMessage: String
|
|
||||||
}
|
|
||||||
|
|
||||||
object ConfigValidation {
|
|
||||||
|
|
||||||
case object SourceIsNotADirectory extends ConfigValidation {
|
|
||||||
override def errorMessage: String = "Source must be a directory"
|
|
||||||
}
|
|
||||||
|
|
||||||
case object SourceIsNotReadable extends ConfigValidation {
|
|
||||||
override def errorMessage: String = "Source must be readable"
|
|
||||||
}
|
|
||||||
|
|
||||||
case object BucketNameIsMissing extends ConfigValidation {
|
|
||||||
override def errorMessage: String = "Bucket name is missing"
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ErrorReadingFile(
|
|
||||||
file: File,
|
|
||||||
message: String
|
|
||||||
) extends ConfigValidation {
|
|
||||||
override def errorMessage: String = s"Error reading file '$file': $message"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
package net.kemitix.thorp.config
|
|
||||||
|
|
||||||
final case class ConfigValidationException(
|
|
||||||
errors: Seq[ConfigValidation]
|
|
||||||
) extends Exception
|
|
|
@ -1,56 +0,0 @@
|
||||||
package net.kemitix.thorp.config
|
|
||||||
|
|
||||||
import java.nio.file.Path
|
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.{Bucket, Sources}
|
|
||||||
import zio.IO
|
|
||||||
|
|
||||||
sealed trait ConfigValidator {
|
|
||||||
|
|
||||||
def validateConfig(
|
|
||||||
config: Configuration
|
|
||||||
): IO[List[ConfigValidation], Configuration] = IO.fromEither {
|
|
||||||
for {
|
|
||||||
_ <- validateSources(config.sources)
|
|
||||||
_ <- validateBucket(config.bucket)
|
|
||||||
} yield config
|
|
||||||
}
|
|
||||||
|
|
||||||
def validateBucket(bucket: Bucket): Either[List[ConfigValidation], Bucket] =
|
|
||||||
if (bucket.name.isEmpty) Left(List(ConfigValidation.BucketNameIsMissing))
|
|
||||||
else Right(bucket)
|
|
||||||
|
|
||||||
def validateSources(
|
|
||||||
sources: Sources): Either[List[ConfigValidation], Sources] =
|
|
||||||
sources.paths.foldLeft(List[ConfigValidation]()) {
|
|
||||||
(acc: List[ConfigValidation], path) =>
|
|
||||||
{
|
|
||||||
validateSource(path) match {
|
|
||||||
case Left(errors) => acc ++ errors
|
|
||||||
case Right(_) => acc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} match {
|
|
||||||
case Nil => Right(sources)
|
|
||||||
case errors => Left(errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
def validateSource(source: Path): Either[List[ConfigValidation], Path] =
|
|
||||||
for {
|
|
||||||
_ <- validateSourceIsDirectory(source)
|
|
||||||
_ <- validateSourceIsReadable(source)
|
|
||||||
} yield source
|
|
||||||
|
|
||||||
def validateSourceIsDirectory(
|
|
||||||
source: Path): Either[List[ConfigValidation], Path] =
|
|
||||||
if (source.toFile.isDirectory) Right(source)
|
|
||||||
else Left(List(ConfigValidation.SourceIsNotADirectory))
|
|
||||||
|
|
||||||
def validateSourceIsReadable(
|
|
||||||
source: Path): Either[List[ConfigValidation], Path] =
|
|
||||||
if (source.toFile.canRead) Right(source)
|
|
||||||
else Left(List(ConfigValidation.SourceIsNotReadable))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object ConfigValidator extends ConfigValidator
|
|
|
@ -1,41 +0,0 @@
|
||||||
package net.kemitix.thorp.config
|
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.{Bucket, Filter, RemoteKey, SimpleLens, Sources}
|
|
||||||
|
|
||||||
private[config] final case class Configuration(
|
|
||||||
bucket: Bucket,
|
|
||||||
prefix: RemoteKey,
|
|
||||||
filters: List[Filter],
|
|
||||||
debug: Boolean,
|
|
||||||
batchMode: Boolean,
|
|
||||||
parallel: Int,
|
|
||||||
sources: Sources
|
|
||||||
)
|
|
||||||
|
|
||||||
private[config] object Configuration {
|
|
||||||
val empty: Configuration = Configuration(
|
|
||||||
bucket = Bucket(""),
|
|
||||||
prefix = RemoteKey(""),
|
|
||||||
filters = List.empty,
|
|
||||||
debug = false,
|
|
||||||
batchMode = false,
|
|
||||||
parallel = 1,
|
|
||||||
sources = Sources(List.empty)
|
|
||||||
)
|
|
||||||
val sources: SimpleLens[Configuration, Sources] =
|
|
||||||
SimpleLens[Configuration, Sources](_.sources, b => a => b.copy(sources = a))
|
|
||||||
val bucket: SimpleLens[Configuration, Bucket] =
|
|
||||||
SimpleLens[Configuration, Bucket](_.bucket, b => a => b.copy(bucket = a))
|
|
||||||
val prefix: SimpleLens[Configuration, RemoteKey] =
|
|
||||||
SimpleLens[Configuration, RemoteKey](_.prefix, b => a => b.copy(prefix = a))
|
|
||||||
val filters: SimpleLens[Configuration, List[Filter]] =
|
|
||||||
SimpleLens[Configuration, List[Filter]](_.filters,
|
|
||||||
b => a => b.copy(filters = a))
|
|
||||||
val debug: SimpleLens[Configuration, Boolean] =
|
|
||||||
SimpleLens[Configuration, Boolean](_.debug, b => a => b.copy(debug = a))
|
|
||||||
val batchMode: SimpleLens[Configuration, Boolean] =
|
|
||||||
SimpleLens[Configuration, Boolean](_.batchMode,
|
|
||||||
b => a => b.copy(batchMode = a))
|
|
||||||
val parallel: SimpleLens[Configuration, Int] =
|
|
||||||
SimpleLens[Configuration, Int](_.parallel, b => a => b.copy(parallel = a))
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
package net.kemitix.thorp.config
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
import net.kemitix.thorp.filesystem.FileSystem
|
|
||||||
import zio.ZIO
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a configuration from settings in a file within the
|
|
||||||
* `source` directory and from supplied configuration options.
|
|
||||||
*/
|
|
||||||
trait ConfigurationBuilder {
|
|
||||||
|
|
||||||
private val userConfigFile = ".config/thorp.conf"
|
|
||||||
private val globalConfig = new File("/etc/thorp.conf")
|
|
||||||
private val userHome = new File(System.getProperty("user.home"))
|
|
||||||
|
|
||||||
def buildConfig(priorityOpts: ConfigOptions)
|
|
||||||
: ZIO[FileSystem, ConfigValidationException, Configuration] =
|
|
||||||
(getConfigOptions(priorityOpts).map(collateOptions) >>=
|
|
||||||
ConfigValidator.validateConfig)
|
|
||||||
.catchAll(errors => ZIO.fail(ConfigValidationException(errors)))
|
|
||||||
|
|
||||||
private def getConfigOptions(priorityOpts: ConfigOptions) =
|
|
||||||
for {
|
|
||||||
sourceOpts <- SourceConfigLoader.loadSourceConfigs(
|
|
||||||
ConfigQuery.sources(priorityOpts))
|
|
||||||
userOpts <- userOptions(priorityOpts ++ sourceOpts)
|
|
||||||
globalOpts <- globalOptions(priorityOpts ++ sourceOpts ++ userOpts)
|
|
||||||
} yield priorityOpts ++ sourceOpts ++ userOpts ++ globalOpts
|
|
||||||
|
|
||||||
private val emptyConfig = ZIO.succeed(ConfigOptions.empty)
|
|
||||||
|
|
||||||
private def userOptions(priorityOpts: ConfigOptions) =
|
|
||||||
if (ConfigQuery.ignoreUserOptions(priorityOpts)) emptyConfig
|
|
||||||
else ParseConfigFile.parseFile(new File(userHome, userConfigFile))
|
|
||||||
|
|
||||||
private def globalOptions(priorityOpts: ConfigOptions) =
|
|
||||||
if (ConfigQuery.ignoreGlobalOptions(priorityOpts)) emptyConfig
|
|
||||||
else ParseConfigFile.parseFile(globalConfig)
|
|
||||||
|
|
||||||
private def collateOptions(configOptions: ConfigOptions): Configuration =
|
|
||||||
ConfigOptions.options
|
|
||||||
.get(configOptions)
|
|
||||||
.foldLeft(Configuration.empty) { (config, configOption) =>
|
|
||||||
configOption.update(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object ConfigurationBuilder extends ConfigurationBuilder
|
|
|
@ -1,23 +0,0 @@
|
||||||
package net.kemitix.thorp.config
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
import net.kemitix.thorp.filesystem.FileSystem
|
|
||||||
import zio.{IO, RIO, ZIO}
|
|
||||||
|
|
||||||
trait ParseConfigFile {
|
|
||||||
|
|
||||||
def parseFile(
|
|
||||||
file: File): ZIO[FileSystem, Seq[ConfigValidation], ConfigOptions] =
|
|
||||||
(FileSystem.exists(file) >>= readLines(file) >>= ParseConfigLines.parseLines)
|
|
||||||
.catchAll(h =>
|
|
||||||
IO.fail(List(ConfigValidation.ErrorReadingFile(file, h.getMessage))))
|
|
||||||
|
|
||||||
private def readLines(file: File)(
|
|
||||||
exists: Boolean): RIO[FileSystem, Seq[String]] =
|
|
||||||
if (exists) FileSystem.lines(file)
|
|
||||||
else ZIO.succeed(Seq.empty)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object ParseConfigFile extends ParseConfigFile
|
|
|
@ -1,48 +0,0 @@
|
||||||
package net.kemitix.thorp.config
|
|
||||||
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
import net.kemitix.thorp.config.ConfigOption._
|
|
||||||
import zio.UIO
|
|
||||||
|
|
||||||
trait ParseConfigLines {
|
|
||||||
|
|
||||||
private val pattern = "^\\s*(?<key>\\S*)\\s*=\\s*(?<value>\\S*)\\s*$"
|
|
||||||
private val format = Pattern.compile(pattern)
|
|
||||||
|
|
||||||
def parseLines(lines: Seq[String]): UIO[ConfigOptions] =
|
|
||||||
UIO(ConfigOptions(lines.flatMap(parseLine).toList))
|
|
||||||
|
|
||||||
private def parseLine(str: String) =
|
|
||||||
format.matcher(str) match {
|
|
||||||
case m if m.matches => parseKeyValue(m.group("key"), m.group("value"))
|
|
||||||
case _ => List.empty
|
|
||||||
}
|
|
||||||
|
|
||||||
private def parseKeyValue(
|
|
||||||
key: String,
|
|
||||||
value: String
|
|
||||||
): List[ConfigOption] =
|
|
||||||
key.toLowerCase match {
|
|
||||||
case "parallel" => value.toIntOption.map(Parallel).toList
|
|
||||||
case "source" => List(Source(Paths.get(value)))
|
|
||||||
case "bucket" => List(Bucket(value))
|
|
||||||
case "prefix" => List(Prefix(value))
|
|
||||||
case "include" => List(Include(value))
|
|
||||||
case "exclude" => List(Exclude(value))
|
|
||||||
case "debug" => if (truthy(value)) List(Debug()) else List.empty
|
|
||||||
case _ => List.empty
|
|
||||||
}
|
|
||||||
|
|
||||||
private def truthy(value: String): Boolean =
|
|
||||||
value.toLowerCase match {
|
|
||||||
case "true" => true
|
|
||||||
case "yes" => true
|
|
||||||
case "enabled" => true
|
|
||||||
case _ => false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object ParseConfigLines extends ParseConfigLines
|
|
|
@ -1,26 +0,0 @@
|
||||||
package net.kemitix.thorp.config
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.Sources
|
|
||||||
import net.kemitix.thorp.filesystem.FileSystem
|
|
||||||
import zio.ZIO
|
|
||||||
|
|
||||||
trait SourceConfigLoader {
|
|
||||||
|
|
||||||
val thorpConfigFileName = ".thorp.conf"
|
|
||||||
|
|
||||||
def loadSourceConfigs(
|
|
||||||
sources: Sources): ZIO[FileSystem, Seq[ConfigValidation], ConfigOptions] =
|
|
||||||
ZIO
|
|
||||||
.foreach(sources.paths) { path =>
|
|
||||||
ParseConfigFile.parseFile(new File(path.toFile, thorpConfigFileName))
|
|
||||||
}
|
|
||||||
.map(_.foldLeft(ConfigOptions(sources.paths.map(ConfigOption.Source))) {
|
|
||||||
(acc, co) =>
|
|
||||||
acc ++ co
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object SourceConfigLoader extends SourceConfigLoader
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
package net.kemitix.thorp.config;
|
||||||
|
|
||||||
|
import net.kemitix.thorp.domain.Sources;
|
||||||
|
import net.kemitix.thorp.filesystem.TemporaryFolder;
|
||||||
|
import org.assertj.core.api.WithAssertions;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ConfigOptionTest
|
||||||
|
implements TemporaryFolder, WithAssertions {
|
||||||
|
@Test
|
||||||
|
@DisplayName("when more the one source then preserve their order")
|
||||||
|
public void whenMultiSource_PreserveOrder() {
|
||||||
|
withDirectory(path1 -> {
|
||||||
|
withDirectory(path2 -> {
|
||||||
|
ConfigOptions configOptions = ConfigOptions.create(
|
||||||
|
Arrays.asList(
|
||||||
|
ConfigOption.source(path1),
|
||||||
|
ConfigOption.source(path2),
|
||||||
|
ConfigOption.bucket("bucket"),
|
||||||
|
ConfigOption.ignoreGlobalOptions(),
|
||||||
|
ConfigOption.ignoreUserOptions()
|
||||||
|
));
|
||||||
|
List<Path> expected = Arrays.asList(path1, path2);
|
||||||
|
assertThatCode(() -> {
|
||||||
|
Configuration result =
|
||||||
|
ConfigurationBuilder.buildConfig(configOptions);
|
||||||
|
assertThat(result.sources.paths()).isEqualTo(expected);
|
||||||
|
}).doesNotThrowAnyException();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
package net.kemitix.thorp.config;
|
||||||
|
|
||||||
|
import net.kemitix.thorp.domain.Sources;
|
||||||
|
import org.assertj.core.api.WithAssertions;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ConfigQueryTest
|
||||||
|
implements WithAssertions {
|
||||||
|
@Nested
|
||||||
|
@DisplayName("show version")
|
||||||
|
public class ShowVersionTest{
|
||||||
|
@Test
|
||||||
|
@DisplayName("when set then show")
|
||||||
|
public void whenSet_thenShow() {
|
||||||
|
assertThat(ConfigQuery.showVersion(
|
||||||
|
ConfigOptions.create(
|
||||||
|
Collections.singletonList(
|
||||||
|
ConfigOption.version()))
|
||||||
|
)).isTrue();
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("when not set then do not show")
|
||||||
|
public void whenNotSet_thenDoNotShow() {
|
||||||
|
assertThat(ConfigQuery.showVersion(
|
||||||
|
ConfigOptions.create(
|
||||||
|
Collections.emptyList())
|
||||||
|
)).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
@DisplayName("batch mode")
|
||||||
|
public class BatchModeTest{
|
||||||
|
@Test
|
||||||
|
@DisplayName("when set then show")
|
||||||
|
public void whenSet_thenShow() {
|
||||||
|
assertThat(ConfigQuery.batchMode(
|
||||||
|
ConfigOptions.create(
|
||||||
|
Collections.singletonList(
|
||||||
|
ConfigOption.batchMode()))
|
||||||
|
)).isTrue();
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("when not set then do not show")
|
||||||
|
public void whenNotSet_thenDoNotShow() {
|
||||||
|
assertThat(ConfigQuery.batchMode(
|
||||||
|
ConfigOptions.create(
|
||||||
|
Collections.emptyList())
|
||||||
|
)).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
@DisplayName("ignore user options")
|
||||||
|
public class IgnoreUserOptionsTest{
|
||||||
|
@Test
|
||||||
|
@DisplayName("when set then show")
|
||||||
|
public void whenSet_thenShow() {
|
||||||
|
assertThat(ConfigQuery.ignoreUserOptions(
|
||||||
|
ConfigOptions.create(
|
||||||
|
Collections.singletonList(
|
||||||
|
ConfigOption.ignoreUserOptions()))
|
||||||
|
)).isTrue();
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("when not set then do not show")
|
||||||
|
public void whenNotSet_thenDoNotShow() {
|
||||||
|
assertThat(ConfigQuery.ignoreUserOptions(
|
||||||
|
ConfigOptions.create(
|
||||||
|
Collections.emptyList())
|
||||||
|
)).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
@DisplayName("ignore global options")
|
||||||
|
public class IgnoreGlobalOptionsTest{
|
||||||
|
@Test
|
||||||
|
@DisplayName("when set then show")
|
||||||
|
public void whenSet_thenShow() {
|
||||||
|
assertThat(ConfigQuery.ignoreGlobalOptions(
|
||||||
|
ConfigOptions.create(
|
||||||
|
Collections.singletonList(
|
||||||
|
ConfigOption.ignoreGlobalOptions()))
|
||||||
|
)).isTrue();
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("when not set then do not show")
|
||||||
|
public void whenNotSet_thenDoNotShow() {
|
||||||
|
assertThat(ConfigQuery.ignoreGlobalOptions(
|
||||||
|
ConfigOptions.create(
|
||||||
|
Collections.emptyList())
|
||||||
|
)).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
@DisplayName("source")
|
||||||
|
public class SourcesTest {
|
||||||
|
Path pathA = Paths.get("a-path");
|
||||||
|
Path pathB = Paths.get("b-path");
|
||||||
|
@Test
|
||||||
|
@DisplayName("when not set then use current directory")
|
||||||
|
public void whenNoSet_thenCurrentDir() {
|
||||||
|
Sources expected = Sources.create(
|
||||||
|
Collections.singletonList(
|
||||||
|
Paths.get(
|
||||||
|
System.getenv("PWD")
|
||||||
|
)));
|
||||||
|
assertThat(ConfigQuery.sources(ConfigOptions.empty()))
|
||||||
|
.isEqualTo(expected);
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("when one source then have one source")
|
||||||
|
public void whenOneSource_thenOneSource() {
|
||||||
|
List<Path> expected = Collections.singletonList(pathA);
|
||||||
|
assertThat(ConfigQuery.sources(
|
||||||
|
ConfigOptions.create(
|
||||||
|
Collections.singletonList(
|
||||||
|
ConfigOption.source(pathA)))).paths())
|
||||||
|
.isEqualTo(expected);
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("when two sources then have two sources")
|
||||||
|
public void whenTwoSources_thenTwoSources() {
|
||||||
|
List<Path> expected = Arrays.asList(pathA, pathB);
|
||||||
|
assertThat(
|
||||||
|
ConfigQuery.sources(
|
||||||
|
ConfigOptions.create(
|
||||||
|
Arrays.asList(
|
||||||
|
ConfigOption.source(pathA),
|
||||||
|
ConfigOption.source(pathB))
|
||||||
|
)).paths())
|
||||||
|
.isEqualTo(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,214 @@
|
||||||
|
package net.kemitix.thorp.config;
|
||||||
|
|
||||||
|
import net.kemitix.thorp.domain.Bucket;
|
||||||
|
import net.kemitix.thorp.domain.RemoteKey;
|
||||||
|
import net.kemitix.thorp.filesystem.TemporaryFolder;
|
||||||
|
import org.assertj.core.api.WithAssertions;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ConfigurationBuilderTest
|
||||||
|
implements WithAssertions {
|
||||||
|
Path pwd = Paths.get(System.getenv("PWD"));
|
||||||
|
Bucket aBucket = Bucket.named("aBucket");
|
||||||
|
ConfigOption coBucket = ConfigOption.bucket(aBucket.name());
|
||||||
|
String thorpConfigFileName = ".thorp.conf";
|
||||||
|
ConfigOptions configOptions(List<ConfigOption> options) {
|
||||||
|
List<ConfigOption> optionList = new ArrayList<>(options);
|
||||||
|
optionList.add(ConfigOption.ignoreUserOptions());
|
||||||
|
optionList.add(ConfigOption.ignoreGlobalOptions());
|
||||||
|
return ConfigOptions.create(optionList);
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("when no source then user current directory")
|
||||||
|
public void whenNoSource_thenUseCurrentDir() throws IOException, ConfigValidationException {
|
||||||
|
Configuration result = ConfigurationBuilder.buildConfig(
|
||||||
|
configOptions(Collections.singletonList(coBucket)));
|
||||||
|
assertThat(result.sources.paths()).containsExactly(pwd);
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
@DisplayName("default source")
|
||||||
|
public class DefaultSourceTests {
|
||||||
|
@Nested
|
||||||
|
@DisplayName("with .thorp.conf")
|
||||||
|
public class WithThorpConfTests implements TemporaryFolder {
|
||||||
|
@Test
|
||||||
|
@DisplayName("with settings")
|
||||||
|
public void WithSettingsTests() {
|
||||||
|
withDirectory(source -> {
|
||||||
|
//given
|
||||||
|
List<String> settings = Arrays.asList(
|
||||||
|
"bucket = a-bucket",
|
||||||
|
"prefix = a-prefix",
|
||||||
|
"include = an-inclusion",
|
||||||
|
"exclude = an-exclusion"
|
||||||
|
);
|
||||||
|
createFile(source, thorpConfigFileName, settings);
|
||||||
|
//when
|
||||||
|
Configuration result =
|
||||||
|
invoke(configOptions(Collections.singletonList(
|
||||||
|
ConfigOption.source(source))));
|
||||||
|
//then
|
||||||
|
assertThat(result.bucket).isEqualTo(Bucket.named("a-bucket"));
|
||||||
|
assertThat(result.prefix).isEqualTo(RemoteKey.create("a-prefix"));
|
||||||
|
assertThat(result.filters).hasSize(2)
|
||||||
|
.anySatisfy(filter ->
|
||||||
|
assertThat(filter.predicate()
|
||||||
|
.test("an-exclusion")).isTrue())
|
||||||
|
.anySatisfy(filter ->
|
||||||
|
assertThat(filter.predicate()
|
||||||
|
.test("an-inclusion")).isTrue());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
@DisplayName("single source")
|
||||||
|
public class SingleSourceTests implements TemporaryFolder {
|
||||||
|
@Test
|
||||||
|
@DisplayName("has single source")
|
||||||
|
public void hasSingleSource() {
|
||||||
|
withDirectory(aSource -> {
|
||||||
|
Configuration result =
|
||||||
|
invoke(
|
||||||
|
configOptions(Arrays.asList(
|
||||||
|
ConfigOption.source(aSource),
|
||||||
|
coBucket)));
|
||||||
|
assertThat(result.sources.paths()).containsExactly(aSource);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
@DisplayName("multiple sources")
|
||||||
|
public class MultipleSources implements TemporaryFolder {
|
||||||
|
@Test
|
||||||
|
@DisplayName("included in order")
|
||||||
|
public void hasBothSourcesInOrder() {
|
||||||
|
withDirectory(currentSource -> {
|
||||||
|
withDirectory(previousSource -> {
|
||||||
|
Configuration result =
|
||||||
|
invoke(configOptions(Arrays.asList(
|
||||||
|
ConfigOption.source(currentSource),
|
||||||
|
ConfigOption.source(previousSource),
|
||||||
|
coBucket)));
|
||||||
|
assertThat(result.sources.paths())
|
||||||
|
.containsExactly(
|
||||||
|
currentSource,
|
||||||
|
previousSource);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("config file includes another source")
|
||||||
|
public class ConfigLinkedSourceTests implements TemporaryFolder {
|
||||||
|
@Test
|
||||||
|
@DisplayName("include the linked source")
|
||||||
|
public void configIncludeOtherSource() {
|
||||||
|
withDirectory(currentSource -> {
|
||||||
|
withDirectory(previousSource -> {
|
||||||
|
createFile(currentSource,
|
||||||
|
thorpConfigFileName,
|
||||||
|
Collections.singletonList(
|
||||||
|
"source = " + previousSource));
|
||||||
|
Configuration result = invoke(configOptions(Arrays.asList(
|
||||||
|
ConfigOption.source(currentSource),
|
||||||
|
coBucket)));
|
||||||
|
assertThat(result.sources.paths())
|
||||||
|
.containsExactly(
|
||||||
|
currentSource,
|
||||||
|
previousSource);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("when linked source has config file")
|
||||||
|
public void whenSettingsFileInBothSources() {
|
||||||
|
withDirectory(currentSource -> {
|
||||||
|
withDirectory(previousSource -> {
|
||||||
|
//given
|
||||||
|
createFile(currentSource,
|
||||||
|
thorpConfigFileName,
|
||||||
|
Arrays.asList(
|
||||||
|
"source = " + previousSource,
|
||||||
|
"bucket = current-bucket",
|
||||||
|
"prefix = current-prefix",
|
||||||
|
"include = current-include",
|
||||||
|
"exclude = current-exclude"));
|
||||||
|
createFile(previousSource,
|
||||||
|
thorpConfigFileName,
|
||||||
|
Arrays.asList(
|
||||||
|
"bucket = previous-bucket",
|
||||||
|
"prefix = previous-prefix",
|
||||||
|
"include = previous-include",
|
||||||
|
"exclude = previous-exclude"));
|
||||||
|
//when
|
||||||
|
Configuration result = invoke(configOptions(Arrays.asList(
|
||||||
|
ConfigOption.source(currentSource),
|
||||||
|
coBucket)));
|
||||||
|
//then
|
||||||
|
assertThat(result.sources.paths()).containsExactly(currentSource, previousSource);
|
||||||
|
assertThat(result.bucket.name()).isEqualTo("current-bucket");
|
||||||
|
assertThat(result.prefix.key()).isEqualTo("current-prefix");
|
||||||
|
assertThat(result.filters).anyMatch(filter -> filter.predicate().test("current-include"));
|
||||||
|
assertThat(result.filters).anyMatch(filter -> filter.predicate().test("current-exclude"));
|
||||||
|
assertThat(result.filters).noneMatch(filter -> filter.predicate().test("previous-include"));
|
||||||
|
assertThat(result.filters).noneMatch(filter -> filter.predicate().test("previous-exclude"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
@DisplayName("linked source links to third source")
|
||||||
|
public class LinkedSourceLinkedSourceTests implements TemporaryFolder {
|
||||||
|
@Test
|
||||||
|
@DisplayName("ignore third source")
|
||||||
|
public void ignoreThirdSource() {
|
||||||
|
withDirectory(currentSource -> {
|
||||||
|
withDirectory(parentSource -> {
|
||||||
|
createFile(currentSource, thorpConfigFileName,
|
||||||
|
Collections.singletonList("source = " + parentSource));
|
||||||
|
withDirectory(grandParentSource -> {
|
||||||
|
createFile(parentSource, thorpConfigFileName,
|
||||||
|
Collections.singletonList("source = " + grandParentSource));
|
||||||
|
//when
|
||||||
|
Configuration result = invoke(configOptions(Arrays.asList(
|
||||||
|
ConfigOption.source(currentSource), coBucket)));
|
||||||
|
//then
|
||||||
|
assertThat(result.sources.paths())
|
||||||
|
.containsExactly(currentSource, parentSource)
|
||||||
|
.doesNotContain(grandParentSource);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("when batch mode option then batch mode in configuration")
|
||||||
|
public void whenBatchMode_thenBatchMode() {
|
||||||
|
Configuration result= invoke(configOptions(Arrays.asList(
|
||||||
|
ConfigOption.batchMode(),
|
||||||
|
coBucket)));
|
||||||
|
assertThat(result.batchMode).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Configuration invoke(ConfigOptions configOptions) {
|
||||||
|
try {
|
||||||
|
return ConfigurationBuilder.buildConfig(configOptions);
|
||||||
|
} catch (IOException | ConfigValidationException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package net.kemitix.thorp.config;
|
||||||
|
|
||||||
|
import net.kemitix.thorp.filesystem.TemporaryFolder;
|
||||||
|
import org.assertj.core.api.WithAssertions;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
public class ParseConfigFileTest
|
||||||
|
implements WithAssertions, TemporaryFolder {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("when file is missing then no options")
|
||||||
|
public void whenFileMissing_thenNoOptions() throws IOException {
|
||||||
|
assertThat(invoke(new File("/path/to/missing/file")))
|
||||||
|
.isEqualTo(ConfigOptions.empty());
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("when file is empty then no options")
|
||||||
|
public void whenEmptyFile_thenNoOptions() {
|
||||||
|
withDirectory(dir -> {
|
||||||
|
File file = createFile(dir, "empty-file", Collections.emptyList());
|
||||||
|
assertThat(invoke(file)).isEqualTo(ConfigOptions.empty());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("when no valid entried then no options")
|
||||||
|
public void whenNoValidEntries_thenNoOptions() {
|
||||||
|
withDirectory(dir -> {
|
||||||
|
File file = createFile(dir, "invalid-config",
|
||||||
|
Arrays.asList("no valid = config items", "invalid line"));
|
||||||
|
assertThat(invoke(file)).isEqualTo(ConfigOptions.empty());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("when file is valid then parse options")
|
||||||
|
public void whenValidFile_thenOptions() {
|
||||||
|
withDirectory(dir -> {
|
||||||
|
File file = createFile(dir, "simple-config", Arrays.asList(
|
||||||
|
"source = /path/to/source",
|
||||||
|
"bucket = bucket-name"));
|
||||||
|
assertThat(invoke(file)).isEqualTo(
|
||||||
|
ConfigOptions.create(
|
||||||
|
Arrays.asList(
|
||||||
|
ConfigOption.source(Paths.get("/path/to/source")),
|
||||||
|
ConfigOption.bucket("bucket-name"))));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigOptions invoke(File file) {
|
||||||
|
try {
|
||||||
|
return ParseConfigFile.parseFile(file);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
package net.kemitix.thorp.config;
|
||||||
|
|
||||||
|
import org.assertj.core.api.WithAssertions;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ParseConfigLinesTest
|
||||||
|
implements WithAssertions {
|
||||||
|
|
||||||
|
private final ParseConfigLines parser = new ParseConfigLines();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("source")
|
||||||
|
public void source() {
|
||||||
|
testParser("source = /path/to/source",
|
||||||
|
ConfigOption.source(Paths.get("/path/to/source")));
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("bucket")
|
||||||
|
public void bucket() {
|
||||||
|
testParser("bucket = bucket-name",
|
||||||
|
ConfigOption.bucket("bucket-name"));
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("prefix")
|
||||||
|
public void prefix() {
|
||||||
|
testParser("prefix = prefix/to/files",
|
||||||
|
ConfigOption.prefix("prefix/to/files"));
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("include")
|
||||||
|
public void include() {
|
||||||
|
testParser("include = path/to/include",
|
||||||
|
ConfigOption.include("path/to/include"));
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("exclude")
|
||||||
|
public void exclude() {
|
||||||
|
testParser("exclude = path/to/exclude",
|
||||||
|
ConfigOption.exclude("path/to/exclude"));
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("parallel")
|
||||||
|
public void parallel() {
|
||||||
|
testParser("parallel = 3",
|
||||||
|
ConfigOption.parallel(3));
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("parallel - invalid")
|
||||||
|
public void parallelInvalid() {
|
||||||
|
testParserIgnores("parallel = invalid");
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("debug - true")
|
||||||
|
public void debugTrue() {
|
||||||
|
testParser("debug = true",
|
||||||
|
ConfigOption.debug());
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("debug - false")
|
||||||
|
public void debugFalse() {
|
||||||
|
testParserIgnores("debug = false");
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("comment")
|
||||||
|
public void comment() {
|
||||||
|
testParserIgnores("# ignore name");
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("unrecognised option")
|
||||||
|
public void unrecognised() {
|
||||||
|
testParserIgnores("unsupported = option");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testParser(String line, ConfigOption configOption) {
|
||||||
|
assertThat(invoke(Collections.singletonList(line))).isEqualTo(
|
||||||
|
ConfigOptions.create(
|
||||||
|
Collections.singletonList(configOption)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testParserIgnores(String line) {
|
||||||
|
assertThat(invoke(Collections.singletonList(line))).isEqualTo(
|
||||||
|
ConfigOptions.create(
|
||||||
|
Collections.emptyList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConfigOptions invoke(List<String> lines) {
|
||||||
|
return parser.parseLines(lines);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,38 +0,0 @@
|
||||||
package net.kemitix.thorp.config
|
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.{Sources, TemporaryFolder}
|
|
||||||
import net.kemitix.thorp.filesystem.FileSystem
|
|
||||||
import org.scalatest.FunSpec
|
|
||||||
import zio.DefaultRuntime
|
|
||||||
|
|
||||||
class ConfigOptionTest extends FunSpec with TemporaryFolder {
|
|
||||||
|
|
||||||
describe("when more than one source") {
|
|
||||||
it("should preserve their order") {
|
|
||||||
withDirectory(path1 => {
|
|
||||||
withDirectory(path2 => {
|
|
||||||
val configOptions = ConfigOptions(
|
|
||||||
List[ConfigOption](
|
|
||||||
ConfigOption.Source(path1),
|
|
||||||
ConfigOption.Source(path2),
|
|
||||||
ConfigOption.Bucket("bucket"),
|
|
||||||
ConfigOption.IgnoreGlobalOptions,
|
|
||||||
ConfigOption.IgnoreUserOptions
|
|
||||||
))
|
|
||||||
val expected = Sources(List(path1, path2))
|
|
||||||
val result = invoke(configOptions)
|
|
||||||
assert(result.isRight, result)
|
|
||||||
assertResult(expected)(ConfigQuery.sources(configOptions))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def invoke(configOptions: ConfigOptions) = {
|
|
||||||
new DefaultRuntime {}.unsafeRunSync {
|
|
||||||
ConfigurationBuilder
|
|
||||||
.buildConfig(configOptions)
|
|
||||||
.provide(FileSystem.Live)
|
|
||||||
}.toEither
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
package net.kemitix.thorp.config
|
|
||||||
|
|
||||||
import java.nio.file.Paths
|
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.Sources
|
|
||||||
import org.scalatest.FreeSpec
|
|
||||||
|
|
||||||
class ConfigQueryTest extends FreeSpec {
|
|
||||||
|
|
||||||
"show version" - {
|
|
||||||
"when is set" - {
|
|
||||||
"should be true" in {
|
|
||||||
val result =
|
|
||||||
ConfigQuery.showVersion(ConfigOptions(List(ConfigOption.Version)))
|
|
||||||
assertResult(true)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"when not set" - {
|
|
||||||
"should be false" in {
|
|
||||||
val result = ConfigQuery.showVersion(ConfigOptions(List()))
|
|
||||||
assertResult(false)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"batch mode" - {
|
|
||||||
"when is set" - {
|
|
||||||
"should be true" in {
|
|
||||||
val result =
|
|
||||||
ConfigQuery.batchMode(ConfigOptions(List(ConfigOption.BatchMode)))
|
|
||||||
assertResult(true)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"when not set" - {
|
|
||||||
"should be false" in {
|
|
||||||
val result = ConfigQuery.batchMode(ConfigOptions(List()))
|
|
||||||
assertResult(false)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"ignore user options" - {
|
|
||||||
"when is set" - {
|
|
||||||
"should be true" in {
|
|
||||||
val result = ConfigQuery.ignoreUserOptions(
|
|
||||||
ConfigOptions(List(ConfigOption.IgnoreUserOptions)))
|
|
||||||
assertResult(true)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"when not set" - {
|
|
||||||
"should be false" in {
|
|
||||||
val result = ConfigQuery.ignoreUserOptions(ConfigOptions(List()))
|
|
||||||
assertResult(false)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"ignore global options" - {
|
|
||||||
"when is set" - {
|
|
||||||
"should be true" in {
|
|
||||||
val result = ConfigQuery.ignoreGlobalOptions(
|
|
||||||
ConfigOptions(List(ConfigOption.IgnoreGlobalOptions)))
|
|
||||||
assertResult(true)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"when not set" - {
|
|
||||||
"should be false" in {
|
|
||||||
val result = ConfigQuery.ignoreGlobalOptions(ConfigOptions(List()))
|
|
||||||
assertResult(false)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"sources" - {
|
|
||||||
val pathA = Paths.get("a-path")
|
|
||||||
val pathB = Paths.get("b-path")
|
|
||||||
"when not set" - {
|
|
||||||
"should have current dir" - {
|
|
||||||
val pwd = Paths.get(System.getenv("PWD"))
|
|
||||||
val expected = Sources(List(pwd))
|
|
||||||
val result = ConfigQuery.sources(ConfigOptions(List()))
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"when is set once" - {
|
|
||||||
"should have one source" in {
|
|
||||||
val expected = Sources(List(pathA))
|
|
||||||
val result =
|
|
||||||
ConfigQuery.sources(ConfigOptions(List(ConfigOption.Source(pathA))))
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"when is set twice" - {
|
|
||||||
"should have two sources" in {
|
|
||||||
val expected = Sources(List(pathA, pathB))
|
|
||||||
val result = ConfigQuery.sources(
|
|
||||||
ConfigOptions(
|
|
||||||
List(ConfigOption.Source(pathA), ConfigOption.Source(pathB))))
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,174 +0,0 @@
|
||||||
package net.kemitix.thorp.config
|
|
||||||
|
|
||||||
import java.nio.file.{Path, Paths}
|
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.Filter.{Exclude, Include}
|
|
||||||
import net.kemitix.thorp.domain._
|
|
||||||
import net.kemitix.thorp.filesystem.FileSystem
|
|
||||||
import org.scalatest.FunSpec
|
|
||||||
import zio.DefaultRuntime
|
|
||||||
|
|
||||||
class ConfigurationBuilderTest extends FunSpec with TemporaryFolder {
|
|
||||||
|
|
||||||
private val pwd: Path = Paths.get(System.getenv("PWD"))
|
|
||||||
private val aBucket = Bucket("aBucket")
|
|
||||||
private val coBucket: ConfigOption.Bucket = ConfigOption.Bucket(aBucket.name)
|
|
||||||
private val thorpConfigFileName = ".thorp.conf"
|
|
||||||
|
|
||||||
private def configOptions(options: ConfigOption*): ConfigOptions =
|
|
||||||
ConfigOptions(
|
|
||||||
List[ConfigOption](
|
|
||||||
ConfigOption.IgnoreUserOptions,
|
|
||||||
ConfigOption.IgnoreGlobalOptions
|
|
||||||
) ++ options)
|
|
||||||
|
|
||||||
describe("when no source") {
|
|
||||||
it("should use the current (PWD) directory") {
|
|
||||||
val expected = Right(Sources(List(pwd)))
|
|
||||||
val options = configOptions(coBucket)
|
|
||||||
val result = invoke(options).map(_.sources)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("a source") {
|
|
||||||
describe("with .thorp.conf") {
|
|
||||||
describe("with settings") {
|
|
||||||
withDirectory(source => {
|
|
||||||
writeFile(source,
|
|
||||||
thorpConfigFileName,
|
|
||||||
"bucket = a-bucket",
|
|
||||||
"prefix = a-prefix",
|
|
||||||
"include = an-inclusion",
|
|
||||||
"exclude = an-exclusion")
|
|
||||||
val result = invoke(configOptions(ConfigOption.Source(source)))
|
|
||||||
it("should have bucket") {
|
|
||||||
val expected = Right(Bucket("a-bucket"))
|
|
||||||
assertResult(expected)(result.map(_.bucket))
|
|
||||||
}
|
|
||||||
it("should have prefix") {
|
|
||||||
val expected = Right(RemoteKey("a-prefix"))
|
|
||||||
assertResult(expected)(result.map(_.prefix))
|
|
||||||
}
|
|
||||||
it("should have filters") {
|
|
||||||
val expected =
|
|
||||||
Right(
|
|
||||||
List[Filter](Exclude("an-exclusion"), Include("an-inclusion")))
|
|
||||||
assertResult(expected)(result.map(_.filters))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("when has a single source with no .thorp.conf") {
|
|
||||||
it("should only include the source once") {
|
|
||||||
withDirectory(aSource => {
|
|
||||||
val expected = Right(Sources(List(aSource)))
|
|
||||||
val options = configOptions(ConfigOption.Source(aSource), coBucket)
|
|
||||||
val result = invoke(options).map(_.sources)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("when has two sources") {
|
|
||||||
it("should include both sources in order") {
|
|
||||||
withDirectory(currentSource => {
|
|
||||||
withDirectory(previousSource => {
|
|
||||||
val expected = Right(List(currentSource, previousSource))
|
|
||||||
val options = configOptions(ConfigOption.Source(currentSource),
|
|
||||||
ConfigOption.Source(previousSource),
|
|
||||||
coBucket)
|
|
||||||
val result = invoke(options).map(_.sources.paths)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("when current source has .thorp.conf with source to another") {
|
|
||||||
it("should include both sources in order") {
|
|
||||||
withDirectory(currentSource => {
|
|
||||||
withDirectory(previousSource => {
|
|
||||||
writeFile(currentSource,
|
|
||||||
thorpConfigFileName,
|
|
||||||
s"source = $previousSource")
|
|
||||||
val expected = Right(List(currentSource, previousSource))
|
|
||||||
val options =
|
|
||||||
configOptions(ConfigOption.Source(currentSource), coBucket)
|
|
||||||
val result = invoke(options).map(_.sources.paths)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
describe("when settings are in current and previous") {
|
|
||||||
it("should include settings from only current") {
|
|
||||||
withDirectory(previousSource => {
|
|
||||||
withDirectory(currentSource => {
|
|
||||||
writeFile(
|
|
||||||
currentSource,
|
|
||||||
thorpConfigFileName,
|
|
||||||
s"source = $previousSource",
|
|
||||||
"bucket = current-bucket",
|
|
||||||
"prefix = current-prefix",
|
|
||||||
"include = current-include",
|
|
||||||
"exclude = current-exclude"
|
|
||||||
)
|
|
||||||
writeFile(previousSource,
|
|
||||||
thorpConfigFileName,
|
|
||||||
"bucket = previous-bucket",
|
|
||||||
"prefix = previous-prefix",
|
|
||||||
"include = previous-include",
|
|
||||||
"exclude = previous-exclude")
|
|
||||||
// should have both sources in order
|
|
||||||
val expectedSources =
|
|
||||||
Right(Sources(List(currentSource, previousSource)))
|
|
||||||
// should have bucket from current only
|
|
||||||
val expectedBuckets = Right(Bucket("current-bucket"))
|
|
||||||
// should have prefix from current only
|
|
||||||
val expectedPrefixes = Right(RemoteKey("current-prefix"))
|
|
||||||
// should have filters from both sources
|
|
||||||
val expectedFilters = Right(
|
|
||||||
List[Filter](Filter.Exclude("current-exclude"),
|
|
||||||
Filter.Include("current-include")))
|
|
||||||
val options = configOptions(ConfigOption.Source(currentSource))
|
|
||||||
val result = invoke(options)
|
|
||||||
assertResult(expectedSources)(result.map(_.sources))
|
|
||||||
assertResult(expectedBuckets)(result.map(_.bucket))
|
|
||||||
assertResult(expectedPrefixes)(result.map(_.prefix))
|
|
||||||
assertResult(expectedFilters)(result.map(_.filters))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe(
|
|
||||||
"when source has thorp.config source to another source that does the same") {
|
|
||||||
it("should only include first two sources") {
|
|
||||||
withDirectory(currentSource => {
|
|
||||||
withDirectory(parentSource => {
|
|
||||||
writeFile(currentSource,
|
|
||||||
thorpConfigFileName,
|
|
||||||
s"source = $parentSource")
|
|
||||||
withDirectory(grandParentSource => {
|
|
||||||
writeFile(parentSource,
|
|
||||||
thorpConfigFileName,
|
|
||||||
s"source = $grandParentSource")
|
|
||||||
val expected = Right(List(currentSource, parentSource))
|
|
||||||
val options =
|
|
||||||
configOptions(ConfigOption.Source(currentSource), coBucket)
|
|
||||||
val result = invoke(options).map(_.sources.paths)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def invoke(configOptions: ConfigOptions) = {
|
|
||||||
new DefaultRuntime {}.unsafeRunSync {
|
|
||||||
ConfigurationBuilder
|
|
||||||
.buildConfig(configOptions)
|
|
||||||
.provide(FileSystem.Live)
|
|
||||||
}.toEither
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
package net.kemitix.thorp.config
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.nio.file.Paths
|
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.TemporaryFolder
|
|
||||||
import net.kemitix.thorp.filesystem.FileSystem
|
|
||||||
import org.scalatest.FunSpec
|
|
||||||
import zio.DefaultRuntime
|
|
||||||
|
|
||||||
class ParseConfigFileTest extends FunSpec with TemporaryFolder {
|
|
||||||
|
|
||||||
private val empty = Right(ConfigOptions.empty)
|
|
||||||
|
|
||||||
describe("parse a missing file") {
|
|
||||||
val file = new File("/path/to/missing/file")
|
|
||||||
it("should return no options") {
|
|
||||||
assertResult(empty)(invoke(file))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("parse an empty file") {
|
|
||||||
it("should return no options") {
|
|
||||||
withDirectory(dir => {
|
|
||||||
val file = createFile(dir, "empty-file")
|
|
||||||
assertResult(empty)(invoke(file))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("parse a file with no valid entries") {
|
|
||||||
it("should return no options") {
|
|
||||||
withDirectory(dir => {
|
|
||||||
val file = createFile(dir, "invalid-config", "no valid = config items")
|
|
||||||
assertResult(empty)(invoke(file))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("parse a file with properties") {
|
|
||||||
it("should return some options") {
|
|
||||||
val expected = Right(
|
|
||||||
ConfigOptions(
|
|
||||||
List[ConfigOption](ConfigOption.Source(Paths.get("/path/to/source")),
|
|
||||||
ConfigOption.Bucket("bucket-name"))))
|
|
||||||
withDirectory(dir => {
|
|
||||||
val file = createFile(dir,
|
|
||||||
"simple-config",
|
|
||||||
"source = /path/to/source",
|
|
||||||
"bucket = bucket-name")
|
|
||||||
assertResult(expected)(invoke(file))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def invoke(file: File) = {
|
|
||||||
new DefaultRuntime {}.unsafeRunSync {
|
|
||||||
ParseConfigFile
|
|
||||||
.parseFile(file)
|
|
||||||
.provide(FileSystem.Live)
|
|
||||||
}.toEither
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
package net.kemitix.thorp.config
|
|
||||||
|
|
||||||
import java.nio.file.Paths
|
|
||||||
|
|
||||||
import org.scalatest.FunSpec
|
|
||||||
import zio.DefaultRuntime
|
|
||||||
|
|
||||||
class ParseConfigLinesTest extends FunSpec {
|
|
||||||
|
|
||||||
describe("parse single lines") {
|
|
||||||
describe("source") {
|
|
||||||
it("should parse") {
|
|
||||||
val expected =
|
|
||||||
Right(
|
|
||||||
ConfigOptions(
|
|
||||||
List(ConfigOption.Source(Paths.get("/path/to/source")))))
|
|
||||||
val result = invoke(List("source = /path/to/source"))
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("bucket") {
|
|
||||||
it("should parse") {
|
|
||||||
val expected =
|
|
||||||
Right(ConfigOptions(List(ConfigOption.Bucket("bucket-name"))))
|
|
||||||
val result = invoke(List("bucket = bucket-name"))
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("prefix") {
|
|
||||||
it("should parse") {
|
|
||||||
val expected =
|
|
||||||
Right(ConfigOptions(List(ConfigOption.Prefix("prefix/to/files"))))
|
|
||||||
val result = invoke(List("prefix = prefix/to/files"))
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("include") {
|
|
||||||
it("should parse") {
|
|
||||||
val expected =
|
|
||||||
Right(ConfigOptions(List(ConfigOption.Include("path/to/include"))))
|
|
||||||
val result = invoke(List("include = path/to/include"))
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("exclude") {
|
|
||||||
it("should parse") {
|
|
||||||
val expected =
|
|
||||||
Right(ConfigOptions(List(ConfigOption.Exclude("path/to/exclude"))))
|
|
||||||
val result = invoke(List("exclude = path/to/exclude"))
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("parallel") {
|
|
||||||
describe("when valid") {
|
|
||||||
it("should parse") {
|
|
||||||
val expected =
|
|
||||||
Right(ConfigOptions(List(ConfigOption.Parallel(3))))
|
|
||||||
val result = invoke(List("parallel = 3"))
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("when invalid") {
|
|
||||||
it("should ignore") {
|
|
||||||
val expected =
|
|
||||||
Right(ConfigOptions(List.empty))
|
|
||||||
val result = invoke(List("parallel = invalid"))
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("debug - true") {
|
|
||||||
it("should parse") {
|
|
||||||
val expected = Right(ConfigOptions(List(ConfigOption.Debug())))
|
|
||||||
val result = invoke(List("debug = true"))
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("debug - false") {
|
|
||||||
it("should parse") {
|
|
||||||
val expected = Right(ConfigOptions.empty)
|
|
||||||
val result = invoke(List("debug = false"))
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("comment line") {
|
|
||||||
it("should be ignored") {
|
|
||||||
val expected = Right(ConfigOptions.empty)
|
|
||||||
val result = invoke(List("# ignore me"))
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("unrecognised option") {
|
|
||||||
it("should be ignored") {
|
|
||||||
val expected = Right(ConfigOptions.empty)
|
|
||||||
val result = invoke(List("unsupported = option"))
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def invoke(lines: List[String]) = {
|
|
||||||
new DefaultRuntime {}.unsafeRunSync {
|
|
||||||
ParseConfigLines.parseLines(lines)
|
|
||||||
}.toEither
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,6 @@
|
||||||
package net.kemitix.thorp.console
|
package net.kemitix.thorp.console
|
||||||
|
|
||||||
|
import scala.jdk.CollectionConverters._
|
||||||
import net.kemitix.thorp.domain.StorageEvent.ActionSummary
|
import net.kemitix.thorp.domain.StorageEvent.ActionSummary
|
||||||
import net.kemitix.thorp.domain.Terminal._
|
import net.kemitix.thorp.domain.Terminal._
|
||||||
import net.kemitix.thorp.domain.{Bucket, RemoteKey, Sources}
|
import net.kemitix.thorp.domain.{Bucket, RemoteKey, Sources}
|
||||||
|
@ -27,7 +28,7 @@ object ConsoleOut {
|
||||||
prefix: RemoteKey,
|
prefix: RemoteKey,
|
||||||
sources: Sources
|
sources: Sources
|
||||||
) extends ConsoleOut {
|
) extends ConsoleOut {
|
||||||
private val sourcesList = sources.paths.mkString(", ")
|
private val sourcesList = sources.paths.asScala.mkString(", ")
|
||||||
override def en: String =
|
override def en: String =
|
||||||
List(s"Bucket: ${bucket.name}",
|
List(s"Bucket: ${bucket.name}",
|
||||||
s"Prefix: ${prefix.key}",
|
s"Prefix: ${prefix.key}",
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 299 KiB After Width: | Height: | Size: 249 KiB |
|
@ -1,6 +1,5 @@
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>net.kemitix.thorp</groupId>
|
<groupId>net.kemitix.thorp</groupId>
|
||||||
<artifactId>thorp-parent</artifactId>
|
<artifactId>thorp-parent</artifactId>
|
||||||
|
@ -12,48 +11,29 @@
|
||||||
<name>domain</name>
|
<name>domain</name>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- scala -->
|
<!-- mon -->
|
||||||
<dependency>
|
|
||||||
<groupId>org.scala-lang</groupId>
|
|
||||||
<artifactId>scala-library</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- eip-zio -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.kemitix</groupId>
|
<groupId>net.kemitix</groupId>
|
||||||
<artifactId>eip-zio_2.13</artifactId>
|
<artifactId>mon</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- zio -->
|
<!-- lombok -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.zio</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>zio_2.13</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
</dependency>
|
<optional>true</optional>
|
||||||
<dependency>
|
|
||||||
<groupId>dev.zio</groupId>
|
|
||||||
<artifactId>zio-streams_2.13</artifactId>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- scala - testing -->
|
<!-- testing -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.scalatest</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>scalatest_2.13</artifactId>
|
<artifactId>junit-jupiter</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.scalamock</groupId>
|
<groupId>org.assertj</groupId>
|
||||||
<artifactId>scalamock_2.13</artifactId>
|
<artifactId>assertj-core</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>net.alchim31.maven</groupId>
|
|
||||||
<artifactId>scala-maven-plugin</artifactId>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
|
|
||||||
</project>
|
</project>
|
107
domain/src/main/java/net/kemitix/thorp/domain/Action.java
Normal file
107
domain/src/main/java/net/kemitix/thorp/domain/Action.java
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public abstract class Action {
|
||||||
|
public final Bucket bucket;
|
||||||
|
public final Long size;
|
||||||
|
public final RemoteKey remoteKey;
|
||||||
|
|
||||||
|
public abstract String asString();
|
||||||
|
|
||||||
|
public static DoNothing doNothing(
|
||||||
|
Bucket body,
|
||||||
|
RemoteKey remoteKey,
|
||||||
|
Long size
|
||||||
|
) {
|
||||||
|
return new DoNothing(body, size, remoteKey);
|
||||||
|
}
|
||||||
|
public static ToUpload toUpload(
|
||||||
|
Bucket body,
|
||||||
|
LocalFile localFile,
|
||||||
|
Long size
|
||||||
|
) {
|
||||||
|
return new ToUpload(body, localFile, size);
|
||||||
|
}
|
||||||
|
public static ToCopy toCopy(
|
||||||
|
Bucket body,
|
||||||
|
RemoteKey sourceKey,
|
||||||
|
MD5Hash hash,
|
||||||
|
RemoteKey targetKey,
|
||||||
|
Long size
|
||||||
|
) {
|
||||||
|
return new ToCopy(body, sourceKey, hash, targetKey, size);
|
||||||
|
}
|
||||||
|
public static ToDelete toDelete(
|
||||||
|
Bucket body,
|
||||||
|
RemoteKey remoteKey,
|
||||||
|
Long size
|
||||||
|
) {
|
||||||
|
return new ToDelete(body, size, remoteKey);
|
||||||
|
}
|
||||||
|
public static class DoNothing extends Action {
|
||||||
|
private DoNothing(
|
||||||
|
Bucket body,
|
||||||
|
Long size,
|
||||||
|
RemoteKey remoteKey
|
||||||
|
) {
|
||||||
|
super(body, size, remoteKey);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public String asString() {
|
||||||
|
return String.format("Do nothing: %s", remoteKey.key());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static class ToUpload extends Action {
|
||||||
|
public final LocalFile localFile;
|
||||||
|
private ToUpload(
|
||||||
|
Bucket body,
|
||||||
|
LocalFile localFile,
|
||||||
|
Long size
|
||||||
|
) {
|
||||||
|
super(body, size, localFile.remoteKey);
|
||||||
|
this.localFile = localFile;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public String asString() {
|
||||||
|
return String.format("Upload: %s", localFile.remoteKey.key());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static class ToCopy extends Action {
|
||||||
|
public final RemoteKey sourceKey;
|
||||||
|
public final MD5Hash hash;
|
||||||
|
public final RemoteKey targetKey;
|
||||||
|
private ToCopy(
|
||||||
|
Bucket body,
|
||||||
|
RemoteKey sourceKey,
|
||||||
|
MD5Hash hash,
|
||||||
|
RemoteKey targetKey,
|
||||||
|
Long size
|
||||||
|
) {
|
||||||
|
super(body, size, targetKey);
|
||||||
|
this.sourceKey = sourceKey;
|
||||||
|
this.hash = hash;
|
||||||
|
this.targetKey = targetKey;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public String asString() {
|
||||||
|
return String.format("Copy: %s => %s",
|
||||||
|
sourceKey.key(), targetKey.key());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static class ToDelete extends Action {
|
||||||
|
private ToDelete(
|
||||||
|
Bucket body,
|
||||||
|
Long size,
|
||||||
|
RemoteKey remoteKey
|
||||||
|
) {
|
||||||
|
super(body, size, remoteKey);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public String asString() {
|
||||||
|
return String.format("Delete: %s", remoteKey.key());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
domain/src/main/java/net/kemitix/thorp/domain/Bucket.java
Normal file
15
domain/src/main/java/net/kemitix/thorp/domain/Bucket.java
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import net.kemitix.mon.TypeAlias;
|
||||||
|
|
||||||
|
public class Bucket extends TypeAlias<String> {
|
||||||
|
private Bucket(String value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
public String name() {
|
||||||
|
return getValue();
|
||||||
|
}
|
||||||
|
public static Bucket named(String name) {
|
||||||
|
return new Bucket(name);
|
||||||
|
}
|
||||||
|
}
|
27
domain/src/main/java/net/kemitix/thorp/domain/Counters.java
Normal file
27
domain/src/main/java/net/kemitix/thorp/domain/Counters.java
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.With;
|
||||||
|
|
||||||
|
@With
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class Counters {
|
||||||
|
public final int uploaded;
|
||||||
|
public final int deleted;
|
||||||
|
public final int copied;
|
||||||
|
public final int errors;
|
||||||
|
public static Counters empty = new Counters(0, 0, 0, 0);
|
||||||
|
public Counters incrementUploaded() {
|
||||||
|
return withUploaded(uploaded + 1);
|
||||||
|
}
|
||||||
|
public Counters incrementDeleted() {
|
||||||
|
return withDeleted(deleted + 1);
|
||||||
|
}
|
||||||
|
public Counters incrementCopied() {
|
||||||
|
return withCopied(copied + 1);
|
||||||
|
}
|
||||||
|
public Counters incrementErrors() {
|
||||||
|
return withErrors(errors + 1);
|
||||||
|
}
|
||||||
|
}
|
45
domain/src/main/java/net/kemitix/thorp/domain/Filter.java
Normal file
45
domain/src/main/java/net/kemitix/thorp/domain/Filter.java
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import net.kemitix.mon.TypeAlias;
|
||||||
|
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public interface Filter {
|
||||||
|
static Include include(String include) {
|
||||||
|
return Include.create(include);
|
||||||
|
}
|
||||||
|
static Exclude exclude(String exclude) {
|
||||||
|
return Exclude.create(exclude);
|
||||||
|
}
|
||||||
|
Predicate<String> predicate();
|
||||||
|
class Include extends TypeAlias<Pattern> implements Filter {
|
||||||
|
private Include(Pattern value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
public static Include create(String include) {
|
||||||
|
return new Include(Pattern.compile(include));
|
||||||
|
}
|
||||||
|
public static Include all() {
|
||||||
|
return Include.create(".*");
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public Predicate<String> predicate() {
|
||||||
|
return getValue().asPredicate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Exclude extends TypeAlias<Pattern> implements Filter {
|
||||||
|
private Exclude(Pattern value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
public static Exclude create(String exclude) {
|
||||||
|
return new Exclude(Pattern.compile(exclude));
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public Predicate<String> predicate() {
|
||||||
|
return getValue().asPredicate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ServiceLoader;
|
||||||
|
|
||||||
|
public interface HashGenerator {
|
||||||
|
|
||||||
|
HashType hashType();
|
||||||
|
String label();
|
||||||
|
String hashFile(Path path) throws IOException, NoSuchAlgorithmException;
|
||||||
|
Hashes hash(Path path) throws IOException, NoSuchAlgorithmException;
|
||||||
|
MD5Hash hashChunk(Path path, Long index, long partSize) throws IOException, NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
static List<HashGenerator> all() {
|
||||||
|
ServiceLoader<HashGenerator> hashGenerators = ServiceLoader.load(HashGenerator.class);
|
||||||
|
List<HashGenerator> list = new ArrayList<>();
|
||||||
|
for(HashGenerator hashGenerator: hashGenerators) {
|
||||||
|
list.add(hashGenerator);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
static HashGenerator generatorFor(String label) {
|
||||||
|
return all()
|
||||||
|
.stream()
|
||||||
|
.filter(g -> g.label().equals(label))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new RuntimeException("Unknown hash type: " + label));
|
||||||
|
}
|
||||||
|
static HashType typeFrom(String label) {
|
||||||
|
return generatorFor(label).hashType();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Hashes hashObject(Path path) throws IOException, NoSuchAlgorithmException {
|
||||||
|
List<Hashes> hashesList = new ArrayList<>();
|
||||||
|
for (HashGenerator hashGenerator : all()) {
|
||||||
|
hashesList.add(hashGenerator.hash(path));
|
||||||
|
}
|
||||||
|
return Hashes.mergeAll(hashesList);
|
||||||
|
}
|
||||||
|
}
|
12
domain/src/main/java/net/kemitix/thorp/domain/HashType.java
Normal file
12
domain/src/main/java/net/kemitix/thorp/domain/HashType.java
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
public class HashType {
|
||||||
|
public final String label;
|
||||||
|
public static HashType MD5 = new HashType("MD5");
|
||||||
|
public static HashType DUMMY = new HashType("Dummy"); // testing only
|
||||||
|
}
|
48
domain/src/main/java/net/kemitix/thorp/domain/Hashes.java
Normal file
48
domain/src/main/java/net/kemitix/thorp/domain/Hashes.java
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import net.kemitix.mon.TypeAlias;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class Hashes extends TypeAlias<Map<HashType, MD5Hash>> {
|
||||||
|
private Hashes() {
|
||||||
|
super(new HashMap<>());
|
||||||
|
}
|
||||||
|
public static Hashes create() {
|
||||||
|
return new Hashes();
|
||||||
|
}
|
||||||
|
public static Hashes create(HashType key, MD5Hash value) {
|
||||||
|
Hashes hashes = Hashes.create();
|
||||||
|
hashes.getValue().put(key, value);
|
||||||
|
return hashes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Hashes mergeAll(List<Hashes> hashesList) {
|
||||||
|
Hashes hashes = Hashes.create();
|
||||||
|
Map<HashType, MD5Hash> values = hashes.getValue();
|
||||||
|
hashesList.stream().map(TypeAlias::getValue).forEach(values::putAll);
|
||||||
|
return hashes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Hashes withKeyValue(HashType key, MD5Hash value) {
|
||||||
|
Hashes hashes = Hashes.create();
|
||||||
|
hashes.getValue().putAll(getValue());
|
||||||
|
hashes.getValue().put(key, value);
|
||||||
|
return hashes;
|
||||||
|
}
|
||||||
|
public Set<HashType> keys() {
|
||||||
|
return getValue().keySet();
|
||||||
|
}
|
||||||
|
public Collection<MD5Hash> values() {
|
||||||
|
return getValue().values();
|
||||||
|
}
|
||||||
|
public Optional<MD5Hash> get(HashType key) {
|
||||||
|
return Optional.ofNullable(getValue().get(key));
|
||||||
|
}
|
||||||
|
public Hashes merge(Hashes other) {
|
||||||
|
Hashes hashes = Hashes.create();
|
||||||
|
hashes.getValue().putAll(getValue());
|
||||||
|
hashes.getValue().putAll(other.getValue());
|
||||||
|
return hashes;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class HexEncoder {
|
||||||
|
|
||||||
|
public static String encode(byte[] bytes) {
|
||||||
|
return String.format("%0" + (bytes.length << 1) + "x",
|
||||||
|
new BigInteger(1, bytes))
|
||||||
|
.toUpperCase();
|
||||||
|
}
|
||||||
|
public static byte[] decode(String hexString) {
|
||||||
|
ByteArrayOutputStream bytes =
|
||||||
|
new ByteArrayOutputStream(hexString.length() * 4);
|
||||||
|
List<String> hexBytes = Arrays.stream(hexString
|
||||||
|
.replaceAll("[^0-9A-Fa-f]", "")
|
||||||
|
.split("")).collect(Collectors.toList());
|
||||||
|
sliding(hexBytes, 2)
|
||||||
|
.map(hb -> String.join("", hb))
|
||||||
|
.mapToInt(hex -> Integer.parseInt(hex, 16))
|
||||||
|
.forEach(bytes::write);
|
||||||
|
return bytes.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Stream<List<T>> sliding(List<T> list, int size) {
|
||||||
|
if(size > list.size())
|
||||||
|
return Stream.empty();
|
||||||
|
return IntStream.range(0, list.size()-size+1)
|
||||||
|
.filter(i -> i % size == 0)
|
||||||
|
.mapToObj(start -> list.subList(start, start+size));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import net.kemitix.mon.TypeAlias;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
public class LastModified extends TypeAlias<Instant> {
|
||||||
|
private LastModified(Instant value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
public static LastModified at(Instant instant) {
|
||||||
|
return new LastModified(instant);
|
||||||
|
}
|
||||||
|
public Instant at() {
|
||||||
|
return getValue();
|
||||||
|
}
|
||||||
|
}
|
31
domain/src/main/java/net/kemitix/thorp/domain/LocalFile.java
Normal file
31
domain/src/main/java/net/kemitix/thorp/domain/LocalFile.java
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class LocalFile {
|
||||||
|
public final File file;
|
||||||
|
public final File source;
|
||||||
|
public final Hashes hashes;
|
||||||
|
public final RemoteKey remoteKey;
|
||||||
|
public final Long length;
|
||||||
|
public static LocalFile create(
|
||||||
|
File file,
|
||||||
|
File source,
|
||||||
|
Hashes hashes,
|
||||||
|
RemoteKey remoteKey,
|
||||||
|
Long length
|
||||||
|
) {
|
||||||
|
return new LocalFile(file, source, hashes, remoteKey, length);
|
||||||
|
}
|
||||||
|
public boolean matchesHash(MD5Hash hash) {
|
||||||
|
return hashes.values().contains(hash);
|
||||||
|
}
|
||||||
|
public Optional<String> md5base64() {
|
||||||
|
return hashes.get(HashType.MD5).map(MD5Hash::hash64);
|
||||||
|
}
|
||||||
|
}
|
42
domain/src/main/java/net/kemitix/thorp/domain/MD5Hash.java
Normal file
42
domain/src/main/java/net/kemitix/thorp/domain/MD5Hash.java
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import net.kemitix.mon.TypeAlias;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
import java.util.stream.LongStream;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class MD5Hash extends TypeAlias<String> {
|
||||||
|
private MD5Hash(String value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
public static MD5Hash create(String in) {
|
||||||
|
return new MD5Hash(in);
|
||||||
|
}
|
||||||
|
public static MD5Hash fromDigest(byte[] digest) {
|
||||||
|
return new MD5Hash(digestAsString(digest));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String digestAsString(byte[] digest) {
|
||||||
|
return IntStream.range(0, digest.length)
|
||||||
|
.map(i -> digest[i])
|
||||||
|
.mapToObj(b -> String.format("%02x", b))
|
||||||
|
.map(s -> s.substring(s.length() - 2, s.length()))
|
||||||
|
.flatMap(x -> Stream.of(x.split("")))
|
||||||
|
.collect(Collectors.joining());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String hash() {
|
||||||
|
return QuoteStripper.stripQuotes(String.join("", getValue()));
|
||||||
|
}
|
||||||
|
public byte[] digest() {
|
||||||
|
return HexEncoder.decode(hash());
|
||||||
|
}
|
||||||
|
public String hash64() {
|
||||||
|
return Base64.getEncoder().encodeToString(digest());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
public interface MD5HashData {
|
||||||
|
|
||||||
|
class Root {
|
||||||
|
public static final String hashString = "a3a6ac11a0eb577b81b3bb5c95cc8a6e";
|
||||||
|
public static final MD5Hash hash = MD5Hash.create(hashString);
|
||||||
|
public static final String base64 = "o6asEaDrV3uBs7tclcyKbg==";
|
||||||
|
public static final RemoteKey remoteKey = RemoteKey.create("root-file");
|
||||||
|
public static final Long size = 55L;
|
||||||
|
}
|
||||||
|
class Leaf {
|
||||||
|
public static final String hashString = "208386a650bdec61cfcd7bd8dcb6b542";
|
||||||
|
public static final MD5Hash hash = MD5Hash.create(hashString);
|
||||||
|
public static final String base64 = "IIOGplC97GHPzXvY3La1Qg==";
|
||||||
|
public static final RemoteKey remoteKey = RemoteKey.create("subdir/leaf-file");
|
||||||
|
public static final Long size = 58L;
|
||||||
|
}
|
||||||
|
class BigFile {
|
||||||
|
public static final String hashString = "b1ab1f7680138e6db7309200584e35d8";
|
||||||
|
public static final MD5Hash hash = MD5Hash.create(hashString);
|
||||||
|
|
||||||
|
public static class Part1 {
|
||||||
|
public static final int offset = 0;
|
||||||
|
public static final int size = 1048576;
|
||||||
|
public static final String hashString = "39d4a9c78b9cfddf6d241a201a4ab726";
|
||||||
|
public static final MD5Hash hash = MD5Hash.create(hashString);
|
||||||
|
}
|
||||||
|
public static class Part2 {
|
||||||
|
public static final int offset = 1048576;
|
||||||
|
public static final int size = 1048576;
|
||||||
|
public static final String hashString = "af5876f3a3bc6e66f4ae96bb93d8dae0";
|
||||||
|
public static final MD5Hash hash = MD5Hash.create(hashString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
domain/src/main/java/net/kemitix/thorp/domain/MapView.java
Normal file
40
domain/src/main/java/net/kemitix/thorp/domain/MapView.java
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class MapView<K, V> {
|
||||||
|
private final Map<K, V> map;
|
||||||
|
public static <K, V> MapView<K, V> empty(){
|
||||||
|
return MapView.of(new HashMap<>());
|
||||||
|
}
|
||||||
|
public static <K, V> MapView<K, V> of(Map<K, V> map) {
|
||||||
|
return new MapView<>(map);
|
||||||
|
}
|
||||||
|
public boolean contains(K key) {
|
||||||
|
return map.containsKey(key);
|
||||||
|
}
|
||||||
|
public Optional<V> get(K key) {
|
||||||
|
return Optional.ofNullable(map.get(key));
|
||||||
|
}
|
||||||
|
public Collection<K> keys() { return map.keySet(); }
|
||||||
|
public Optional<Tuple<K, V>> collectFirst(BiFunction<K, V, Boolean> test) {
|
||||||
|
return map.entrySet().stream()
|
||||||
|
.filter(e -> test.apply(e.getKey(), e.getValue()))
|
||||||
|
.findFirst()
|
||||||
|
.map(e -> Tuple.create(e.getKey(), e.getValue()));
|
||||||
|
}
|
||||||
|
public Map<K, V> asMap() {
|
||||||
|
return new HashMap<>(map);
|
||||||
|
}
|
||||||
|
public int size() {
|
||||||
|
return map.size();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public interface QuoteStripper {
|
||||||
|
static String stripQuotes(String in) {
|
||||||
|
return Arrays.stream(in.split(""))
|
||||||
|
.filter(c -> !c.equals("\""))
|
||||||
|
.collect(Collectors.joining());
|
||||||
|
}
|
||||||
|
}
|
49
domain/src/main/java/net/kemitix/thorp/domain/RemoteKey.java
Normal file
49
domain/src/main/java/net/kemitix/thorp/domain/RemoteKey.java
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import net.kemitix.mon.TypeAlias;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class RemoteKey extends TypeAlias<String> {
|
||||||
|
private RemoteKey(String value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
public static RemoteKey create(String key) {
|
||||||
|
return new RemoteKey(key);
|
||||||
|
}
|
||||||
|
public String key() {
|
||||||
|
return getValue();
|
||||||
|
}
|
||||||
|
public Optional<File> asFile(Path source, RemoteKey prefix) {
|
||||||
|
if (key().length() == 0 || !key().startsWith(prefix.key())) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(
|
||||||
|
source.resolve(relativeTo(prefix))
|
||||||
|
.toFile());
|
||||||
|
}
|
||||||
|
public Path relativeTo(RemoteKey prefix) {
|
||||||
|
if (prefix.key().equals("")) {
|
||||||
|
return Paths.get(key());
|
||||||
|
}
|
||||||
|
return Paths.get(prefix.key()).relativize(Paths.get(key()));
|
||||||
|
}
|
||||||
|
public RemoteKey resolve(String path) {
|
||||||
|
return RemoteKey.create(
|
||||||
|
Stream.of(key(), path)
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.collect(Collectors.joining("/")));
|
||||||
|
}
|
||||||
|
public static RemoteKey fromSourcePath(Path source, Path path) {
|
||||||
|
return RemoteKey.create(
|
||||||
|
source.relativize(path).toString());
|
||||||
|
}
|
||||||
|
public static RemoteKey from(Path source, RemoteKey prefix, File file) {
|
||||||
|
return prefix.resolve(source.relativize(file.toPath()).toString());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class RemoteObjects {
|
||||||
|
public final MapView<MD5Hash, RemoteKey> byHash;
|
||||||
|
public final MapView<RemoteKey, MD5Hash> byKey;
|
||||||
|
public static final RemoteObjects empty =
|
||||||
|
new RemoteObjects(MapView.empty(), MapView.empty());
|
||||||
|
public static RemoteObjects create(
|
||||||
|
Map<MD5Hash, RemoteKey> byHash,
|
||||||
|
Map<RemoteKey, MD5Hash> byKey
|
||||||
|
) {
|
||||||
|
return new RemoteObjects(
|
||||||
|
MapView.of(byHash),
|
||||||
|
MapView.of(byKey)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
public boolean remoteKeyExists(RemoteKey remoteKey) {
|
||||||
|
return byKey.contains(remoteKey);
|
||||||
|
}
|
||||||
|
public boolean remoteMatchesLocalFile(LocalFile localFile) {
|
||||||
|
return byKey.get(localFile.remoteKey)
|
||||||
|
.map(localFile::matchesHash)
|
||||||
|
.orElse(false);
|
||||||
|
}
|
||||||
|
public Optional<Tuple<RemoteKey, MD5Hash>> remoteHasHash(Hashes hashes) {
|
||||||
|
return byHash.collectFirst(
|
||||||
|
(hash, key) -> hashes.values().contains(hash))
|
||||||
|
.map(Tuple::swap);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
public class SizeTranslation {
|
||||||
|
static long kbLimit = 10240L;
|
||||||
|
static long mbLimit = kbLimit * 1024;
|
||||||
|
static long gbLimit = mbLimit * 1024;
|
||||||
|
public static String sizeInEnglish(long length) {
|
||||||
|
double bytes = length;
|
||||||
|
if (length > gbLimit) {
|
||||||
|
return String.format("%.3fGb", bytes / 1024 / 1024 / 1024);
|
||||||
|
}
|
||||||
|
if (length > mbLimit) {
|
||||||
|
return String.format("%.2fMb", bytes / 1024 / 1024);
|
||||||
|
}
|
||||||
|
if (length > kbLimit) {
|
||||||
|
return String.format("%.0fKb", bytes / 1024);
|
||||||
|
}
|
||||||
|
return String.format("%db", length);
|
||||||
|
}
|
||||||
|
}
|
36
domain/src/main/java/net/kemitix/thorp/domain/Sources.java
Normal file
36
domain/src/main/java/net/kemitix/thorp/domain/Sources.java
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import net.kemitix.mon.TypeAlias;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Sources extends TypeAlias<List<Path>> {
|
||||||
|
private Sources(List<Path> value) { super(value); }
|
||||||
|
public static final Sources emptySources = new Sources(Collections.emptyList());
|
||||||
|
public static Sources create(List<Path> paths) {
|
||||||
|
return new Sources(paths);
|
||||||
|
}
|
||||||
|
public List<Path> paths() {
|
||||||
|
return new ArrayList<>(getValue());
|
||||||
|
}
|
||||||
|
public Path forPath(Path path) {
|
||||||
|
return getValue().stream()
|
||||||
|
.filter(path::startsWith)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() ->
|
||||||
|
new RuntimeException(
|
||||||
|
"Path is not within any known source"));
|
||||||
|
}
|
||||||
|
public Sources append(Path path) {
|
||||||
|
return append(Collections.singletonList(path));
|
||||||
|
}
|
||||||
|
public Sources append(List<Path> paths) {
|
||||||
|
List<Path> collected = new ArrayList<>();
|
||||||
|
collected.addAll(getValue());
|
||||||
|
collected.addAll(paths);
|
||||||
|
return Sources.create(collected);
|
||||||
|
}
|
||||||
|
}
|
101
domain/src/main/java/net/kemitix/thorp/domain/StorageEvent.java
Normal file
101
domain/src/main/java/net/kemitix/thorp/domain/StorageEvent.java
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
public class StorageEvent {
|
||||||
|
public static DoNothingEvent doNothingEvent(RemoteKey remoteKey) {
|
||||||
|
return new DoNothingEvent(remoteKey);
|
||||||
|
}
|
||||||
|
public static StorageEvent copyEvent(RemoteKey sourceKey, RemoteKey targetKey) {
|
||||||
|
return new CopyEvent(sourceKey, targetKey);
|
||||||
|
}
|
||||||
|
public static UploadEvent uploadEvent(RemoteKey remoteKey, MD5Hash md5Hash) {
|
||||||
|
return new UploadEvent(remoteKey, md5Hash);
|
||||||
|
}
|
||||||
|
public static DeleteEvent deleteEvent(RemoteKey remoteKey) {
|
||||||
|
return new DeleteEvent(remoteKey);
|
||||||
|
}
|
||||||
|
public static ErrorEvent errorEvent(ActionSummary action, RemoteKey remoteKey, Throwable e) {
|
||||||
|
return new ErrorEvent(action, remoteKey, e);
|
||||||
|
}
|
||||||
|
public static ShutdownEvent shutdownEvent() {
|
||||||
|
return new ShutdownEvent();
|
||||||
|
}
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public static class DoNothingEvent extends StorageEvent {
|
||||||
|
public final RemoteKey remoteKey;
|
||||||
|
}
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public static class CopyEvent extends StorageEvent {
|
||||||
|
public final RemoteKey sourceKey;
|
||||||
|
public final RemoteKey targetKey;
|
||||||
|
}
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public static class UploadEvent extends StorageEvent {
|
||||||
|
public final RemoteKey remoteKey;
|
||||||
|
public final MD5Hash md5Hash;
|
||||||
|
}
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public static class DeleteEvent extends StorageEvent {
|
||||||
|
public final RemoteKey remoteKey;
|
||||||
|
}
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public static class ErrorEvent extends StorageEvent {
|
||||||
|
public final ActionSummary action;
|
||||||
|
public final RemoteKey remoteKey;
|
||||||
|
public final Throwable e;
|
||||||
|
}
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public static class ShutdownEvent extends StorageEvent {}
|
||||||
|
public interface ActionSummary {
|
||||||
|
String name();
|
||||||
|
String keys();
|
||||||
|
static Copy copy(String keys) {
|
||||||
|
return new Copy(keys);
|
||||||
|
}
|
||||||
|
static Upload upload(String keys) {
|
||||||
|
return new Upload(keys);
|
||||||
|
}
|
||||||
|
static Delete delete(String keys) {
|
||||||
|
return new Delete(keys);
|
||||||
|
}
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
class Copy implements ActionSummary {
|
||||||
|
public final String keys;
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "Copy";
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public String keys() {
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
class Upload implements ActionSummary {
|
||||||
|
public final String keys;
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "Upload";
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public String keys() {
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
class Delete implements ActionSummary {
|
||||||
|
public final String keys;
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "Delete";
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public String keys() {
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
200
domain/src/main/java/net/kemitix/thorp/domain/Terminal.java
Normal file
200
domain/src/main/java/net/kemitix/thorp/domain/Terminal.java
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
public class Terminal {
|
||||||
|
|
||||||
|
public static String esc = "\u001B";
|
||||||
|
public static String csi = esc + "[";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear from cursor to end of screen.
|
||||||
|
*/
|
||||||
|
public static String eraseToEndOfScreen = csi + "0J";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear from cursor to beginning of screen.
|
||||||
|
*/
|
||||||
|
public static String eraseToStartOfScreen = csi + "1J";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear screen and move cursor to top-left.
|
||||||
|
*
|
||||||
|
* On DOS the "2J" command also moves to 1,1, so we force that behaviour for all.
|
||||||
|
*/
|
||||||
|
public static String eraseScreen = csi + "2J" + cursorPosition(1, 1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear screen and scrollback buffer then move cursor to top-left.
|
||||||
|
*
|
||||||
|
* Anticipate that same DOS behaviour here, and to maintain consistency with {@link #eraseScreen}.
|
||||||
|
*/
|
||||||
|
public static String eraseScreenAndBuffer = csi + "3J";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the terminal line to the right of the cursor.
|
||||||
|
*
|
||||||
|
* Does not move the cursor.
|
||||||
|
*/
|
||||||
|
public static String eraseLineForward = csi + "0K";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the terminal line to the left of the cursor.
|
||||||
|
*
|
||||||
|
* Does not move the cursor.
|
||||||
|
*/
|
||||||
|
public static String eraseLineBack = csi + "1K";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the whole terminal line.
|
||||||
|
*
|
||||||
|
* Does not move the cursor.
|
||||||
|
*/
|
||||||
|
public static String eraseLine = csi + "2K";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the cursor position/state.
|
||||||
|
*/
|
||||||
|
public static String saveCursorPosition = csi + "s";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the cursor position/state.
|
||||||
|
*/
|
||||||
|
public static String restoreCursorPosition = csi + "u";
|
||||||
|
public static String enableAlternateBuffer = csi + "?1049h";
|
||||||
|
public static String disableAlternateBuffer = csi + "?1049l";
|
||||||
|
|
||||||
|
private static Map<Integer, String> getSubBars() {
|
||||||
|
Map<Integer, String> subBars = new HashMap<>();
|
||||||
|
subBars.put(0, " ");
|
||||||
|
subBars.put(1, "▏");
|
||||||
|
subBars.put(2, "▎");
|
||||||
|
subBars.put(3, "▍");
|
||||||
|
subBars.put(4, "▌");
|
||||||
|
subBars.put(5, "▋");
|
||||||
|
subBars.put(6, "▊");
|
||||||
|
subBars.put(7, "▉");
|
||||||
|
return subBars;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the cursor up, default 1 line.
|
||||||
|
*
|
||||||
|
* Stops at the edge of the screen.
|
||||||
|
*/
|
||||||
|
public static String cursorUp(int lines) {
|
||||||
|
return csi + lines + "A";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the cursor down, default 1 line.
|
||||||
|
*
|
||||||
|
* Stops at the edge of the screen.
|
||||||
|
*/
|
||||||
|
public static String cursorDown(int lines) {
|
||||||
|
return csi + lines + "B";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the cursor forward, default 1 column.
|
||||||
|
*
|
||||||
|
* Stops at the edge of the screen.
|
||||||
|
*/
|
||||||
|
public static String cursorForward(int cols) {
|
||||||
|
return csi + cols + "C";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the cursor back, default 1 column,
|
||||||
|
*
|
||||||
|
* Stops at the edge of the screen.
|
||||||
|
*/
|
||||||
|
public static String cursorBack(int cols) {
|
||||||
|
return csi + cols + "D";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the cursor to the beginning of the line, default 1, down.
|
||||||
|
*/
|
||||||
|
public static String cursorNextLine(int lines) {
|
||||||
|
return csi + lines + "E";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the cursor to the beginning of the line, default 1, up.
|
||||||
|
*/
|
||||||
|
public static String cursorPrevLine(int lines) {
|
||||||
|
return csi + lines + "F";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the cursor to the column on the current line.
|
||||||
|
*/
|
||||||
|
public static String cursorHorizAbs(int col) {
|
||||||
|
return csi + col + "G";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the cursor to the position on screen (1,1 is the top-left).
|
||||||
|
*/
|
||||||
|
public static String cursorPosition(int row, int col) {
|
||||||
|
return csi + row + ";" + col + "H";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll page up, default 1, lines.
|
||||||
|
*/
|
||||||
|
public static String scrollUp(int lines) {
|
||||||
|
return csi + lines + "S";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll page down, default 1, lines.
|
||||||
|
*/
|
||||||
|
public static String scrollDown(int lines) {
|
||||||
|
return csi + lines + "T";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Width of the terminal, as reported by the COLUMNS environment variable.
|
||||||
|
*
|
||||||
|
* N.B. Not all environment will update this value when the terminal is resized.
|
||||||
|
*
|
||||||
|
* @return the number of columns in the terminal
|
||||||
|
*/
|
||||||
|
public static int width() {
|
||||||
|
return Optional.ofNullable(System.getenv("COLUMNS"))
|
||||||
|
.map(Integer::parseInt)
|
||||||
|
.map(x -> Math.max(x, 10))
|
||||||
|
.orElse(80);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String progressBar(
|
||||||
|
double pos,
|
||||||
|
double max,
|
||||||
|
int width
|
||||||
|
) {
|
||||||
|
Map<Integer, String> subBars = getSubBars();
|
||||||
|
int barWidth = width - 2;
|
||||||
|
int phases = subBars.values().size();
|
||||||
|
int pxWidth = barWidth * phases;
|
||||||
|
double ratio = pos / max;
|
||||||
|
int pxDone = (int) (ratio * pxWidth);
|
||||||
|
int fullHeadSize = pxDone / phases;
|
||||||
|
int part = pxDone % phases;
|
||||||
|
String partial = part != 0 ? subBars.getOrDefault(part, "") : "";
|
||||||
|
String head = repeat("█", fullHeadSize) + partial;
|
||||||
|
int tailSize = barWidth - head.length();
|
||||||
|
String tail = repeat(" ", tailSize);
|
||||||
|
return "[" + head + tail + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String repeat(String s, int times) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
IntStream.range(0, times).forEach(x -> sb.append(s));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
16
domain/src/main/java/net/kemitix/thorp/domain/Tuple.java
Normal file
16
domain/src/main/java/net/kemitix/thorp/domain/Tuple.java
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class Tuple<A, B> {
|
||||||
|
public final A a;
|
||||||
|
public final B b;
|
||||||
|
public static <A, B> Tuple<A, B> create(A a, B b) {
|
||||||
|
return new Tuple<>(a, b);
|
||||||
|
}
|
||||||
|
public Tuple<B, A> swap() {
|
||||||
|
return Tuple.create(b, a);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,40 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
sealed trait Action {
|
|
||||||
def bucket: Bucket
|
|
||||||
def size: Long
|
|
||||||
def remoteKey: RemoteKey
|
|
||||||
}
|
|
||||||
object Action {
|
|
||||||
|
|
||||||
final case class DoNothing(
|
|
||||||
bucket: Bucket,
|
|
||||||
remoteKey: RemoteKey,
|
|
||||||
size: Long
|
|
||||||
) extends Action
|
|
||||||
|
|
||||||
final case class ToUpload(
|
|
||||||
bucket: Bucket,
|
|
||||||
localFile: LocalFile,
|
|
||||||
size: Long
|
|
||||||
) extends Action {
|
|
||||||
override def remoteKey: RemoteKey = localFile.remoteKey
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ToCopy(
|
|
||||||
bucket: Bucket,
|
|
||||||
sourceKey: RemoteKey,
|
|
||||||
hash: MD5Hash,
|
|
||||||
targetKey: RemoteKey,
|
|
||||||
size: Long
|
|
||||||
) extends Action {
|
|
||||||
override def remoteKey: RemoteKey = targetKey
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ToDelete(
|
|
||||||
bucket: Bucket,
|
|
||||||
remoteKey: RemoteKey,
|
|
||||||
size: Long
|
|
||||||
) extends Action
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
final case class Bucket(
|
|
||||||
name: String
|
|
||||||
)
|
|
|
@ -1,20 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
final case class Counters(
|
|
||||||
uploaded: Int,
|
|
||||||
deleted: Int,
|
|
||||||
copied: Int,
|
|
||||||
errors: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
object Counters {
|
|
||||||
val empty: Counters = Counters(0, 0, 0, 0)
|
|
||||||
val uploaded: SimpleLens[Counters, Int] =
|
|
||||||
SimpleLens[Counters, Int](_.uploaded, b => a => b.copy(uploaded = a))
|
|
||||||
val deleted: SimpleLens[Counters, Int] =
|
|
||||||
SimpleLens[Counters, Int](_.deleted, b => a => b.copy(deleted = a))
|
|
||||||
val copied: SimpleLens[Counters, Int] =
|
|
||||||
SimpleLens[Counters, Int](_.copied, b => a => b.copy(copied = a))
|
|
||||||
val errors: SimpleLens[Counters, Int] =
|
|
||||||
SimpleLens[Counters, Int](_.errors, b => a => b.copy(errors = a))
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
import java.util.function.Predicate
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
sealed trait Filter {
|
|
||||||
def predicate: Predicate[String]
|
|
||||||
}
|
|
||||||
|
|
||||||
object Filter {
|
|
||||||
final case class Include(include: String) extends Filter {
|
|
||||||
lazy val predicate: Predicate[String] = Pattern.compile(include).asPredicate
|
|
||||||
}
|
|
||||||
object Include {
|
|
||||||
def all: Include = Include(".*")
|
|
||||||
}
|
|
||||||
final case class Exclude(exclude: String) extends Filter {
|
|
||||||
lazy val predicate: Predicate[String] =
|
|
||||||
Pattern.compile(exclude).asPredicate()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
trait HashType
|
|
||||||
|
|
||||||
object HashType {
|
|
||||||
case object MD5 extends HashType
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
import java.math.BigInteger
|
|
||||||
|
|
||||||
trait HexEncoder {
|
|
||||||
|
|
||||||
def encode(bytes: Array[Byte]): String =
|
|
||||||
String
|
|
||||||
.format(s"%0${bytes.length << 1}x", new BigInteger(1, bytes))
|
|
||||||
.toUpperCase
|
|
||||||
|
|
||||||
def decode(hexString: String): Array[Byte] =
|
|
||||||
hexString
|
|
||||||
.replaceAll("[^0-9A-Fa-f]", "")
|
|
||||||
.toSeq
|
|
||||||
.sliding(2, 2)
|
|
||||||
.map(_.unwrap)
|
|
||||||
.toArray
|
|
||||||
.map(Integer.parseInt(_, 16).toByte)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object HexEncoder extends HexEncoder
|
|
|
@ -1,11 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
object Implicits {
|
|
||||||
|
|
||||||
@SuppressWarnings(Array("org.wartremover.warts.Equals"))
|
|
||||||
implicit final class AnyOps[A](self: A) {
|
|
||||||
def ===(other: A): Boolean = self == other
|
|
||||||
def =/=(other: A): Boolean = self != other
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.HashType.MD5
|
|
||||||
import net.kemitix.thorp.domain.Implicits._
|
|
||||||
|
|
||||||
final case class LocalFile private (
|
|
||||||
file: File,
|
|
||||||
source: File,
|
|
||||||
hashes: Hashes,
|
|
||||||
remoteKey: RemoteKey,
|
|
||||||
length: Long
|
|
||||||
)
|
|
||||||
|
|
||||||
object LocalFile {
|
|
||||||
val remoteKey: SimpleLens[LocalFile, RemoteKey] =
|
|
||||||
SimpleLens[LocalFile, RemoteKey](_.remoteKey,
|
|
||||||
b => a => b.copy(remoteKey = a))
|
|
||||||
def matchesHash(localFile: LocalFile)(other: MD5Hash): Boolean =
|
|
||||||
localFile.hashes.values.exists(other === _)
|
|
||||||
def md5base64(localFile: LocalFile): Option[String] =
|
|
||||||
localFile.hashes.get(MD5).map(MD5Hash.hash64)
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
import java.util.Base64
|
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.QuoteStripper.stripQuotes
|
|
||||||
|
|
||||||
final case class MD5Hash(in: String)
|
|
||||||
|
|
||||||
object MD5Hash {
|
|
||||||
def fromDigest(digest: Array[Byte]): MD5Hash =
|
|
||||||
MD5Hash((digest map ("%02x" format _)).mkString)
|
|
||||||
def hash(md5Hash: MD5Hash): String = md5Hash.in.filter(stripQuotes)
|
|
||||||
def digest(md5Hash: MD5Hash): Array[Byte] =
|
|
||||||
HexEncoder.decode(MD5Hash.hash(md5Hash))
|
|
||||||
def hash64(md5Hash: MD5Hash): String =
|
|
||||||
Base64.getEncoder.encodeToString(MD5Hash.digest(md5Hash))
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
object MD5HashData {
|
|
||||||
|
|
||||||
object Root {
|
|
||||||
val hash: MD5Hash = MD5Hash("a3a6ac11a0eb577b81b3bb5c95cc8a6e")
|
|
||||||
val base64: String = "o6asEaDrV3uBs7tclcyKbg=="
|
|
||||||
val remoteKey = RemoteKey("root-file")
|
|
||||||
val size: Long = 55
|
|
||||||
}
|
|
||||||
object Leaf {
|
|
||||||
val hash: MD5Hash = MD5Hash("208386a650bdec61cfcd7bd8dcb6b542")
|
|
||||||
val base64: String = "IIOGplC97GHPzXvY3La1Qg=="
|
|
||||||
val remoteKey = RemoteKey("subdir/leaf-file")
|
|
||||||
val size: Long = 58
|
|
||||||
}
|
|
||||||
object BigFile {
|
|
||||||
val hash: MD5Hash = MD5Hash("b1ab1f7680138e6db7309200584e35d8")
|
|
||||||
object Part1 {
|
|
||||||
val offset: Int = 0
|
|
||||||
val size: Int = 1048576
|
|
||||||
val hash: MD5Hash = MD5Hash("39d4a9c78b9cfddf6d241a201a4ab726")
|
|
||||||
}
|
|
||||||
object Part2 {
|
|
||||||
val offset: Int = 1048576
|
|
||||||
val size: Int = 1048576
|
|
||||||
val hash: MD5Hash = MD5Hash("af5876f3a3bc6e66f4ae96bb93d8dae0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
import Implicits._
|
|
||||||
|
|
||||||
object QuoteStripper {
|
|
||||||
|
|
||||||
def stripQuotes: Char => Boolean = _ =/= '"'
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.nio.file.{Path, Paths}
|
|
||||||
|
|
||||||
import Implicits._
|
|
||||||
import zio.UIO
|
|
||||||
|
|
||||||
final case class RemoteKey(key: String)
|
|
||||||
|
|
||||||
object RemoteKey {
|
|
||||||
|
|
||||||
val key: SimpleLens[RemoteKey, String] =
|
|
||||||
SimpleLens[RemoteKey, String](_.key, b => a => b.copy(key = a))
|
|
||||||
|
|
||||||
def asFile(source: Path, prefix: RemoteKey)(
|
|
||||||
remoteKey: RemoteKey): Option[File] =
|
|
||||||
if (remoteKey.key.length === 0 || !remoteKey.key.startsWith(prefix.key))
|
|
||||||
None
|
|
||||||
else Some(source.resolve(RemoteKey.relativeTo(prefix)(remoteKey)).toFile)
|
|
||||||
|
|
||||||
def relativeTo(prefix: RemoteKey)(remoteKey: RemoteKey): Path = prefix match {
|
|
||||||
case RemoteKey("") => Paths.get(remoteKey.key)
|
|
||||||
case _ => Paths.get(prefix.key).relativize(Paths.get(remoteKey.key))
|
|
||||||
}
|
|
||||||
|
|
||||||
def resolve(path: String)(remoteKey: RemoteKey): RemoteKey =
|
|
||||||
RemoteKey(List(remoteKey.key, path).filterNot(_.isEmpty).mkString("/"))
|
|
||||||
|
|
||||||
def fromSourcePath(source: Path, path: Path): RemoteKey =
|
|
||||||
RemoteKey(source.relativize(path).toString)
|
|
||||||
|
|
||||||
def from(source: Path, prefix: RemoteKey, file: File): UIO[RemoteKey] =
|
|
||||||
UIO(RemoteKey.resolve(source.relativize(file.toPath).toString)(prefix))
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
import zio.UIO
|
|
||||||
|
|
||||||
import scala.collection.MapView
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A list of objects and their MD5 hash values.
|
|
||||||
*/
|
|
||||||
final case class RemoteObjects private (
|
|
||||||
byHash: MapView[MD5Hash, RemoteKey],
|
|
||||||
byKey: MapView[RemoteKey, MD5Hash]
|
|
||||||
)
|
|
||||||
|
|
||||||
object RemoteObjects {
|
|
||||||
|
|
||||||
val empty: RemoteObjects = RemoteObjects(MapView.empty, MapView.empty)
|
|
||||||
|
|
||||||
def create(byHash: MapView[MD5Hash, RemoteKey],
|
|
||||||
byKey: MapView[RemoteKey, MD5Hash]): RemoteObjects =
|
|
||||||
RemoteObjects(byHash, byKey)
|
|
||||||
|
|
||||||
def remoteKeyExists(
|
|
||||||
remoteObjects: RemoteObjects,
|
|
||||||
remoteKey: RemoteKey
|
|
||||||
): UIO[Boolean] = UIO(remoteObjects.byKey.contains(remoteKey))
|
|
||||||
|
|
||||||
def remoteMatchesLocalFile(
|
|
||||||
remoteObjects: RemoteObjects,
|
|
||||||
localFile: LocalFile
|
|
||||||
): UIO[Boolean] =
|
|
||||||
UIO(
|
|
||||||
remoteObjects.byKey
|
|
||||||
.get(localFile.remoteKey)
|
|
||||||
.exists(LocalFile.matchesHash(localFile)))
|
|
||||||
|
|
||||||
def remoteHasHash(
|
|
||||||
remoteObjects: RemoteObjects,
|
|
||||||
hashes: Hashes
|
|
||||||
): UIO[Option[(RemoteKey, MD5Hash)]] =
|
|
||||||
UIO(remoteObjects.byHash.collectFirst {
|
|
||||||
case (hash, key) if (hashes.values.exists(h => h == hash)) => (key, hash)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
final case class SimpleLens[A, B](field: A => B, update: A => B => A) {
|
|
||||||
|
|
||||||
def composeLens[C](other: SimpleLens[B, C]): SimpleLens[A, C] =
|
|
||||||
SimpleLens[A, C](
|
|
||||||
a => other.field(field(a)),
|
|
||||||
a => c => update(a)(other.update(field(a))(c))
|
|
||||||
)
|
|
||||||
|
|
||||||
def ^|->[C](other: SimpleLens[B, C]): SimpleLens[A, C] = composeLens(other)
|
|
||||||
|
|
||||||
def set(b: B)(a: A): A = update(a)(b)
|
|
||||||
|
|
||||||
def get(a: A): B = field(a)
|
|
||||||
|
|
||||||
def modify(f: B => B)(a: A): A = update(a)(f(field(a)))
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
object SizeTranslation {
|
|
||||||
|
|
||||||
val kbLimit: Long = 10240L
|
|
||||||
val mbLimit: Long = kbLimit * 1024
|
|
||||||
val gbLimit: Long = mbLimit * 1024
|
|
||||||
|
|
||||||
def sizeInEnglish(length: Long): String =
|
|
||||||
length.toDouble match {
|
|
||||||
case bytes if bytes > gbLimit => f"${bytes / 1024 / 1024 / 1024}%.3fGb"
|
|
||||||
case bytes if bytes > mbLimit => f"${bytes / 1024 / 1024}%.2fMb"
|
|
||||||
case bytes if bytes > kbLimit => f"${bytes / 1024}%.0fKb"
|
|
||||||
case bytes => s"${length}b"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
import java.nio.file.Path
|
|
||||||
|
|
||||||
import zio.{UIO, ZIO}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The paths to synchronise with target.
|
|
||||||
*
|
|
||||||
* The first source path takes priority over those later in the list,
|
|
||||||
* etc. Where there is any file with the same relative path within
|
|
||||||
* more than one source, the file in the first listed path is
|
|
||||||
* uploaded, and the others are ignored.
|
|
||||||
*
|
|
||||||
* A path should only occur once in paths.
|
|
||||||
*/
|
|
||||||
final case class Sources(paths: List[Path]) {
|
|
||||||
def +(path: Path): Sources = this ++ List(path)
|
|
||||||
def ++(otherPaths: List[Path]): Sources =
|
|
||||||
Sources(
|
|
||||||
otherPaths.foldLeft(paths)(
|
|
||||||
(acc, path) =>
|
|
||||||
if (acc contains path) acc
|
|
||||||
else acc ++ List(path)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
object Sources {
|
|
||||||
val emptySources: Sources = Sources(List.empty)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the source path for the given path.
|
|
||||||
*/
|
|
||||||
def forPath(path: Path)(sources: Sources): UIO[Path] =
|
|
||||||
ZIO
|
|
||||||
.fromOption(sources.paths.find(s => path.startsWith(s)))
|
|
||||||
.orDieWith { _ =>
|
|
||||||
new RuntimeException("Path is not within any known source")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
sealed trait StorageEvent
|
|
||||||
|
|
||||||
object StorageEvent {
|
|
||||||
|
|
||||||
final case class DoNothingEvent(
|
|
||||||
remoteKey: RemoteKey
|
|
||||||
) extends StorageEvent
|
|
||||||
|
|
||||||
final case class CopyEvent(
|
|
||||||
sourceKey: RemoteKey,
|
|
||||||
targetKey: RemoteKey
|
|
||||||
) extends StorageEvent
|
|
||||||
|
|
||||||
final case class UploadEvent(
|
|
||||||
remoteKey: RemoteKey,
|
|
||||||
md5Hash: MD5Hash
|
|
||||||
) extends StorageEvent
|
|
||||||
|
|
||||||
final case class DeleteEvent(
|
|
||||||
remoteKey: RemoteKey
|
|
||||||
) extends StorageEvent
|
|
||||||
|
|
||||||
final case class ErrorEvent(
|
|
||||||
action: ActionSummary,
|
|
||||||
remoteKey: RemoteKey,
|
|
||||||
e: Throwable
|
|
||||||
) extends StorageEvent
|
|
||||||
|
|
||||||
final case class ShutdownEvent() extends StorageEvent
|
|
||||||
|
|
||||||
sealed trait ActionSummary {
|
|
||||||
val name: String
|
|
||||||
val keys: String
|
|
||||||
}
|
|
||||||
object ActionSummary {
|
|
||||||
final case class Copy(keys: String) extends ActionSummary {
|
|
||||||
override val name: String = "Copy"
|
|
||||||
}
|
|
||||||
final case class Upload(keys: String) extends ActionSummary {
|
|
||||||
override val name: String = "Upload"
|
|
||||||
}
|
|
||||||
final case class Delete(keys: String) extends ActionSummary {
|
|
||||||
override val name: String = "Delete"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
import java.io.{File, IOException, PrintWriter}
|
|
||||||
import java.nio.file.attribute.BasicFileAttributes
|
|
||||||
import java.nio.file.{FileVisitResult, Files, Path, SimpleFileVisitor}
|
|
||||||
|
|
||||||
import scala.util.Try
|
|
||||||
|
|
||||||
trait TemporaryFolder {
|
|
||||||
|
|
||||||
@SuppressWarnings(Array("org.wartremover.warts.TryPartial"))
|
|
||||||
def withDirectory(testCode: Path => Any): Unit = {
|
|
||||||
val dir: Path = Files.createTempDirectory("thorp-temp")
|
|
||||||
val t = Try(testCode(dir))
|
|
||||||
remove(dir)
|
|
||||||
t.get
|
|
||||||
()
|
|
||||||
}
|
|
||||||
|
|
||||||
def remove(root: Path): Unit = {
|
|
||||||
Files.walkFileTree(
|
|
||||||
root,
|
|
||||||
new SimpleFileVisitor[Path] {
|
|
||||||
override def visitFile(file: Path,
|
|
||||||
attrs: BasicFileAttributes): FileVisitResult = {
|
|
||||||
Files.delete(file)
|
|
||||||
FileVisitResult.CONTINUE
|
|
||||||
}
|
|
||||||
override def postVisitDirectory(dir: Path,
|
|
||||||
exc: IOException): FileVisitResult = {
|
|
||||||
Files.delete(dir)
|
|
||||||
FileVisitResult.CONTINUE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
def createFile(directory: Path, name: String, contents: String*): File = {
|
|
||||||
val _ = directory.toFile.mkdirs
|
|
||||||
val file = directory.resolve(name).toFile
|
|
||||||
val writer = new PrintWriter(file, "UTF-8")
|
|
||||||
contents.foreach(writer.println)
|
|
||||||
writer.close()
|
|
||||||
file
|
|
||||||
}
|
|
||||||
|
|
||||||
def writeFile(directory: Path, name: String, contents: String*): Unit =
|
|
||||||
createFile(directory, name, contents: _*)
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,166 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
import Implicits._
|
|
||||||
|
|
||||||
object Terminal {
|
|
||||||
|
|
||||||
val esc: String = "\u001B"
|
|
||||||
val csi: String = esc + "["
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear from cursor to end of screen.
|
|
||||||
*/
|
|
||||||
val eraseToEndOfScreen: String = csi + "0J"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear from cursor to beginning of screen.
|
|
||||||
*/
|
|
||||||
val eraseToStartOfScreen: String = csi + "1J"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear screen and move cursor to top-left.
|
|
||||||
*
|
|
||||||
* On DOS the "2J" command also moves to 1,1, so we force that behaviour for all.
|
|
||||||
*/
|
|
||||||
val eraseScreen: String = csi + "2J" + cursorPosition(1, 1)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear screen and scrollback buffer then move cursor to top-left.
|
|
||||||
*
|
|
||||||
* Anticipate that same DOS behaviour here, and to maintain consistency with {@link #eraseScreen}.
|
|
||||||
*/
|
|
||||||
val eraseScreenAndBuffer: String = csi + "3J"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the terminal line to the right of the cursor.
|
|
||||||
*
|
|
||||||
* Does not move the cursor.
|
|
||||||
*/
|
|
||||||
val eraseLineForward: String = csi + "0K"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the terminal line to the left of the cursor.
|
|
||||||
*
|
|
||||||
* Does not move the cursor.
|
|
||||||
*/
|
|
||||||
val eraseLineBack: String = csi + "1K"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the whole terminal line.
|
|
||||||
*
|
|
||||||
* Does not move the cursor.
|
|
||||||
*/
|
|
||||||
val eraseLine: String = csi + "2K"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the cursor position/state.
|
|
||||||
*/
|
|
||||||
val saveCursorPosition: String = csi + "s"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores the cursor position/state.
|
|
||||||
*/
|
|
||||||
val restoreCursorPosition: String = csi + "u"
|
|
||||||
val enableAlternateBuffer: String = csi + "?1049h"
|
|
||||||
val disableAlternateBuffer: String = csi + "?1049l"
|
|
||||||
private val subBars = Map(0 -> " ",
|
|
||||||
1 -> "▏",
|
|
||||||
2 -> "▎",
|
|
||||||
3 -> "▍",
|
|
||||||
4 -> "▌",
|
|
||||||
5 -> "▋",
|
|
||||||
6 -> "▊",
|
|
||||||
7 -> "▉")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the cursor up, default 1 line.
|
|
||||||
*
|
|
||||||
* Stops at the edge of the screen.
|
|
||||||
*/
|
|
||||||
def cursorUp(lines: Int): String = s"${csi}${lines}A"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the cursor down, default 1 line.
|
|
||||||
*
|
|
||||||
* Stops at the edge of the screen.
|
|
||||||
*/
|
|
||||||
def cursorDown(lines: Int): String = s"${csi}${lines}B"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the cursor forward, default 1 column.
|
|
||||||
*
|
|
||||||
* Stops at the edge of the screen.
|
|
||||||
*/
|
|
||||||
def cursorForward(cols: Int): String = s"${csi}${cols}C"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the cursor back, default 1 column,
|
|
||||||
*
|
|
||||||
* Stops at the edge of the screen.
|
|
||||||
*/
|
|
||||||
def cursorBack(cols: Int): String = s"${csi}${cols}D"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the cursor to the beginning of the line, default 1, down.
|
|
||||||
*/
|
|
||||||
def cursorNextLine(lines: Int): String = s"${csi}${lines}E"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the cursor to the beginning of the line, default 1, up.
|
|
||||||
*/
|
|
||||||
def cursorPrevLine(lines: Int): String = s"${csi}${lines}F"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the cursor to the column on the current line.
|
|
||||||
*/
|
|
||||||
def cursorHorizAbs(col: Int): String = s"${csi}${col}G"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the cursor to the position on screen (1,1 is the top-left).
|
|
||||||
*/
|
|
||||||
def cursorPosition(row: Int, col: Int): String = s"${csi}${row};${col}H"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scroll page up, default 1, lines.
|
|
||||||
*/
|
|
||||||
def scrollUp(lines: Int): String = s"${csi}${lines}S"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scroll page down, default 1, lines.
|
|
||||||
*/
|
|
||||||
def scrollDown(lines: Int): String = s"${csi}${lines}T"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Width of the terminal, as reported by the COLUMNS environment variable.
|
|
||||||
*
|
|
||||||
* N.B. Not all environment will update this value when the terminal is resized.
|
|
||||||
*
|
|
||||||
* @return the number of columns in the terminal
|
|
||||||
*/
|
|
||||||
def width: Int = {
|
|
||||||
Option(System.getenv("COLUMNS"))
|
|
||||||
.map(_.toInt)
|
|
||||||
.map(Math.max(_, 10))
|
|
||||||
.getOrElse(80)
|
|
||||||
}
|
|
||||||
|
|
||||||
def progressBar(
|
|
||||||
pos: Double,
|
|
||||||
max: Double,
|
|
||||||
width: Int
|
|
||||||
): String = {
|
|
||||||
val barWidth = width - 2
|
|
||||||
val phases = subBars.values.size
|
|
||||||
val pxWidth = barWidth * phases
|
|
||||||
val ratio = pos / max
|
|
||||||
val pxDone = pxWidth * ratio
|
|
||||||
val fullHeadSize: Int = (pxDone / phases).toInt
|
|
||||||
val part = (pxDone % phases).toInt
|
|
||||||
val partial = if (part =/= 0) subBars.getOrElse(part, "") else ""
|
|
||||||
val head = ("█" * fullHeadSize) + partial
|
|
||||||
val tailSize = barWidth - head.length
|
|
||||||
val tail = " " * tailSize
|
|
||||||
s"[$head$tail]"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
package net.kemitix.thorp
|
|
||||||
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
package object domain {
|
|
||||||
type Hashes = Map[HashType, MD5Hash]
|
|
||||||
type LastModified = Instant
|
|
||||||
}
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import org.assertj.core.api.WithAssertions;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class HashesTest
|
||||||
|
implements WithAssertions {
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("mergeAll")
|
||||||
|
public class MergeAll {
|
||||||
|
@Test
|
||||||
|
@DisplayName("")
|
||||||
|
public void mergeAll() {
|
||||||
|
//given
|
||||||
|
HashType key1 = HashType.MD5;
|
||||||
|
HashType key2 = HashType.DUMMY;
|
||||||
|
MD5Hash value1 = MD5Hash.create("1");
|
||||||
|
MD5Hash value2 = MD5Hash.create("2");
|
||||||
|
Hashes hashes1 = Hashes.create(key1, value1);
|
||||||
|
Hashes hashes2 = Hashes.create(key2, value2);
|
||||||
|
//when
|
||||||
|
Hashes result = Hashes.mergeAll(Arrays.asList(hashes1,hashes2));
|
||||||
|
//then
|
||||||
|
assertThat(result.keys()).containsExactlyInAnyOrder(key1, key2);
|
||||||
|
assertThat(result.values()).containsExactlyInAnyOrder(value1, value2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import org.assertj.core.api.WithAssertions;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
public class HexEncoderTest
|
||||||
|
implements WithAssertions {
|
||||||
|
|
||||||
|
private String text = "test text to encode to hex";
|
||||||
|
private String hex = "74657374207465787420746F20656E636F646520746F20686578";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("can round trip a hash decode then encode")
|
||||||
|
public void roundTripDecodeEncode() {
|
||||||
|
String result = HexEncoder.encode(HexEncoder.decode(hex));
|
||||||
|
assertThat(result).isEqualTo(hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("can round trip a hash encode then decode")
|
||||||
|
public void roundTripEncodeDecode() {
|
||||||
|
byte[] input = hex.getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] result = HexEncoder.decode(HexEncoder.encode(input));
|
||||||
|
assertThat(result).isEqualTo(input);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import org.assertj.core.api.WithAssertions;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class MD5HashTest
|
||||||
|
implements WithAssertions {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("recover base64 hash")
|
||||||
|
public void recoverBase64Hash() {
|
||||||
|
assertThat(MD5HashData.Root.hash.hash64())
|
||||||
|
.isEqualTo(MD5HashData.Root.base64);
|
||||||
|
assertThat(MD5HashData.Leaf.hash.hash64())
|
||||||
|
.isEqualTo(MD5HashData.Leaf.base64);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("hash() strips quotes")
|
||||||
|
public void hashStripsQuotes() {
|
||||||
|
//given
|
||||||
|
String dQuote = "\"";
|
||||||
|
MD5Hash md5Hash = MD5Hash.create(dQuote + MD5HashData.Root.hashString + dQuote);
|
||||||
|
//when
|
||||||
|
String result = md5Hash.hash();
|
||||||
|
//then
|
||||||
|
assertThat(result).isEqualTo(MD5HashData.Root.hashString);
|
||||||
|
}
|
||||||
|
}
|
201
domain/src/test/java/net/kemitix/thorp/domain/RemoteKeyTest.java
Normal file
201
domain/src/test/java/net/kemitix/thorp/domain/RemoteKeyTest.java
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import org.assertj.core.api.WithAssertions;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class RemoteKeyTest
|
||||||
|
implements WithAssertions {
|
||||||
|
|
||||||
|
private RemoteKey emptyKey = RemoteKey.create("");
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Create a RemoteKey")
|
||||||
|
public class CreateRemoteKey {
|
||||||
|
@Nested
|
||||||
|
@DisplayName("resolve()")
|
||||||
|
public class ResolvePath {
|
||||||
|
@Test
|
||||||
|
@DisplayName("key is empty")
|
||||||
|
public void keyIsEmpty() {
|
||||||
|
//given
|
||||||
|
RemoteKey expected = RemoteKey.create("path");
|
||||||
|
RemoteKey key = emptyKey;
|
||||||
|
//when
|
||||||
|
RemoteKey result = key.resolve("path");
|
||||||
|
//then
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("path is empty")
|
||||||
|
public void pathIsEmpty() {
|
||||||
|
//given
|
||||||
|
RemoteKey expected = RemoteKey.create("key");
|
||||||
|
RemoteKey key = RemoteKey.create("key");
|
||||||
|
String path = "";
|
||||||
|
//when
|
||||||
|
RemoteKey result = key.resolve(path);
|
||||||
|
//then
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("key and path are empty")
|
||||||
|
public void keyAndPathEmpty() {
|
||||||
|
//given
|
||||||
|
RemoteKey expected = RemoteKeyTest.this.emptyKey;
|
||||||
|
String path = "";
|
||||||
|
RemoteKey key = emptyKey;
|
||||||
|
//when
|
||||||
|
RemoteKey result = key.resolve(path);
|
||||||
|
//then
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
@DisplayName("asFile()")
|
||||||
|
public class AsFile {
|
||||||
|
@Test
|
||||||
|
@DisplayName("key and prefix are non-empty")
|
||||||
|
public void keyAndPrefixNonEmpty() {
|
||||||
|
//given
|
||||||
|
Optional<File> expected = Optional.of(new File("source/key"));
|
||||||
|
RemoteKey key = RemoteKey.create("prefix/key");
|
||||||
|
Path source = Paths.get("source");
|
||||||
|
RemoteKey prefix = RemoteKey.create("prefix");
|
||||||
|
//when
|
||||||
|
Optional<File> result = key.asFile(source, prefix);
|
||||||
|
//then
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("prefix is empty")
|
||||||
|
public void prefixEmpty() {
|
||||||
|
//given
|
||||||
|
Optional<File> expected = Optional.of(new File("source/key"));
|
||||||
|
RemoteKey key = RemoteKey.create("key");
|
||||||
|
Path source = Paths.get("source");
|
||||||
|
RemoteKey prefix = emptyKey;
|
||||||
|
//when
|
||||||
|
Optional<File> result = key.asFile(source, prefix);
|
||||||
|
//then
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("key is empty")
|
||||||
|
public void keyEmpty() {
|
||||||
|
//given
|
||||||
|
Optional<File> expected = Optional.empty();
|
||||||
|
RemoteKey key = emptyKey;
|
||||||
|
Path source = Paths.get("source");
|
||||||
|
RemoteKey prefix = RemoteKey.create("source/key");
|
||||||
|
//when
|
||||||
|
Optional<File> result = key.asFile(source, prefix);
|
||||||
|
//then
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("key and prefix are empty")
|
||||||
|
public void keyAndPrefixEmpty() {
|
||||||
|
//given
|
||||||
|
Optional<File> expected = Optional.empty();
|
||||||
|
RemoteKey key = emptyKey;
|
||||||
|
Path source = Paths.get("source");
|
||||||
|
RemoteKey prefix = emptyKey;
|
||||||
|
//when
|
||||||
|
Optional<File> result = key.asFile(source, prefix);
|
||||||
|
//then
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
@DisplayName("fromSourcePath()")
|
||||||
|
public class FromSourcePath {
|
||||||
|
@Test
|
||||||
|
@DisplayName("path is in source")
|
||||||
|
public void pathInSource() {
|
||||||
|
//given
|
||||||
|
RemoteKey expected = RemoteKey.create("child");
|
||||||
|
Path source = Paths.get("/source");
|
||||||
|
Path path = source.resolve("/source/child");
|
||||||
|
//when
|
||||||
|
RemoteKey result = RemoteKey.fromSourcePath(source, path);
|
||||||
|
//then
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
@DisplayName("from(source, prefix, file)")
|
||||||
|
public class FromSourcePrefixFile {
|
||||||
|
@Test
|
||||||
|
@DisplayName("file in source")
|
||||||
|
public void fileInSource() {
|
||||||
|
//given
|
||||||
|
RemoteKey expected = RemoteKey.create("prefix/dir/filename");
|
||||||
|
Path source = Paths.get("/source");
|
||||||
|
RemoteKey prefix = RemoteKey.create("prefix");
|
||||||
|
File file = new File("/source/dir/filename");
|
||||||
|
//when
|
||||||
|
RemoteKey result = RemoteKey.from(source, prefix, file);
|
||||||
|
//then
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
@DisplayName("asFile()")
|
||||||
|
public class AsFile {
|
||||||
|
@Test
|
||||||
|
@DisplayName("remoteKey is empty")
|
||||||
|
public void remoteKeyEmpty() {
|
||||||
|
//given
|
||||||
|
Optional<File> expected = Optional.empty();
|
||||||
|
Path source = Paths.get("/source");
|
||||||
|
RemoteKey prefix = RemoteKey.create("prefix");
|
||||||
|
RemoteKey remoteKey = emptyKey;
|
||||||
|
//when
|
||||||
|
Optional<File> result = remoteKey.asFile(source, prefix);
|
||||||
|
//then
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
@DisplayName("remoteKey is not empty")
|
||||||
|
public class RemoteKeyNotEmpty {
|
||||||
|
@Test
|
||||||
|
@DisplayName("remoteKey is within prefix")
|
||||||
|
public void remoteKeyWithinPrefix() {
|
||||||
|
//given
|
||||||
|
Optional<File> expected = Optional.of(new File("/source/key"));
|
||||||
|
Path source = Paths.get("/source");
|
||||||
|
RemoteKey prefix = RemoteKey.create("prefix");
|
||||||
|
RemoteKey remoteKey = RemoteKey.create("prefix/key");
|
||||||
|
//when
|
||||||
|
Optional<File> result = remoteKey.asFile(source, prefix);
|
||||||
|
//then
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("remoteKey is outwith prefix")
|
||||||
|
public void remoteKeyIsOutwithPrefix() {
|
||||||
|
//given
|
||||||
|
Optional<File> expected = Optional.empty();
|
||||||
|
Path source = Paths.get("/source");
|
||||||
|
RemoteKey prefix = RemoteKey.create("prefix");
|
||||||
|
RemoteKey remoteKey = RemoteKey.create("elsewhere/key");
|
||||||
|
//when
|
||||||
|
Optional<File> result = remoteKey.asFile(source, prefix);
|
||||||
|
//then
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import org.assertj.core.api.WithAssertions;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class SizeTranslationTest
|
||||||
|
implements WithAssertions {
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("sizeInEnglish()")
|
||||||
|
public class SizeInEnglish {
|
||||||
|
@Test
|
||||||
|
@DisplayName("when size is less the 1Kb")
|
||||||
|
public void sizeLessThan1Kb() {
|
||||||
|
//should be in bytes
|
||||||
|
assertThat(SizeTranslation.sizeInEnglish(512))
|
||||||
|
.isEqualTo("512b");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("when size is a less than 10Kb")
|
||||||
|
public void sizeLessThan10Kb() {
|
||||||
|
//should still be in bytes
|
||||||
|
assertThat(SizeTranslation.sizeInEnglish(2000))
|
||||||
|
.isEqualTo("2000b");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("when size is over 10Kb and less than 10Mb")
|
||||||
|
public void sizeBetween10KbAnd10Mb() {
|
||||||
|
//should be in Kb with zero decimal places
|
||||||
|
assertThat(SizeTranslation.sizeInEnglish(5599232))
|
||||||
|
.isEqualTo("5468Kb");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("when size is over 10Mb and less than 10Gb")
|
||||||
|
public void sizeBetween10Mb10Gb() {
|
||||||
|
//should be in Mb with two decimal place
|
||||||
|
assertThat(SizeTranslation.sizeInEnglish(5733789833L))
|
||||||
|
.isEqualTo("5468.17Mb");
|
||||||
|
}
|
||||||
|
@Test@DisplayName("when size is over 10Gb")
|
||||||
|
public void sizeOver10Gb() {
|
||||||
|
//should be in Gb with three decimal place
|
||||||
|
assertThat(SizeTranslation.sizeInEnglish(5871400857278L))
|
||||||
|
.isEqualTo("5468.168Gb");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
|
import org.assertj.core.api.WithAssertions;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class TerminalTest
|
||||||
|
implements WithAssertions {
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("progressBar()")
|
||||||
|
public class ProgressBar {
|
||||||
|
@Test
|
||||||
|
@DisplayName("width 10 - 0%")
|
||||||
|
public void width10at0() {
|
||||||
|
String bar = Terminal.progressBar(0d, 10d, 12);
|
||||||
|
assertThat(bar).isEqualTo("[ ]");
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("width 10 - 10%")
|
||||||
|
public void width10at10() {
|
||||||
|
String bar = Terminal.progressBar(1d, 10d, 12);
|
||||||
|
assertThat(bar).isEqualTo("[█ ]");
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("width 10 - 50%")
|
||||||
|
public void width10at50() {
|
||||||
|
String bar = Terminal.progressBar(5d, 10d, 12);
|
||||||
|
assertThat(bar).isEqualTo("[█████ ]");
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("width 1 - 8/8th")
|
||||||
|
public void width8of8() {
|
||||||
|
String bar = Terminal.progressBar(8d, 8d, 3);
|
||||||
|
assertThat(bar).isEqualTo("[█]");
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("width 1 - 7/8th")
|
||||||
|
public void width7of8() {
|
||||||
|
String bar = Terminal.progressBar(7d, 8d, 3);
|
||||||
|
assertThat(bar).isEqualTo("[▉]");
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("width 1 - 6/8th")
|
||||||
|
public void width6of8() {
|
||||||
|
String bar = Terminal.progressBar(6d, 8d, 3);
|
||||||
|
assertThat(bar).isEqualTo("[▊]");
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("width 1 - 5/8th")
|
||||||
|
public void width5of8() {
|
||||||
|
String bar = Terminal.progressBar(5d, 8d, 3);
|
||||||
|
assertThat(bar).isEqualTo("[▋]");
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("width 1 - 4/8th")
|
||||||
|
public void width4of8() {
|
||||||
|
String bar = Terminal.progressBar(4d, 8d, 3);
|
||||||
|
assertThat(bar).isEqualTo("[▌]");
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("width 1 - 3/8th")
|
||||||
|
public void width3of8() {
|
||||||
|
String bar = Terminal.progressBar(3d, 8d, 3);
|
||||||
|
assertThat(bar).isEqualTo("[▍]");
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("width 1 - 2/8th")
|
||||||
|
public void width2of8() {
|
||||||
|
String bar = Terminal.progressBar(2d, 8d, 3);
|
||||||
|
assertThat(bar).isEqualTo("[▎]");
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@DisplayName("width 1 - 1/8th")
|
||||||
|
public void width1of8() {
|
||||||
|
String bar = Terminal.progressBar(1d, 8d, 3);
|
||||||
|
assertThat(bar).isEqualTo("[▏]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
|
|
||||||
import org.scalatest.FreeSpec
|
|
||||||
|
|
||||||
class HexEncoderTest extends FreeSpec {
|
|
||||||
|
|
||||||
val text = "test text to encode to hex"
|
|
||||||
val hex = "74657374207465787420746F20656E636F646520746F20686578"
|
|
||||||
|
|
||||||
"can round trip a hash decode then encode" in {
|
|
||||||
val input = hex
|
|
||||||
val result = HexEncoder.encode(HexEncoder.decode(input))
|
|
||||||
assertResult(input)(result)
|
|
||||||
}
|
|
||||||
"can round trip a hash encode then decode" in {
|
|
||||||
val input = hex.getBytes(StandardCharsets.UTF_8)
|
|
||||||
val result = HexEncoder.decode(HexEncoder.encode(input))
|
|
||||||
assertResult(input)(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
import org.scalatest.FunSpec
|
|
||||||
|
|
||||||
class MD5HashTest extends FunSpec {
|
|
||||||
|
|
||||||
describe("recover base64 hash") {
|
|
||||||
it("should recover base 64 #1") {
|
|
||||||
val rootHash = MD5HashData.Root.hash
|
|
||||||
assertResult(MD5HashData.Root.base64)(MD5Hash.hash64(rootHash))
|
|
||||||
}
|
|
||||||
it("should recover base 64 #2") {
|
|
||||||
val leafHash = MD5HashData.Leaf.hash
|
|
||||||
assertResult(MD5HashData.Leaf.base64)(MD5Hash.hash64(leafHash))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,130 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.nio.file.Paths
|
|
||||||
|
|
||||||
import org.scalatest.FreeSpec
|
|
||||||
import zio.DefaultRuntime
|
|
||||||
|
|
||||||
class RemoteKeyTest extends FreeSpec {
|
|
||||||
|
|
||||||
private val emptyKey = RemoteKey("")
|
|
||||||
|
|
||||||
"create a RemoteKey" - {
|
|
||||||
"can resolve a path" - {
|
|
||||||
"when key is empty" in {
|
|
||||||
val key = emptyKey
|
|
||||||
val path = "path"
|
|
||||||
val expected = RemoteKey("path")
|
|
||||||
val result = RemoteKey.resolve(path)(key)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
"when path is empty" in {
|
|
||||||
val key = RemoteKey("key")
|
|
||||||
val path = ""
|
|
||||||
val expected = RemoteKey("key")
|
|
||||||
val result = RemoteKey.resolve(path)(key)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
"when key and path are empty" in {
|
|
||||||
val key = emptyKey
|
|
||||||
val path = ""
|
|
||||||
val expected = emptyKey
|
|
||||||
val result = RemoteKey.resolve(path)(key)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"asFile" - {
|
|
||||||
"when key and prefix are non-empty" in {
|
|
||||||
val key = RemoteKey("prefix/key")
|
|
||||||
val source = Paths.get("source")
|
|
||||||
val prefix = RemoteKey("prefix")
|
|
||||||
val expected = Some(new File("source/key"))
|
|
||||||
val result = RemoteKey.asFile(source, prefix)(key)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
"when prefix is empty" in {
|
|
||||||
val key = RemoteKey("key")
|
|
||||||
val source = Paths.get("source")
|
|
||||||
val prefix = emptyKey
|
|
||||||
val expected = Some(new File("source/key"))
|
|
||||||
val result = RemoteKey.asFile(source, prefix)(key)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
"when key is empty" in {
|
|
||||||
val key = emptyKey
|
|
||||||
val source = Paths.get("source")
|
|
||||||
val prefix = RemoteKey("prefix")
|
|
||||||
val expected = None
|
|
||||||
val result = RemoteKey.asFile(source, prefix)(key)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
"when key and prefix are empty" in {
|
|
||||||
val key = emptyKey
|
|
||||||
val source = Paths.get("source")
|
|
||||||
val prefix = emptyKey
|
|
||||||
val expected = None
|
|
||||||
val result = RemoteKey.asFile(source, prefix)(key)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"fromSourcePath" - {
|
|
||||||
"when path in source" in {
|
|
||||||
val source = Paths.get("/source")
|
|
||||||
val path = source.resolve("/source/child")
|
|
||||||
val expected = RemoteKey("child")
|
|
||||||
val result = RemoteKey.fromSourcePath(source, path)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"from source, prefix, file" - {
|
|
||||||
"when file in source" in {
|
|
||||||
val source = Paths.get("/source")
|
|
||||||
val prefix = RemoteKey("prefix")
|
|
||||||
val file = new File("/source/dir/filename")
|
|
||||||
val expected = RemoteKey("prefix/dir/filename")
|
|
||||||
val program = RemoteKey.from(source, prefix, file)
|
|
||||||
val result = new DefaultRuntime {}.unsafeRunSync(program).toEither
|
|
||||||
assertResult(Right(expected))(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"asFile" - {
|
|
||||||
"remoteKey is empty" in {
|
|
||||||
val source = Paths.get("/source")
|
|
||||||
val prefix = RemoteKey("prefix")
|
|
||||||
val remoteKey = RemoteKey("")
|
|
||||||
|
|
||||||
val expected = None
|
|
||||||
|
|
||||||
val result = RemoteKey.asFile(source, prefix)(remoteKey)
|
|
||||||
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
"remoteKey is not empty" - {
|
|
||||||
"remoteKey is within prefix" in {
|
|
||||||
val source = Paths.get("/source")
|
|
||||||
val prefix = RemoteKey("prefix")
|
|
||||||
val remoteKey = RemoteKey("prefix/key")
|
|
||||||
|
|
||||||
val expected = Some(Paths.get("/source/key").toFile)
|
|
||||||
|
|
||||||
val result = RemoteKey.asFile(source, prefix)(remoteKey)
|
|
||||||
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
"remoteKey is outwith prefix" in {
|
|
||||||
val source = Paths.get("/source")
|
|
||||||
val prefix = RemoteKey("prefix")
|
|
||||||
val remoteKey = RemoteKey("elsewhere/key")
|
|
||||||
|
|
||||||
val expected = None
|
|
||||||
|
|
||||||
val result = RemoteKey.asFile(source, prefix)(remoteKey)
|
|
||||||
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
import org.scalatest.FreeSpec
|
|
||||||
|
|
||||||
class SimpleLensTest extends FreeSpec {
|
|
||||||
|
|
||||||
"lens" - {
|
|
||||||
val subject = Subject(0, "s")
|
|
||||||
"modify" in {
|
|
||||||
val expected = Subject(1, "s")
|
|
||||||
val result = Subject.anIntLens.modify(_ + 1)(subject)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
"get" in {
|
|
||||||
val expected = "s"
|
|
||||||
val result = Subject.aStringLens.get(subject)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
"set" in {
|
|
||||||
val expected = Subject(0, "k")
|
|
||||||
val result = Subject.aStringLens.set("k")(subject)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"lens composed" - {
|
|
||||||
val wrapper = Wrapper(1, Subject(2, "x"))
|
|
||||||
val subjectStringLens = Wrapper.aSubjectLens ^|-> Subject.aStringLens
|
|
||||||
"modify" in {
|
|
||||||
val expected = Wrapper(1, Subject(2, "X"))
|
|
||||||
val result = subjectStringLens.modify(_.toUpperCase)(wrapper)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
"get" in {
|
|
||||||
val expected = "x"
|
|
||||||
val result = subjectStringLens.get(wrapper)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
"set" in {
|
|
||||||
val expected = Wrapper(1, Subject(2, "k"))
|
|
||||||
val result = subjectStringLens.set("k")(wrapper)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case class Subject(anInt: Int, aString: String)
|
|
||||||
object Subject {
|
|
||||||
val anIntLens: SimpleLens[Subject, Int] =
|
|
||||||
SimpleLens[Subject, Int](_.anInt, subject => i => subject.copy(anInt = i))
|
|
||||||
val aStringLens: SimpleLens[Subject, String] =
|
|
||||||
SimpleLens[Subject, String](_.aString,
|
|
||||||
subject => str => subject.copy(aString = str))
|
|
||||||
}
|
|
||||||
case class Wrapper(anInt: Int, aSubject: Subject)
|
|
||||||
object Wrapper {
|
|
||||||
val anIntLens: SimpleLens[Wrapper, Int] =
|
|
||||||
SimpleLens[Wrapper, Int](_.anInt, wrapper => i => wrapper.copy(anInt = i))
|
|
||||||
val aSubjectLens: SimpleLens[Wrapper, Subject] =
|
|
||||||
SimpleLens[Wrapper, Subject](
|
|
||||||
_.aSubject,
|
|
||||||
wrapper => subject => wrapper.copy(aSubject = subject))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
import org.scalatest.FunSpec
|
|
||||||
|
|
||||||
class SizeTranslationTest extends FunSpec {
|
|
||||||
|
|
||||||
describe("sizeInEnglish") {
|
|
||||||
describe("when size is less the 1Kb") {
|
|
||||||
it("should in in bytes") {
|
|
||||||
assertResult("512b")(SizeTranslation.sizeInEnglish(512))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("when size is a less than 10Kb") {
|
|
||||||
it("should still be in bytes") {
|
|
||||||
assertResult("2000b")(SizeTranslation.sizeInEnglish(2000))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("when size is over 10Kb and less than 10Mb") {
|
|
||||||
it("should be in Kb with zero decimal places") {
|
|
||||||
assertResult("5468Kb")(SizeTranslation.sizeInEnglish(5599232))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("when size is over 10Mb and less than 10Gb") {
|
|
||||||
it("should be in Mb with two decimal place") {
|
|
||||||
assertResult("5468.17Mb")(SizeTranslation.sizeInEnglish(5733789833L))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("when size is over 10Gb") {
|
|
||||||
it("should be in Gb with three decimal place") {
|
|
||||||
assertResult("5468.168Gb")(
|
|
||||||
SizeTranslation.sizeInEnglish(5871400857278L))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
import org.scalatest.FunSpec
|
|
||||||
|
|
||||||
class TerminalTest extends FunSpec {
|
|
||||||
|
|
||||||
describe("progressBar") {
|
|
||||||
describe("width 10 - 0%") {
|
|
||||||
it("should match") {
|
|
||||||
val bar = Terminal.progressBar(0d, 10d, 12)
|
|
||||||
assertResult("[ ]")(bar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("width 10 - 10%") {
|
|
||||||
it("should match") {
|
|
||||||
val bar = Terminal.progressBar(1d, 10d, 12)
|
|
||||||
assertResult("[█ ]")(bar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("width 1 - 8/8th") {
|
|
||||||
it("should match") {
|
|
||||||
val bar = Terminal.progressBar(8d, 8d, 3)
|
|
||||||
assertResult("[█]")(bar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("width 1 - 7/8th") {
|
|
||||||
it("should match") {
|
|
||||||
val bar = Terminal.progressBar(7d, 8d, 3)
|
|
||||||
assertResult("[▉]")(bar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("width 1 - 6/8th") {
|
|
||||||
it("should match") {
|
|
||||||
val bar = Terminal.progressBar(6d, 8d, 3)
|
|
||||||
assertResult("[▊]")(bar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("width 1 - 5/8th") {
|
|
||||||
it("should match") {
|
|
||||||
val bar = Terminal.progressBar(5d, 8d, 3)
|
|
||||||
assertResult("[▋]")(bar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("width 1 - 4/8th") {
|
|
||||||
it("should match") {
|
|
||||||
val bar = Terminal.progressBar(4d, 8d, 3)
|
|
||||||
assertResult("[▌]")(bar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("width 1 - 3/8th") {
|
|
||||||
it("should match") {
|
|
||||||
val bar = Terminal.progressBar(3d, 8d, 3)
|
|
||||||
assertResult("[▍]")(bar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("width 1 - 2/8th") {
|
|
||||||
it("should match") {
|
|
||||||
val bar = Terminal.progressBar(2d, 8d, 3)
|
|
||||||
assertResult("[▎]")(bar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
describe("width 1 - 1/8th") {
|
|
||||||
it("should match") {
|
|
||||||
val bar = Terminal.progressBar(1d, 8d, 3)
|
|
||||||
assertResult("[▏]")(bar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,37 +12,28 @@
|
||||||
<name>filesystem</name>
|
<name>filesystem</name>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<!-- lombok -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- thorp -->
|
<!-- thorp -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.kemitix.thorp</groupId>
|
<groupId>net.kemitix.thorp</groupId>
|
||||||
<artifactId>thorp-domain</artifactId>
|
<artifactId>thorp-domain</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- scala -->
|
<!-- testing -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.scala-lang</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>scala-library</artifactId>
|
<artifactId>junit-jupiter</artifactId>
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- zio -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>dev.zio</groupId>
|
|
||||||
<artifactId>zio_2.13</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>dev.zio</groupId>
|
|
||||||
<artifactId>zio-streams_2.13</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- scala - testing -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.scalatest</groupId>
|
|
||||||
<artifactId>scalatest_2.13</artifactId>
|
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.scalamock</groupId>
|
<groupId>org.assertj</groupId>
|
||||||
<artifactId>scalamock_2.13</artifactId>
|
<artifactId>assertj-core</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue