Convert Config to full ZIO effect module (#134)

* [config] new module

* [config] stub module

* [domain] Rename domain.Config as domain.LegacyConfig

* [config] Move LegacyConfig to config module

* [config] Move config parsing and validation into module

* [config] Complete migration to module for Config

* [config] Config You should not name methods after their defining object

* [config] Rename LegacyConfig as Configuration

Also remove redundant uses

* [core] LocalFileStream Refactoring

* [changelog] update
This commit is contained in:
Paul Campbell 2019-07-28 21:47:01 +01:00 committed by GitHub
parent 96a83e6c3e
commit ccefd286f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 517 additions and 327 deletions

View file

@ -20,6 +20,7 @@ The format is based on [[https://keepachangelog.com/en/1.0.0/][Keep a Changelog]
- [internal] Replace Monocle with local SimpleLens implementation (#121) - [internal] Replace Monocle with local SimpleLens implementation (#121)
- [internal] Don't use String as key in Map for hashes (#124) - [internal] Don't use String as key in Map for hashes (#124)
- [internal] Convert Storage to full ZIO effect module (#133) - [internal] Convert Storage to full ZIO effect module (#133)
- [internal] Convert Config to full ZIO effect module (#134)
** Dependencies ** Dependencies

View file

@ -58,6 +58,7 @@ val zioDependencies = Seq(
) )
// cli -> thorp-lib -> storage-aws -> core -> storage-api -> console -> domain // cli -> thorp-lib -> storage-aws -> core -> storage-api -> console -> domain
// core -> config -> domain
lazy val thorp = (project in file(".")) lazy val thorp = (project in file("."))
.settings(commonSettings) .settings(commonSettings)
@ -67,7 +68,6 @@ lazy val cli = (project in file("cli"))
.settings(commonSettings) .settings(commonSettings)
.settings(mainClass in assembly := Some("net.kemitix.thorp.cli.Main")) .settings(mainClass in assembly := Some("net.kemitix.thorp.cli.Main"))
.settings(applicationSettings) .settings(applicationSettings)
.settings(commandLineParsing)
.settings(testDependencies) .settings(testDependencies)
.enablePlugins(BuildInfoPlugin) .enablePlugins(BuildInfoPlugin)
.settings( .settings(
@ -101,6 +101,7 @@ lazy val core = (project in file("core"))
.settings(testDependencies) .settings(testDependencies)
.dependsOn(`storage-api`) .dependsOn(`storage-api`)
.dependsOn(domain % "compile->compile;test->test") .dependsOn(domain % "compile->compile;test->test")
.dependsOn(config)
lazy val `storage-api` = (project in file("storage-api")) lazy val `storage-api` = (project in file("storage-api"))
.settings(commonSettings) .settings(commonSettings)
@ -114,6 +115,14 @@ lazy val console = (project in file("console"))
.settings(assemblyJarName in assembly := "console.jar") .settings(assemblyJarName in assembly := "console.jar")
.dependsOn(domain) .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)
lazy val domain = (project in file("domain")) lazy val domain = (project in file("domain"))
.settings(commonSettings) .settings(commonSettings)
.settings(assemblyJarName in assembly := "domain.jar") .settings(assemblyJarName in assembly := "domain.jar")

View file

@ -1,12 +1,13 @@
package net.kemitix.thorp.cli package net.kemitix.thorp.cli
import net.kemitix.thorp.config.Config
import net.kemitix.thorp.console.Console import net.kemitix.thorp.console.Console
import net.kemitix.thorp.storage.aws.S3Storage import net.kemitix.thorp.storage.aws.S3Storage
import zio.{App, ZIO} import zio.{App, ZIO}
object Main extends App { object Main extends App {
object LiveThorpApp extends S3Storage.Live with Console.Live object LiveThorpApp extends S3Storage.Live with Console.Live with Config.Live
override def run(args: List[String]): ZIO[Environment, Nothing, Int] = override def run(args: List[String]): ZIO[Environment, Nothing, Int] =
Program Program

View file

@ -1,11 +1,12 @@
package net.kemitix.thorp.cli package net.kemitix.thorp.cli
import net.kemitix.thorp.config._
import net.kemitix.thorp.console._ import net.kemitix.thorp.console._
import net.kemitix.thorp.core.CoreTypes.CoreProgram import net.kemitix.thorp.core.CoreTypes.CoreProgram
import net.kemitix.thorp.core._ import net.kemitix.thorp.core._
import net.kemitix.thorp.domain.StorageQueueEvent import net.kemitix.thorp.domain.StorageQueueEvent
import net.kemitix.thorp.storage.aws.S3HashService.defaultHashService import net.kemitix.thorp.storage.aws.S3HashService.defaultHashService
import zio.{UIO, ZIO} import zio.ZIO
trait Program { trait Program {
@ -13,28 +14,27 @@ trait Program {
def run(args: List[String]): CoreProgram[Unit] = { def run(args: List[String]): CoreProgram[Unit] = {
for { for {
cli <- CliArgs.parse(args) cli <- CliArgs.parse(args)
_ <- ZIO.when(showVersion(cli))(putStrLn(version)) config <- ConfigurationBuilder.buildConfig(cli)
_ <- ZIO.when(!showVersion(cli))(execute(cli).catchAll(handleErrors)) _ <- setConfiguration(config)
_ <- ZIO.when(showVersion(cli))(putStrLn(version))
_ <- ZIO.when(!showVersion(cli))(execute.catchAll(handleErrors))
} yield () } yield ()
} }
private def showVersion: ConfigOptions => Boolean = private def showVersion: ConfigOptions => Boolean =
cli => ConfigQuery.showVersion(cli) cli => ConfigQuery.showVersion(cli)
private def execute(cliOptions: ConfigOptions) = { private def execute = {
for { for {
plan <- PlanBuilder.createPlan(defaultHashService, cliOptions) plan <- PlanBuilder.createPlan(defaultHashService)
batchMode <- isBatchMode(cliOptions) batchMode <- isBatchMode
archive <- UnversionedMirrorArchive.default(batchMode, plan.syncTotals) archive <- UnversionedMirrorArchive.default(batchMode, plan.syncTotals)
events <- applyPlan(archive, plan) events <- applyPlan(archive, plan)
_ <- SyncLogging.logRunFinished(events) _ <- SyncLogging.logRunFinished(events)
} yield () } yield ()
} }
private def isBatchMode(cliOptions: ConfigOptions) =
UIO(ConfigQuery.batchMode(cliOptions))
private def handleErrors(throwable: Throwable) = private def handleErrors(throwable: Throwable) =
for { for {
_ <- putStrLn("There were errors:") _ <- putStrLn("There were errors:")

View file

@ -1,8 +1,7 @@
package net.kemitix.thorp.cli package net.kemitix.thorp.config
import java.nio.file.Paths import java.nio.file.Paths
import net.kemitix.thorp.core.{ConfigOption, ConfigOptions}
import scopt.OParser import scopt.OParser
import zio.Task import zio.Task

View file

@ -0,0 +1,50 @@
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]]
}
trait Live extends Config {
val config: Service = new Service {
private val configRef = new AtomicReference(Configuration())
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)
}
}
object Live extends Live
}

View file

@ -1,24 +1,24 @@
package net.kemitix.thorp.core package net.kemitix.thorp.config
import java.nio.file.Path import java.nio.file.Path
import net.kemitix.thorp.config.Configuration._
import net.kemitix.thorp.domain import net.kemitix.thorp.domain
import net.kemitix.thorp.domain.{Config, RemoteKey} import net.kemitix.thorp.domain.RemoteKey
import net.kemitix.thorp.domain.Config._
sealed trait ConfigOption { sealed trait ConfigOption {
def update(config: Config): Config def update(config: Configuration): Configuration
} }
object ConfigOption { object ConfigOption {
case class Source(path: Path) extends ConfigOption { case class Source(path: Path) extends ConfigOption {
override def update(config: Config): Config = override def update(config: Configuration): Configuration =
sources.modify(_ ++ path)(config) sources.modify(_ ++ path)(config)
} }
case class Bucket(name: String) extends ConfigOption { case class Bucket(name: String) extends ConfigOption {
override def update(config: Config): Config = override def update(config: Configuration): Configuration =
if (config.bucket.name.isEmpty) if (config.bucket.name.isEmpty)
bucket.set(domain.Bucket(name))(config) bucket.set(domain.Bucket(name))(config)
else else
@ -26,7 +26,7 @@ object ConfigOption {
} }
case class Prefix(path: String) extends ConfigOption { case class Prefix(path: String) extends ConfigOption {
override def update(config: Config): Config = override def update(config: Configuration): Configuration =
if (config.prefix.key.isEmpty) if (config.prefix.key.isEmpty)
prefix.set(RemoteKey(path))(config) prefix.set(RemoteKey(path))(config)
else else
@ -34,35 +34,35 @@ object ConfigOption {
} }
case class Include(pattern: String) extends ConfigOption { case class Include(pattern: String) extends ConfigOption {
override def update(config: Config): Config = override def update(config: Configuration): Configuration =
filters.modify(domain.Filter.Include(pattern) :: _)(config) filters.modify(domain.Filter.Include(pattern) :: _)(config)
} }
case class Exclude(pattern: String) extends ConfigOption { case class Exclude(pattern: String) extends ConfigOption {
override def update(config: Config): Config = override def update(config: Configuration): Configuration =
filters.modify(domain.Filter.Exclude(pattern) :: _)(config) filters.modify(domain.Filter.Exclude(pattern) :: _)(config)
} }
case class Debug() extends ConfigOption { case class Debug() extends ConfigOption {
override def update(config: Config): Config = override def update(config: Configuration): Configuration =
debug.set(true)(config) debug.set(true)(config)
} }
case object Version extends ConfigOption { case object Version extends ConfigOption {
override def update(config: Config): Config = config override def update(config: Configuration): Configuration = config
} }
case object BatchMode extends ConfigOption { case object BatchMode extends ConfigOption {
override def update(config: Config): Config = override def update(config: Configuration): Configuration =
batchMode.set(true)(config) batchMode.set(true)(config)
} }
case object IgnoreUserOptions extends ConfigOption { case object IgnoreUserOptions extends ConfigOption {
override def update(config: Config): Config = config override def update(config: Configuration): Configuration = config
} }
case object IgnoreGlobalOptions extends ConfigOption { case object IgnoreGlobalOptions extends ConfigOption {
override def update(config: Config): Config = config override def update(config: Configuration): Configuration = config
} }
} }

View file

@ -1,4 +1,4 @@
package net.kemitix.thorp.core package net.kemitix.thorp.config
import net.kemitix.thorp.domain.SimpleLens import net.kemitix.thorp.domain.SimpleLens

View file

@ -1,4 +1,4 @@
package net.kemitix.thorp.core package net.kemitix.thorp.config
import java.nio.file.Paths import java.nio.file.Paths
@ -21,11 +21,11 @@ trait ConfigQuery {
def sources(configOptions: ConfigOptions): Sources = { def sources(configOptions: ConfigOptions): Sources = {
val paths = configOptions.options.flatMap { val paths = configOptions.options.flatMap {
case ConfigOption.Source(sourcePath) => Some(sourcePath) case ConfigOption.Source(sourcePath) => Some(sourcePath)
case _ => None case _ => None
} }
Sources(paths match { Sources(paths match {
case List() => List(Paths.get(System.getenv("PWD"))) case List() => List(Paths.get(System.getenv("PWD")))
case _ => paths case _ => paths
}) })
} }

View file

@ -1,4 +1,4 @@
package net.kemitix.thorp.core package net.kemitix.thorp.config
import java.nio.file.Path import java.nio.file.Path

View file

@ -1,4 +1,4 @@
package net.kemitix.thorp.core package net.kemitix.thorp.config
final case class ConfigValidationException( final case class ConfigValidationException(
errors: List[ConfigValidation] errors: List[ConfigValidation]

View file

@ -1,15 +1,15 @@
package net.kemitix.thorp.core package net.kemitix.thorp.config
import java.nio.file.Path import java.nio.file.Path
import net.kemitix.thorp.domain.{Bucket, Config, Sources} import net.kemitix.thorp.domain.{Bucket, Sources}
import zio.IO import zio.IO
sealed trait ConfigValidator { sealed trait ConfigValidator {
def validateConfig( def validateConfig(
config: Config config: Configuration
): IO[List[ConfigValidation], Config] = IO.fromEither { ): IO[List[ConfigValidation], Configuration] = IO.fromEither {
for { for {
_ <- validateSources(config.sources) _ <- validateSources(config.sources)
_ <- validateBucket(config.bucket) _ <- validateBucket(config.bucket)

View file

@ -0,0 +1,29 @@
package net.kemitix.thorp.config
import net.kemitix.thorp.domain.{Bucket, Filter, RemoteKey, SimpleLens, Sources}
private[config] final case class Configuration(
bucket: Bucket = Bucket(""),
prefix: RemoteKey = RemoteKey(""),
filters: List[Filter] = List(),
debug: Boolean = false,
batchMode: Boolean = false,
sources: Sources = Sources(List())
)
private[config] object Configuration {
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))
}

View file

@ -1,12 +1,8 @@
package net.kemitix.thorp.core package net.kemitix.thorp.config
import java.nio.file.Paths import java.nio.file.Paths
import net.kemitix.thorp.core.ConfigOptions.options import zio.{IO, TaskR}
import net.kemitix.thorp.core.ConfigValidator.validateConfig
import net.kemitix.thorp.core.ParseConfigFile.parseFile
import net.kemitix.thorp.domain.Config
import zio.IO
/** /**
* Builds a configuration from settings in a file within the * Builds a configuration from settings in a file within the
@ -18,12 +14,13 @@ trait ConfigurationBuilder {
private val globalConfig = Paths.get("/etc/thorp.conf") private val globalConfig = Paths.get("/etc/thorp.conf")
private val userHome = Paths.get(System.getProperty("user.home")) private val userHome = Paths.get(System.getProperty("user.home"))
def buildConfig( def buildConfig(priorityOpts: ConfigOptions)
priorityOpts: ConfigOptions): IO[List[ConfigValidation], Config] = : IO[ConfigValidationException, Configuration] =
for { (for {
config <- getConfigOptions(priorityOpts).map(collateOptions) config <- getConfigOptions(priorityOpts).map(collateOptions)
valid <- validateConfig(config) valid <- ConfigValidator.validateConfig(config)
} yield valid } yield valid)
.catchAll(errors => TaskR.fail(ConfigValidationException(errors)))
private def getConfigOptions( private def getConfigOptions(
priorityOpts: ConfigOptions): IO[List[ConfigValidation], ConfigOptions] = priorityOpts: ConfigOptions): IO[List[ConfigValidation], ConfigOptions] =
@ -39,17 +36,17 @@ trait ConfigurationBuilder {
private def userOptions( private def userOptions(
priorityOpts: ConfigOptions): IO[List[ConfigValidation], ConfigOptions] = priorityOpts: ConfigOptions): IO[List[ConfigValidation], ConfigOptions] =
if (ConfigQuery.ignoreUserOptions(priorityOpts)) emptyConfig if (ConfigQuery.ignoreUserOptions(priorityOpts)) emptyConfig
else parseFile(userHome.resolve(userConfigFilename)) else ParseConfigFile.parseFile(userHome.resolve(userConfigFilename))
private def globalOptions( private def globalOptions(
priorityOpts: ConfigOptions): IO[List[ConfigValidation], ConfigOptions] = priorityOpts: ConfigOptions): IO[List[ConfigValidation], ConfigOptions] =
if (ConfigQuery.ignoreGlobalOptions(priorityOpts)) emptyConfig if (ConfigQuery.ignoreGlobalOptions(priorityOpts)) emptyConfig
else parseFile(globalConfig) else ParseConfigFile.parseFile(globalConfig)
private def collateOptions(configOptions: ConfigOptions): Config = private def collateOptions(configOptions: ConfigOptions): Configuration =
options ConfigOptions.options
.get(configOptions) .get(configOptions)
.foldLeft(Config()) { (config, configOption) => .foldLeft(Configuration()) { (config, configOption) =>
configOption.update(config) configOption.update(config)
} }

View file

@ -1,4 +1,4 @@
package net.kemitix.thorp.core package net.kemitix.thorp.config
import java.nio.file.{Files, Path} import java.nio.file.{Files, Path}

View file

@ -1,9 +1,16 @@
package net.kemitix.thorp.core package net.kemitix.thorp.config
import java.nio.file.Paths import java.nio.file.Paths
import java.util.regex.Pattern import java.util.regex.Pattern
import net.kemitix.thorp.core.ConfigOption._ import net.kemitix.thorp.config.ConfigOption.{
Bucket,
Debug,
Exclude,
Include,
Prefix,
Source
}
trait ParseConfigLines { trait ParseConfigLines {

View file

@ -1,4 +1,4 @@
package net.kemitix.thorp.core package net.kemitix.thorp.config
import java.io.{File, FileNotFoundException} import java.io.{File, FileNotFoundException}

View file

@ -1,4 +1,4 @@
package net.kemitix.thorp.core package net.kemitix.thorp.config
import net.kemitix.thorp.domain.Sources import net.kemitix.thorp.domain.Sources
import zio.IO import zio.IO

View file

@ -0,0 +1,29 @@
package net.kemitix.thorp
import net.kemitix.thorp.domain.{Bucket, Filter, RemoteKey, Sources}
import zio.ZIO
package object config {
final val configService: ZIO[Config, Nothing, Config.Service] =
ZIO.access(_.config)
final def setConfiguration(
config: Configuration): ZIO[Config, Nothing, Unit] =
ZIO.accessM(_.config setConfiguration config)
final def isBatchMode: ZIO[Config, Nothing, Boolean] =
ZIO.accessM(_.config isBatchMode)
final def getBucket: ZIO[Config, Nothing, Bucket] =
ZIO.accessM(_.config bucket)
final def getPrefix: ZIO[Config, Nothing, RemoteKey] =
ZIO.accessM(_.config prefix)
final def getSources: ZIO[Config, Nothing, Sources] =
ZIO.accessM(_.config sources)
final def getFilters: ZIO[Config, Nothing, List[Filter]] =
ZIO.accessM(_.config filters)
}

View file

@ -1,9 +1,8 @@
package net.kemitix.thorp.cli package net.kemitix.thorp.config
import java.nio.file.Paths import java.nio.file.Paths
import net.kemitix.thorp.core.ConfigOption.Debug import net.kemitix.thorp.config.ConfigOption.Debug
import net.kemitix.thorp.core.{ConfigOptions, ConfigQuery, Resource}
import org.scalatest.FunSpec import org.scalatest.FunSpec
import zio.DefaultRuntime import zio.DefaultRuntime

View file

@ -10,7 +10,7 @@ package object console {
final def putStrLn(line: String): ZIO[Console, Nothing, Unit] = final def putStrLn(line: String): ZIO[Console, Nothing, Unit] =
ZIO.accessM(_.console putStrLn line) ZIO.accessM(_.console putStrLn line)
final def putStrLn(line: ConsoleOut): ZIO[Console, Nothing, Unit] = final def putMessageLn(line: ConsoleOut): ZIO[Console, Nothing, Unit] =
ZIO.accessM(_.console putStrLn line) ZIO.accessM(_.console putStrLn line)
} }

View file

@ -1,39 +1,47 @@
package net.kemitix.thorp.core package net.kemitix.thorp.core
import net.kemitix.thorp.config._
import net.kemitix.thorp.core.Action.{DoNothing, ToCopy, ToUpload} import net.kemitix.thorp.core.Action.{DoNothing, ToCopy, ToUpload}
import net.kemitix.thorp.domain._ import net.kemitix.thorp.domain._
import zio.ZIO
object ActionGenerator { object ActionGenerator {
def createActions( def createActions(
s3MetaData: S3MetaData, s3MetaData: S3MetaData,
previousActions: Stream[Action] previousActions: Stream[Action]
)(implicit c: Config): Stream[Action] = ): ZIO[Config, Nothing, Stream[Action]] =
s3MetaData match { for {
// #1 local exists, remote exists, remote matches - do nothing bucket <- getBucket
case S3MetaData(localFile, _, Some(RemoteMetaData(key, hash, _))) } yield
if localFile.matches(hash) => s3MetaData match {
doNothing(c.bucket, key) // #1 local exists, remote exists, remote matches - do nothing
// #2 local exists, remote is missing, other matches - copy case S3MetaData(localFile, _, Some(RemoteMetaData(key, hash, _)))
case S3MetaData(localFile, matchByHash, None) if matchByHash.nonEmpty => if localFile.matches(hash) =>
copyFile(c.bucket, localFile, matchByHash) doNothing(bucket, key)
// #3 local exists, remote is missing, other no matches - upload // #2 local exists, remote is missing, other matches - copy
case S3MetaData(localFile, matchByHash, None) case S3MetaData(localFile, matchByHash, None) if matchByHash.nonEmpty =>
if matchByHash.isEmpty && copyFile(bucket, localFile, matchByHash)
isUploadAlreadyQueued(previousActions)(localFile) => // #3 local exists, remote is missing, other no matches - upload
uploadFile(c.bucket, localFile) case S3MetaData(localFile, matchByHash, None)
// #4 local exists, remote exists, remote no match, other matches - copy if matchByHash.isEmpty &&
case S3MetaData(localFile, matchByHash, Some(RemoteMetaData(_, hash, _))) isUploadAlreadyQueued(previousActions)(localFile) =>
if !localFile.matches(hash) && uploadFile(bucket, localFile)
matchByHash.nonEmpty => // #4 local exists, remote exists, remote no match, other matches - copy
copyFile(c.bucket, localFile, matchByHash) case S3MetaData(localFile,
// #5 local exists, remote exists, remote no match, other no matches - upload matchByHash,
case S3MetaData(localFile, matchByHash, Some(_)) if matchByHash.isEmpty => Some(RemoteMetaData(_, hash, _)))
uploadFile(c.bucket, localFile) if !localFile.matches(hash) &&
// fallback matchByHash.nonEmpty =>
case S3MetaData(localFile, _, _) => copyFile(bucket, localFile, matchByHash)
doNothing(c.bucket, localFile.remoteKey) // #5 local exists, remote exists, remote no match, other no matches - upload
} case S3MetaData(localFile, matchByHash, Some(_))
if matchByHash.isEmpty =>
uploadFile(bucket, localFile)
// fallback
case S3MetaData(localFile, _, _) =>
doNothing(bucket, localFile.remoteKey)
}
private def key = LocalFile.remoteKey ^|-> RemoteKey.key private def key = LocalFile.remoteKey ^|-> RemoteKey.key

View file

@ -1,12 +1,13 @@
package net.kemitix.thorp.core package net.kemitix.thorp.core
import net.kemitix.thorp.config.Config
import net.kemitix.thorp.console.Console import net.kemitix.thorp.console.Console
import net.kemitix.thorp.storage.api.Storage import net.kemitix.thorp.storage.api.Storage
import zio.ZIO import zio.ZIO
object CoreTypes { object CoreTypes {
type CoreEnv = Storage with Console type CoreEnv = Storage with Console with Config
type CoreProgram[A] = ZIO[CoreEnv, Throwable, A] type CoreProgram[A] = ZIO[CoreEnv, Throwable, A]
} }

View file

@ -2,42 +2,30 @@ package net.kemitix.thorp.core
import java.nio.file.Path import java.nio.file.Path
import net.kemitix.thorp.config._
import net.kemitix.thorp.core.KeyGenerator.generateKey import net.kemitix.thorp.core.KeyGenerator.generateKey
import net.kemitix.thorp.domain
import net.kemitix.thorp.domain._ import net.kemitix.thorp.domain._
import net.kemitix.thorp.storage.api.HashService import net.kemitix.thorp.storage.api.HashService
import zio.Task import zio.{Task, TaskR, ZIO}
object LocalFileStream { object LocalFileStream {
def findFiles( def findFiles(hashService: HashService)(
source: Path, source: Path
hashService: HashService ): TaskR[Config, LocalFiles] = {
)(
implicit c: Config
): Task[LocalFiles] = {
val isIncluded: Path => Boolean = Filter.isIncluded(c.filters) def recurseIntoSubDirectories(path: Path): TaskR[Config, LocalFiles] =
path.toFile match {
case f if f.isDirectory => loop(path)
case _ => pathToLocalFile(hashService)(path)
}
val pathToLocalFile: Path => Task[LocalFiles] = path => def recurse(paths: Stream[Path]): TaskR[Config, LocalFiles] =
localFile(hashService, c)(path) for {
recursed <- ZIO.foreach(paths)(path => recurseIntoSubDirectories(path))
} yield LocalFiles.reduce(recursed.toStream)
def loop(path: Path): Task[LocalFiles] = { def loop(path: Path): TaskR[Config, LocalFiles] = {
def dirPaths(path: Path): Task[Stream[Path]] =
listFiles(path)
.map(_.filter(isIncluded))
def recurseIntoSubDirectories(path: Path): Task[LocalFiles] =
path.toFile match {
case f if f.isDirectory => loop(path)
case _ => pathToLocalFile(path)
}
def recurse(paths: Stream[Path]): Task[LocalFiles] =
Task.foldLeft(paths)(LocalFiles())((acc, path) => {
recurseIntoSubDirectories(path).map(localFiles => acc ++ localFiles)
})
for { for {
paths <- dirPaths(path) paths <- dirPaths(path)
@ -48,30 +36,50 @@ object LocalFileStream {
loop(source) loop(source)
} }
def localFile( private def dirPaths(path: Path) =
hashService: HashService, for {
c: Config paths <- listFiles(path)
): Path => Task[LocalFiles] = filtered <- includedDirPaths(paths)
path => { } yield filtered
val file = path.toFile
val source = c.sources.forPath(path)
for {
hash <- hashService.hashLocalObject(path)
} yield
LocalFiles(localFiles = Stream(
domain.LocalFile(file,
source.toFile,
hash,
generateKey(c.sources, c.prefix)(path))),
count = 1,
totalSizeBytes = file.length)
}
private def listFiles(path: Path): Task[Stream[Path]] = private def includedDirPaths(paths: Stream[Path]) =
for {
flaggedPaths <- TaskR.foreach(paths)(path =>
isIncluded(path).map((path, _)))
} yield
flaggedPaths.toStream
.filter({ case (_, included) => included })
.map({ case (path, _) => path })
private def localFile(hashService: HashService)(path: Path) = {
val file = path.toFile
for {
sources <- getSources
prefix <- getPrefix
hash <- hashService.hashLocalObject(path)
localFile = LocalFile(file,
sources.forPath(path).toFile,
hash,
generateKey(sources, prefix)(path))
} yield
LocalFiles(localFiles = Stream(localFile),
count = 1,
totalSizeBytes = file.length)
}
private def listFiles(path: Path) =
for { for {
files <- Task(path.toFile.listFiles) files <- Task(path.toFile.listFiles)
_ <- Task.when(files == null)( _ <- Task.when(files == null)(
Task.fail(new IllegalArgumentException(s"Directory not found $path"))) Task.fail(new IllegalArgumentException(s"Directory not found $path")))
} yield Stream(files: _*).map(_.toPath) } yield Stream(files: _*).map(_.toPath)
private def isIncluded(path: Path) =
for {
filters <- getFilters
} yield Filter.isIncluded(filters)(path)
private def pathToLocalFile(hashService: HashService)(path: Path) =
localFile(hashService)(path)
} }

View file

@ -15,3 +15,10 @@ case class LocalFiles(
) )
} }
object LocalFiles {
def reduce: Stream[LocalFiles] => LocalFiles =
list => list.foldLeft(LocalFiles())((acc, lf) => acc ++ lf)
}

View file

@ -1,121 +1,111 @@
package net.kemitix.thorp.core package net.kemitix.thorp.core
import net.kemitix.thorp.config._
import net.kemitix.thorp.console._ import net.kemitix.thorp.console._
import net.kemitix.thorp.core.Action._ import net.kemitix.thorp.core.Action._
import net.kemitix.thorp.domain._ import net.kemitix.thorp.domain._
import net.kemitix.thorp.storage._ import net.kemitix.thorp.storage._
import net.kemitix.thorp.storage.api.{HashService, Storage} import net.kemitix.thorp.storage.api.{HashService, Storage}
import zio.{Task, TaskR} import zio.{TaskR, ZIO}
trait PlanBuilder { trait PlanBuilder {
def createPlan( def createPlan(hashService: HashService)
hashService: HashService, : TaskR[Storage with Console with Config, SyncPlan] =
configOptions: ConfigOptions
): TaskR[Storage with Console, SyncPlan] =
ConfigurationBuilder
.buildConfig(configOptions)
.catchAll(errors => TaskR.fail(ConfigValidationException(errors)))
.flatMap(config => useValidConfig(hashService)(config))
private def useValidConfig(
hashService: HashService
)(implicit c: Config) = {
for { for {
_ <- SyncLogging.logRunStart(c.bucket, c.prefix, c.sources) _ <- SyncLogging.logRunStart
actions <- buildPlan(hashService) actions <- buildPlan(hashService)
} yield actions } yield actions
}
private def buildPlan( private def buildPlan(hashService: HashService) =
hashService: HashService
)(implicit c: Config) =
for { for {
metadata <- gatherMetadata(hashService) metadata <- gatherMetadata(hashService)
} yield assemblePlan(c)(metadata) plan <- assemblePlan(metadata)
} yield plan
private def assemblePlan( private def assemblePlan(metadata: (S3ObjectsData, LocalFiles)) =
implicit c: Config): ((S3ObjectsData, LocalFiles)) => SyncPlan = { metadata match {
case (remoteData, localData) => case (remoteData, localData) =>
SyncPlan( createActions(remoteData, localData)
actions = createActions(c)(remoteData)(localData) .map(_.filter(doesSomething).sortBy(SequencePlan.order))
.filter(doesSomething) .map(
.sortBy(SequencePlan.order), SyncPlan(_, SyncTotals(localData.count, localData.totalSizeBytes)))
syncTotals = SyncTotals(count = localData.count, }
totalSizeBytes = localData.totalSizeBytes)
)
}
private def createActions private def createActions(
: Config => S3ObjectsData => LocalFiles => Stream[Action] = remoteData: S3ObjectsData,
c => localData: LocalFiles
remoteData => ) =
localData => for {
actionsForLocalFiles(c)(remoteData)(localData) ++ fileActions <- actionsForLocalFiles(remoteData, localData)
actionsForRemoteKeys(c)(remoteData) remoteActions <- actionsForRemoteKeys(remoteData)
} yield fileActions ++ remoteActions
private def doesSomething: Action => Boolean = { private def doesSomething: Action => Boolean = {
case _: DoNothing => false case _: DoNothing => false
case _ => true case _ => true
} }
private def actionsForLocalFiles private def actionsForLocalFiles(
: Config => S3ObjectsData => LocalFiles => Stream[Action] = remoteData: S3ObjectsData,
c => localData: LocalFiles
remoteData => ) =
localData => ZIO.foldLeft(localData.localFiles)(Stream.empty[Action])(
localData.localFiles.foldLeft(Stream.empty[Action])((acc, lf) => (acc, localFile) =>
createActionFromLocalFile(c)(lf)(remoteData)(acc) ++ acc) createActionFromLocalFile(remoteData, acc, localFile)
.map(actions => actions ++ acc))
private def createActionFromLocalFile private def createActionFromLocalFile(
: Config => LocalFile => S3ObjectsData => Stream[Action] => Stream[Action] = remoteData: S3ObjectsData,
c => previousActions: Stream[Action],
lf => localFile: LocalFile
remoteData => ) =
previousActions => ActionGenerator.createActions(
ActionGenerator.createActions( S3MetaDataEnricher.getMetadata(localFile, remoteData),
S3MetaDataEnricher.getMetadata(lf, remoteData)(c), previousActions)
previousActions)(c)
private def actionsForRemoteKeys: Config => S3ObjectsData => Stream[Action] = private def actionsForRemoteKeys(remoteData: S3ObjectsData) =
c => ZIO.foldLeft(remoteData.byKey.keys)(Stream.empty[Action]) {
remoteData => (acc, remoteKey) =>
remoteData.byKey.keys.foldLeft(Stream.empty[Action])((acc, rk) => createActionFromRemoteKey(remoteKey).map(action => action #:: acc)
createActionFromRemoteKey(c)(rk) #:: acc) }
private def createActionFromRemoteKey: Config => RemoteKey => Action = private def createActionFromRemoteKey(remoteKey: RemoteKey) =
c => for {
rk => bucket <- getBucket
if (rk.isMissingLocally(c.sources, c.prefix)) prefix <- getPrefix
Action.ToDelete(c.bucket, rk, 0L) sources <- getSources
else DoNothing(c.bucket, rk, 0L) needsDeleted = remoteKey.isMissingLocally(sources, prefix)
} yield
if (needsDeleted) ToDelete(bucket, remoteKey, 0L)
else DoNothing(bucket, remoteKey, 0L)
private def gatherMetadata( private def gatherMetadata(hashService: HashService) =
hashService: HashService
)(implicit c: Config) =
for { for {
remoteData <- fetchRemoteData remoteData <- fetchRemoteData
localData <- findLocalFiles(hashService) localData <- findLocalFiles(hashService)
} yield (remoteData, localData) } yield (remoteData, localData)
private def fetchRemoteData(implicit c: Config) = private def fetchRemoteData =
listObjects(c.bucket, c.prefix) for {
bucket <- getBucket
prefix <- getPrefix
objects <- listObjects(bucket, prefix)
} yield objects
private def findLocalFiles( private def findLocalFiles(hashService: HashService) =
hashService: HashService
)(implicit config: Config) =
for { for {
_ <- SyncLogging.logFileScan _ <- SyncLogging.logFileScan
localFiles <- findFiles(hashService) localFiles <- findFiles(hashService)
} yield localFiles } yield localFiles
private def findFiles( private def findFiles(hashService: HashService) =
hashService: HashService for {
)(implicit c: Config) = { sources <- getSources
Task paths = sources.paths
.foreach(c.sources.paths)(LocalFileStream.findFiles(_, hashService)) found <- ZIO.foreach(paths)(path =>
.map(_.foldLeft(LocalFiles())((acc, localFile) => acc ++ localFile)) LocalFileStream.findFiles(hashService)(path))
} } yield LocalFiles.reduce(found.toStream)
} }

View file

@ -7,7 +7,7 @@ object S3MetaDataEnricher {
def getMetadata( def getMetadata(
localFile: LocalFile, localFile: LocalFile,
s3ObjectsData: S3ObjectsData s3ObjectsData: S3ObjectsData
)(implicit c: Config): S3MetaData = { ): S3MetaData = {
val (keyMatches, hashMatches) = getS3Status(localFile, s3ObjectsData) val (keyMatches, hashMatches) = getS3Status(localFile, s3ObjectsData)
S3MetaData( S3MetaData(
localFile, localFile,

View file

@ -1,5 +1,6 @@
package net.kemitix.thorp.core package net.kemitix.thorp.core
import net.kemitix.thorp.config._
import net.kemitix.thorp.console._ import net.kemitix.thorp.console._
import net.kemitix.thorp.domain.StorageQueueEvent.{ import net.kemitix.thorp.domain.StorageQueueEvent.{
CopyQueueEvent, CopyQueueEvent,
@ -13,17 +14,19 @@ import zio.ZIO
trait SyncLogging { trait SyncLogging {
def logRunStart( def logRunStart: ZIO[Console with Config, Nothing, Unit] =
bucket: Bucket,
prefix: RemoteKey,
sources: Sources
): ZIO[Console, Nothing, Unit] =
for { for {
_ <- putStrLn(ConsoleOut.ValidConfig(bucket, prefix, sources)) bucket <- getBucket
prefix <- getPrefix
sources <- getSources
_ <- putMessageLn(ConsoleOut.ValidConfig(bucket, prefix, sources))
} yield () } yield ()
def logFileScan(implicit c: Config): ZIO[Console, Nothing, Unit] = def logFileScan: ZIO[Config with Console, Nothing, Unit] =
putStrLn(s"Scanning local files: ${c.sources.paths.mkString(", ")}...") for {
sources <- getSources
_ <- putStrLn(s"Scanning local files: ${sources.paths.mkString(", ")}...")
} yield ()
def logRunFinished( def logRunFinished(
actions: Stream[StorageQueueEvent] actions: Stream[StorageQueueEvent]

View file

@ -2,29 +2,35 @@ package net.kemitix.thorp.core
import java.time.Instant import java.time.Instant
import net.kemitix.thorp.config._
import net.kemitix.thorp.core.Action.{DoNothing, ToCopy, ToUpload} import net.kemitix.thorp.core.Action.{DoNothing, ToCopy, ToUpload}
import net.kemitix.thorp.domain.HashType.MD5 import net.kemitix.thorp.domain.HashType.MD5
import net.kemitix.thorp.domain._ import net.kemitix.thorp.domain._
import org.scalatest.FunSpec import org.scalatest.FunSpec
import zio.DefaultRuntime
class ActionGeneratorSuite extends FunSpec { class ActionGeneratorSuite extends FunSpec {
val lastModified = LastModified(Instant.now()) val lastModified = LastModified(Instant.now())
private val source = Resource(this, "upload") private val source = Resource(this, "upload")
private val sourcePath = source.toPath private val sourcePath = source.toPath
private val sources = Sources(List(sourcePath))
private val prefix = RemoteKey("prefix") private val prefix = RemoteKey("prefix")
private val bucket = Bucket("bucket") private val bucket = Bucket("bucket")
implicit private val config: Config = private val configOptions = ConfigOptions(
Config(bucket, prefix, sources = Sources(List(sourcePath))) List(
ConfigOption.Bucket("bucket"),
ConfigOption.Prefix("prefix"),
ConfigOption.Source(sourcePath),
ConfigOption.IgnoreUserOptions,
ConfigOption.IgnoreGlobalOptions
))
private val fileToKey = private val fileToKey =
KeyGenerator.generateKey(config.sources, config.prefix) _ KeyGenerator.generateKey(sources, prefix) _
describe("create actions") { describe("create actions") {
val previousActions = Stream.empty[Action] val previousActions = Stream.empty[Action]
def invoke(input: S3MetaData) =
ActionGenerator.createActions(input, previousActions).toList
describe("#1 local exists, remote exists, remote matches - do nothing") { describe("#1 local exists, remote exists, remote matches - do nothing") {
val theHash = MD5Hash("the-hash") val theHash = MD5Hash("the-hash")
val theFile = LocalFile.resolve("the-file", val theFile = LocalFile.resolve("the-file",
@ -40,8 +46,9 @@ class ActionGeneratorSuite extends FunSpec {
) )
it("do nothing") { it("do nothing") {
val expected = val expected =
List(DoNothing(bucket, theFile.remoteKey, theFile.file.length)) Right(
val result = invoke(input) Stream(DoNothing(bucket, theFile.remoteKey, theFile.file.length)))
val result = invoke(input, previousActions)
assertResult(expected)(result) assertResult(expected)(result)
} }
} }
@ -60,13 +67,14 @@ class ActionGeneratorSuite extends FunSpec {
matchByHash = Set(otherRemoteMetadata), // other matches matchByHash = Set(otherRemoteMetadata), // other matches
matchByKey = None) // remote is missing matchByKey = None) // remote is missing
it("copy from other key") { it("copy from other key") {
val expected = List( val expected = Right(
ToCopy(bucket, Stream(
otherRemoteKey, ToCopy(bucket,
theHash, otherRemoteKey,
theRemoteKey, theHash,
theFile.file.length)) // copy theRemoteKey,
val result = invoke(input) theFile.file.length))) // copy
val result = invoke(input, previousActions)
assertResult(expected)(result) assertResult(expected)(result)
} }
} }
@ -80,8 +88,9 @@ class ActionGeneratorSuite extends FunSpec {
matchByHash = Set.empty, // other no matches matchByHash = Set.empty, // other no matches
matchByKey = None) // remote is missing matchByKey = None) // remote is missing
it("upload") { it("upload") {
val expected = List(ToUpload(bucket, theFile, theFile.file.length)) // upload val expected = Right(
val result = invoke(input) Stream(ToUpload(bucket, theFile, theFile.file.length))) // upload
val result = invoke(input, previousActions)
assertResult(expected)(result) assertResult(expected)(result)
} }
} }
@ -105,13 +114,14 @@ class ActionGeneratorSuite extends FunSpec {
matchByHash = Set(otherRemoteMetadata), // other matches matchByHash = Set(otherRemoteMetadata), // other matches
matchByKey = Some(oldRemoteMetadata)) // remote exists matchByKey = Some(oldRemoteMetadata)) // remote exists
it("copy from other key") { it("copy from other key") {
val expected = List( val expected = Right(
ToCopy(bucket, Stream(
otherRemoteKey, ToCopy(bucket,
theHash, otherRemoteKey,
theRemoteKey, theHash,
theFile.file.length)) // copy theRemoteKey,
val result = invoke(input) theFile.file.length))) // copy
val result = invoke(input, previousActions)
assertResult(expected)(result) assertResult(expected)(result)
} }
} }
@ -132,8 +142,9 @@ class ActionGeneratorSuite extends FunSpec {
matchByKey = Some(theRemoteMetadata) // remote exists matchByKey = Some(theRemoteMetadata) // remote exists
) )
it("upload") { it("upload") {
val expected = List(ToUpload(bucket, theFile, theFile.file.length)) // upload val expected = Right(
val result = invoke(input) Stream(ToUpload(bucket, theFile, theFile.file.length))) // upload
val result = invoke(input, previousActions)
assertResult(expected)(result) assertResult(expected)(result)
} }
} }
@ -147,4 +158,23 @@ class ActionGeneratorSuite extends FunSpec {
private def md5HashMap(theHash: MD5Hash): Map[HashType, MD5Hash] = { private def md5HashMap(theHash: MD5Hash): Map[HashType, MD5Hash] = {
Map(MD5 -> theHash) Map(MD5 -> theHash)
} }
private def invoke(
input: S3MetaData,
previousActions: Stream[Action]
) = {
type TestEnv = Config
val testEnv: TestEnv = new Config.Live {}
def testProgram =
for {
config <- ConfigurationBuilder.buildConfig(configOptions)
_ <- setConfiguration(config)
actions <- ActionGenerator.createActions(input, previousActions)
} yield actions
new DefaultRuntime {}.unsafeRunSync {
testProgram.provide(testEnv)
}.toEither
}
} }

View file

@ -1,5 +1,11 @@
package net.kemitix.thorp.core package net.kemitix.thorp.core
import net.kemitix.thorp.config.{
ConfigOption,
ConfigOptions,
ConfigQuery,
ConfigurationBuilder
}
import net.kemitix.thorp.domain.Sources import net.kemitix.thorp.domain.Sources
import org.scalatest.FunSpec import org.scalatest.FunSpec
import zio.DefaultRuntime import zio.DefaultRuntime

View file

@ -2,6 +2,7 @@ package net.kemitix.thorp.core
import java.nio.file.Paths import java.nio.file.Paths
import net.kemitix.thorp.config.{ConfigOption, ConfigOptions, ConfigQuery}
import net.kemitix.thorp.domain.Sources import net.kemitix.thorp.domain.Sources
import org.scalatest.FreeSpec import org.scalatest.FreeSpec
@ -10,8 +11,8 @@ class ConfigQueryTest extends FreeSpec {
"show version" - { "show version" - {
"when is set" - { "when is set" - {
"should be true" in { "should be true" in {
val result = ConfigQuery.showVersion(ConfigOptions(List( val result =
ConfigOption.Version))) ConfigQuery.showVersion(ConfigOptions(List(ConfigOption.Version)))
assertResult(true)(result) assertResult(true)(result)
} }
} }
@ -25,8 +26,8 @@ class ConfigQueryTest extends FreeSpec {
"batch mode" - { "batch mode" - {
"when is set" - { "when is set" - {
"should be true" in { "should be true" in {
val result = ConfigQuery.batchMode(ConfigOptions(List( val result =
ConfigOption.BatchMode))) ConfigQuery.batchMode(ConfigOptions(List(ConfigOption.BatchMode)))
assertResult(true)(result) assertResult(true)(result)
} }
} }
@ -40,8 +41,8 @@ class ConfigQueryTest extends FreeSpec {
"ignore user options" - { "ignore user options" - {
"when is set" - { "when is set" - {
"should be true" in { "should be true" in {
val result = ConfigQuery.ignoreUserOptions(ConfigOptions(List( val result = ConfigQuery.ignoreUserOptions(
ConfigOption.IgnoreUserOptions))) ConfigOptions(List(ConfigOption.IgnoreUserOptions)))
assertResult(true)(result) assertResult(true)(result)
} }
} }
@ -55,8 +56,8 @@ class ConfigQueryTest extends FreeSpec {
"ignore global options" - { "ignore global options" - {
"when is set" - { "when is set" - {
"should be true" in { "should be true" in {
val result = ConfigQuery.ignoreGlobalOptions(ConfigOptions(List( val result = ConfigQuery.ignoreGlobalOptions(
ConfigOption.IgnoreGlobalOptions))) ConfigOptions(List(ConfigOption.IgnoreGlobalOptions)))
assertResult(true)(result) assertResult(true)(result)
} }
} }
@ -72,26 +73,26 @@ class ConfigQueryTest extends FreeSpec {
val pathB = Paths.get("b-path") val pathB = Paths.get("b-path")
"when not set" - { "when not set" - {
"should have current dir" - { "should have current dir" - {
val pwd = Paths.get(System.getenv("PWD")) val pwd = Paths.get(System.getenv("PWD"))
val expected = Sources(List(pwd)) val expected = Sources(List(pwd))
val result = ConfigQuery.sources(ConfigOptions(List())) val result = ConfigQuery.sources(ConfigOptions(List()))
assertResult(expected)(result) assertResult(expected)(result)
} }
} }
"when is set once" - { "when is set once" - {
"should have one source" in { "should have one source" in {
val expected = Sources(List(pathA)) val expected = Sources(List(pathA))
val result = ConfigQuery.sources(ConfigOptions(List( val result =
ConfigOption.Source(pathA)))) ConfigQuery.sources(ConfigOptions(List(ConfigOption.Source(pathA))))
assertResult(expected)(result) assertResult(expected)(result)
} }
} }
"when is set twice" - { "when is set twice" - {
"should have two sources" in { "should have two sources" in {
val expected = Sources(List(pathA, pathB)) val expected = Sources(List(pathA, pathB))
val result = ConfigQuery.sources(ConfigOptions(List( val result = ConfigQuery.sources(
ConfigOption.Source(pathA), ConfigOptions(
ConfigOption.Source(pathB)))) List(ConfigOption.Source(pathA), ConfigOption.Source(pathB))))
assertResult(expected)(result) assertResult(expected)(result)
} }
} }

View file

@ -2,6 +2,11 @@ package net.kemitix.thorp.core
import java.nio.file.{Path, Paths} import java.nio.file.{Path, Paths}
import net.kemitix.thorp.config.{
ConfigOption,
ConfigOptions,
ConfigurationBuilder
}
import net.kemitix.thorp.domain.Filter.{Exclude, Include} import net.kemitix.thorp.domain.Filter.{Exclude, Include}
import net.kemitix.thorp.domain._ import net.kemitix.thorp.domain._
import org.scalatest.FunSpec import org.scalatest.FunSpec

View file

@ -2,7 +2,8 @@ package net.kemitix.thorp.core
import java.io.File import java.io.File
import net.kemitix.thorp.domain.{Bucket, Config, RemoteKey, Sources} import net.kemitix.thorp.config.Resource
import net.kemitix.thorp.domain.{RemoteKey, Sources}
import org.scalatest.FunSpec import org.scalatest.FunSpec
class KeyGeneratorSuite extends FunSpec { class KeyGeneratorSuite extends FunSpec {
@ -10,10 +11,9 @@ class KeyGeneratorSuite extends FunSpec {
private val source: File = Resource(this, "upload") private val source: File = Resource(this, "upload")
private val sourcePath = source.toPath private val sourcePath = source.toPath
private val prefix = RemoteKey("prefix") private val prefix = RemoteKey("prefix")
implicit private val config: Config = private val sources = Sources(List(sourcePath))
Config(Bucket("bucket"), prefix, sources = Sources(List(sourcePath)))
private val fileToKey = private val fileToKey =
KeyGenerator.generateKey(config.sources, config.prefix) _ KeyGenerator.generateKey(sources, prefix) _
describe("key generator") { describe("key generator") {

View file

@ -2,11 +2,13 @@ package net.kemitix.thorp.core
import java.nio.file.Paths import java.nio.file.Paths
import net.kemitix.thorp.config._
import net.kemitix.thorp.console._
import net.kemitix.thorp.domain.HashType.MD5 import net.kemitix.thorp.domain.HashType.MD5
import net.kemitix.thorp.domain._ import net.kemitix.thorp.domain._
import net.kemitix.thorp.storage.api.HashService import net.kemitix.thorp.storage.api.{HashService, Storage}
import org.scalatest.FunSpec import org.scalatest.FunSpec
import zio.DefaultRuntime import zio.{DefaultRuntime, Task, UIO}
class LocalFileStreamSuite extends FunSpec { class LocalFileStreamSuite extends FunSpec {
@ -21,8 +23,13 @@ class LocalFileStreamSuite extends FunSpec {
private def file(filename: String) = private def file(filename: String) =
sourcePath.resolve(Paths.get(filename)) sourcePath.resolve(Paths.get(filename))
implicit private val config: Config = Config( private val configOptions = ConfigOptions(
sources = Sources(List(sourcePath))) List(
ConfigOption.IgnoreGlobalOptions,
ConfigOption.IgnoreUserOptions,
ConfigOption.Source(sourcePath),
ConfigOption.Bucket("aBucket")
))
describe("findFiles") { describe("findFiles") {
it("should find all files") { it("should find all files") {
@ -47,9 +54,29 @@ class LocalFileStreamSuite extends FunSpec {
} }
private def invoke() = { private def invoke() = {
val runtime = new DefaultRuntime {} type TestEnv = Storage with Console with Config
runtime.unsafeRunSync { val testEnv: TestEnv = new Storage.Test with Console.Test with Config.Live {
LocalFileStream.findFiles(sourcePath, hashService) override def listResult: Task[S3ObjectsData] =
Task.die(new NotImplementedError)
override def uploadResult: UIO[StorageQueueEvent] =
Task.die(new NotImplementedError)
override def copyResult: UIO[StorageQueueEvent] =
Task.die(new NotImplementedError)
override def deleteResult: UIO[StorageQueueEvent] =
Task.die(new NotImplementedError)
override def shutdownResult: UIO[StorageQueueEvent] =
Task.die(new NotImplementedError)
}
def testProgram =
for {
config <- ConfigurationBuilder.buildConfig(configOptions)
_ <- setConfiguration(config)
files <- LocalFileStream.findFiles(hashService)(sourcePath)
} yield files
new DefaultRuntime {}.unsafeRunSync {
testProgram.provide(testEnv)
}.toEither }.toEither
} }

View file

@ -2,8 +2,8 @@ package net.kemitix.thorp.core
import java.nio.file.Path import java.nio.file.Path
import net.kemitix.thorp.config.Resource
import net.kemitix.thorp.domain.MD5HashData.{BigFile, Root} import net.kemitix.thorp.domain.MD5HashData.{BigFile, Root}
import net.kemitix.thorp.domain._
import org.scalatest.FunSpec import org.scalatest.FunSpec
import zio.DefaultRuntime import zio.DefaultRuntime
@ -11,11 +11,7 @@ class MD5HashGeneratorTest extends FunSpec {
private val runtime = new DefaultRuntime {} private val runtime = new DefaultRuntime {}
private val source = Resource(this, "upload") private val source = Resource(this, "upload")
private val sourcePath = source.toPath
private val prefix = RemoteKey("prefix")
implicit private val config: Config =
Config(Bucket("bucket"), prefix, sources = Sources(List(sourcePath)))
describe("md5File()") { describe("md5File()") {
describe("read a small file (smaller than buffer)") { describe("read a small file (smaller than buffer)") {

View file

@ -2,6 +2,12 @@ package net.kemitix.thorp.core
import java.nio.file.{Path, Paths} import java.nio.file.{Path, Paths}
import net.kemitix.thorp.config.{
ConfigOption,
ConfigOptions,
ParseConfigFile,
Resource
}
import org.scalatest.FunSpec import org.scalatest.FunSpec
import zio.DefaultRuntime import zio.DefaultRuntime

View file

@ -2,6 +2,7 @@ package net.kemitix.thorp.core
import java.nio.file.Paths import java.nio.file.Paths
import net.kemitix.thorp.config.{ConfigOption, ConfigOptions, ParseConfigLines}
import org.scalatest.FunSpec import org.scalatest.FunSpec
class ParseConfigLinesTest extends FunSpec { class ParseConfigLinesTest extends FunSpec {

View file

@ -3,6 +3,7 @@ package net.kemitix.thorp.core
import java.io.File import java.io.File
import java.nio.file.Path import java.nio.file.Path
import net.kemitix.thorp.config._
import net.kemitix.thorp.console._ import net.kemitix.thorp.console._
import net.kemitix.thorp.core.Action.{DoNothing, ToCopy, ToDelete, ToUpload} import net.kemitix.thorp.core.Action.{DoNothing, ToCopy, ToDelete, ToUpload}
import net.kemitix.thorp.domain.HashType.MD5 import net.kemitix.thorp.domain.HashType.MD5
@ -24,7 +25,9 @@ class PlanBuilderTest extends FreeSpec with TemporaryFolder {
val options: Path => ConfigOptions = val options: Path => ConfigOptions =
source => source =>
configOptions(ConfigOption.Source(source), configOptions(ConfigOption.Source(source),
ConfigOption.Bucket("a-bucket")) ConfigOption.Bucket("a-bucket"),
ConfigOption.IgnoreUserOptions,
ConfigOption.IgnoreGlobalOptions)
"a file" - { "a file" - {
val filename = "aFile" val filename = "aFile"
val remoteKey = RemoteKey(filename) val remoteKey = RemoteKey(filename)
@ -325,9 +328,9 @@ class PlanBuilderTest extends FreeSpec with TemporaryFolder {
hashService: HashService, hashService: HashService,
configOptions: ConfigOptions, configOptions: ConfigOptions,
result: Task[S3ObjectsData] result: Task[S3ObjectsData]
): Either[Any, List[(String, String, String, String, String)]] = { ) = {
type TestEnv = Storage.Test with Console.Test type TestEnv = Storage with Console with Config
val testEnv: TestEnv = new Storage.Test with Console.Test { val testEnv: TestEnv = new Storage.Test with Console.Test with Config.Live {
override def listResult: Task[S3ObjectsData] = result override def listResult: Task[S3ObjectsData] = result
override def uploadResult: UIO[StorageQueueEvent] = override def uploadResult: UIO[StorageQueueEvent] =
Task.die(new NotImplementedError) Task.die(new NotImplementedError)
@ -339,12 +342,15 @@ class PlanBuilderTest extends FreeSpec with TemporaryFolder {
Task.die(new NotImplementedError) Task.die(new NotImplementedError)
} }
def testProgram =
for {
config <- ConfigurationBuilder.buildConfig(configOptions)
_ <- setConfiguration(config)
plan <- PlanBuilder.createPlan(hashService)
} yield plan
new DefaultRuntime {} new DefaultRuntime {}
.unsafeRunSync { .unsafeRunSync(testProgram.provide(testEnv))
PlanBuilder
.createPlan(hashService, configOptions)
.provide(testEnv)
}
.toEither .toEither
.map(convertResult) .map(convertResult)
} }

View file

@ -2,6 +2,7 @@ package net.kemitix.thorp.core
import java.time.Instant import java.time.Instant
import net.kemitix.thorp.config.Resource
import net.kemitix.thorp.core.S3MetaDataEnricher.{getMetadata, getS3Status} import net.kemitix.thorp.core.S3MetaDataEnricher.{getMetadata, getS3Status}
import net.kemitix.thorp.domain.HashType.MD5 import net.kemitix.thorp.domain.HashType.MD5
import net.kemitix.thorp.domain._ import net.kemitix.thorp.domain._
@ -11,11 +12,10 @@ class S3MetaDataEnricherSuite extends FunSpec {
val lastModified = LastModified(Instant.now()) val lastModified = LastModified(Instant.now())
private val source = Resource(this, "upload") private val source = Resource(this, "upload")
private val sourcePath = source.toPath private val sourcePath = source.toPath
private val sources = Sources(List(sourcePath))
private val prefix = RemoteKey("prefix") private val prefix = RemoteKey("prefix")
implicit private val config: Config =
Config(Bucket("bucket"), prefix, sources = Sources(List(sourcePath)))
private val fileToKey = private val fileToKey =
KeyGenerator.generateKey(config.sources, config.prefix) _ KeyGenerator.generateKey(sources, prefix) _
def getMatchesByKey( def getMatchesByKey(
status: (Option[HashModified], Set[(MD5Hash, KeyModified)])) status: (Option[HashModified], Set[(MD5Hash, KeyModified)]))

View file

@ -1,25 +0,0 @@
package net.kemitix.thorp.domain
final case class Config(
bucket: Bucket = Bucket(""),
prefix: RemoteKey = RemoteKey(""),
filters: List[Filter] = List(),
debug: Boolean = false,
batchMode: Boolean = false,
sources: Sources = Sources(List())
)
object Config {
val sources: SimpleLens[Config, Sources] =
SimpleLens[Config, Sources](_.sources, b => a => b.copy(sources = a))
val bucket: SimpleLens[Config, Bucket] =
SimpleLens[Config, Bucket](_.bucket, b => a => b.copy(bucket = a))
val prefix: SimpleLens[Config, RemoteKey] =
SimpleLens[Config, RemoteKey](_.prefix, b => a => b.copy(prefix = a))
val filters: SimpleLens[Config, List[Filter]] =
SimpleLens[Config, List[Filter]](_.filters, b => a => b.copy(filters = a))
val debug: SimpleLens[Config, Boolean] =
SimpleLens[Config, Boolean](_.debug, b => a => b.copy(debug = a))
val batchMode: SimpleLens[Config, Boolean] =
SimpleLens[Config, Boolean](_.batchMode, b => a => b.copy(batchMode = a))
}

View file

@ -3,7 +3,7 @@ package net.kemitix.thorp.storage.aws
import java.nio.file.Path import java.nio.file.Path
import com.amazonaws.services.s3.transfer.TransferManagerConfiguration import com.amazonaws.services.s3.transfer.TransferManagerConfiguration
import net.kemitix.thorp.core.Resource import net.kemitix.thorp.config.Resource
import org.scalatest.FunSpec import org.scalatest.FunSpec
import zio.DefaultRuntime import zio.DefaultRuntime

View file

@ -2,7 +2,8 @@ package net.kemitix.thorp.storage.aws
import java.time.Instant import java.time.Instant
import net.kemitix.thorp.core.{KeyGenerator, Resource, S3MetaDataEnricher} import net.kemitix.thorp.config.Resource
import net.kemitix.thorp.core.{KeyGenerator, S3MetaDataEnricher}
import net.kemitix.thorp.domain.HashType.MD5 import net.kemitix.thorp.domain.HashType.MD5
import net.kemitix.thorp.domain._ import net.kemitix.thorp.domain._
import org.scalamock.scalatest.MockFactory import org.scalamock.scalatest.MockFactory
@ -12,12 +13,10 @@ class StorageServiceSuite extends FunSpec with MockFactory {
private val source = Resource(this, "upload") private val source = Resource(this, "upload")
private val sourcePath = source.toPath private val sourcePath = source.toPath
private val sources = Sources(List(sourcePath))
private val prefix = RemoteKey("prefix") private val prefix = RemoteKey("prefix")
implicit private val config: Config =
Config(Bucket("bucket"), prefix, sources = Sources(List(sourcePath)))
private val fileToKey = private val fileToKey =
KeyGenerator.generateKey(config.sources, config.prefix) _ KeyGenerator.generateKey(sources, prefix) _
describe("getS3Status") { describe("getS3Status") {

View file

@ -5,8 +5,8 @@ import java.io.File
import com.amazonaws.SdkClientException import com.amazonaws.SdkClientException
import com.amazonaws.services.s3.model.AmazonS3Exception import com.amazonaws.services.s3.model.AmazonS3Exception
import com.amazonaws.services.s3.transfer.model.UploadResult import com.amazonaws.services.s3.transfer.model.UploadResult
import net.kemitix.thorp.config.Resource
import net.kemitix.thorp.console._ import net.kemitix.thorp.console._
import net.kemitix.thorp.core.Resource
import net.kemitix.thorp.domain.HashType.MD5 import net.kemitix.thorp.domain.HashType.MD5
import net.kemitix.thorp.domain.StorageQueueEvent.{ import net.kemitix.thorp.domain.StorageQueueEvent.{
Action, Action,