Install WartRemover pluging (#150)
* [sbt] Install WartRemover * remove warts * remote warts * fix tests * [domain] UploadEventListener fix progress bar (again) * [domain] Remove LastModified - wasn't being used for anything
This commit is contained in:
parent
9a6208025c
commit
af7733952c
85 changed files with 645 additions and 631 deletions
|
@ -26,6 +26,7 @@ val commonSettings = Seq(
|
||||||
"-language:postfixOps",
|
"-language:postfixOps",
|
||||||
"-language:higherKinds",
|
"-language:higherKinds",
|
||||||
"-Ypartial-unification"),
|
"-Ypartial-unification"),
|
||||||
|
wartremoverErrors ++= Warts.unsafe.filterNot(wart => List(Wart.Any, Wart.Nothing, Wart.Serializable).contains(wart)),
|
||||||
test in assembly := {}
|
test in assembly := {}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ object CliArgs {
|
||||||
OParser
|
OParser
|
||||||
.parse(configParser, args, List())
|
.parse(configParser, args, List())
|
||||||
.map(ConfigOptions(_))
|
.map(ConfigOptions(_))
|
||||||
.getOrElse(ConfigOptions())
|
.getOrElse(ConfigOptions.empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
val configParser: OParser[Unit, List[ConfigOption]] = {
|
val configParser: OParser[Unit, List[ConfigOption]] = {
|
||||||
|
|
|
@ -23,7 +23,7 @@ object Config {
|
||||||
trait Live extends Config {
|
trait Live extends Config {
|
||||||
|
|
||||||
val config: Service = new Service {
|
val config: Service = new Service {
|
||||||
private val configRef = new AtomicReference(Configuration())
|
private val configRef = new AtomicReference(Configuration.empty)
|
||||||
override def setConfiguration(
|
override def setConfiguration(
|
||||||
config: Configuration): ZIO[Config, Nothing, Unit] =
|
config: Configuration): ZIO[Config, Nothing, Unit] =
|
||||||
UIO(configRef.set(config))
|
UIO(configRef.set(config))
|
||||||
|
|
|
@ -12,12 +12,12 @@ sealed trait ConfigOption {
|
||||||
|
|
||||||
object ConfigOption {
|
object ConfigOption {
|
||||||
|
|
||||||
case class Source(path: Path) extends ConfigOption {
|
final case class Source(path: Path) extends ConfigOption {
|
||||||
override def update(config: Configuration): Configuration =
|
override def update(config: Configuration): Configuration =
|
||||||
sources.modify(_ + path)(config)
|
sources.modify(_ + path)(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
case class Bucket(name: String) extends ConfigOption {
|
final case class Bucket(name: String) extends ConfigOption {
|
||||||
override def update(config: Configuration): Configuration =
|
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)
|
||||||
|
@ -25,7 +25,7 @@ object ConfigOption {
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
case class Prefix(path: String) extends ConfigOption {
|
final case class Prefix(path: String) extends ConfigOption {
|
||||||
override def update(config: Configuration): Configuration =
|
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)
|
||||||
|
@ -33,17 +33,17 @@ object ConfigOption {
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
case class Include(pattern: String) extends ConfigOption {
|
final case class Include(pattern: String) extends ConfigOption {
|
||||||
override def update(config: Configuration): Configuration =
|
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 {
|
final case class Exclude(pattern: String) extends ConfigOption {
|
||||||
override def update(config: Configuration): Configuration =
|
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 {
|
final case class Debug() extends ConfigOption {
|
||||||
override def update(config: Configuration): Configuration =
|
override def update(config: Configuration): Configuration =
|
||||||
debug.set(true)(config)
|
debug.set(true)(config)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,29 +2,27 @@ package net.kemitix.thorp.config
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.SimpleLens
|
import net.kemitix.thorp.domain.SimpleLens
|
||||||
|
|
||||||
case class ConfigOptions(
|
final case class ConfigOptions(options: List[ConfigOption]) {
|
||||||
options: List[ConfigOption] = List()
|
|
||||||
) {
|
|
||||||
|
|
||||||
def combine(
|
|
||||||
x: ConfigOptions,
|
|
||||||
y: ConfigOptions
|
|
||||||
): ConfigOptions =
|
|
||||||
x ++ y
|
|
||||||
|
|
||||||
def ++(other: ConfigOptions): ConfigOptions =
|
def ++(other: ConfigOptions): ConfigOptions =
|
||||||
ConfigOptions(options ++ other.options)
|
ConfigOptions.combine(this, other)
|
||||||
|
|
||||||
def ::(head: ConfigOption): ConfigOptions =
|
def ::(head: ConfigOption): ConfigOptions =
|
||||||
ConfigOptions(head :: options)
|
ConfigOptions(head :: options)
|
||||||
|
|
||||||
def contains[A1 >: ConfigOption](elem: A1): Boolean =
|
|
||||||
options contains elem
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object ConfigOptions {
|
object ConfigOptions {
|
||||||
|
val empty: ConfigOptions = ConfigOptions(List.empty)
|
||||||
val options: SimpleLens[ConfigOptions, List[ConfigOption]] =
|
val options: SimpleLens[ConfigOptions, List[ConfigOption]] =
|
||||||
SimpleLens[ConfigOptions, List[ConfigOption]](_.options,
|
SimpleLens[ConfigOptions, List[ConfigOption]](_.options,
|
||||||
c => a => c.copy(options = a))
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,26 +7,27 @@ import net.kemitix.thorp.domain.Sources
|
||||||
trait ConfigQuery {
|
trait ConfigQuery {
|
||||||
|
|
||||||
def showVersion(configOptions: ConfigOptions): Boolean =
|
def showVersion(configOptions: ConfigOptions): Boolean =
|
||||||
configOptions contains ConfigOption.Version
|
ConfigOptions.contains(ConfigOption.Version)(configOptions)
|
||||||
|
|
||||||
def batchMode(configOptions: ConfigOptions): Boolean =
|
def batchMode(configOptions: ConfigOptions): Boolean =
|
||||||
configOptions contains ConfigOption.BatchMode
|
ConfigOptions.contains(ConfigOption.BatchMode)(configOptions)
|
||||||
|
|
||||||
def ignoreUserOptions(configOptions: ConfigOptions): Boolean =
|
def ignoreUserOptions(configOptions: ConfigOptions): Boolean =
|
||||||
configOptions contains ConfigOption.IgnoreUserOptions
|
ConfigOptions.contains(ConfigOption.IgnoreUserOptions)(configOptions)
|
||||||
|
|
||||||
def ignoreGlobalOptions(configOptions: ConfigOptions): Boolean =
|
def ignoreGlobalOptions(configOptions: ConfigOptions): Boolean =
|
||||||
configOptions contains ConfigOption.IgnoreGlobalOptions
|
ConfigOptions.contains(ConfigOption.IgnoreGlobalOptions)(configOptions)
|
||||||
|
|
||||||
def sources(configOptions: ConfigOptions): Sources = {
|
def sources(configOptions: ConfigOptions): Sources = {
|
||||||
val paths = configOptions.options.flatMap {
|
val explicitPaths = configOptions.options.flatMap {
|
||||||
case ConfigOption.Source(sourcePath) => Some(sourcePath)
|
case ConfigOption.Source(sourcePath) => List(sourcePath)
|
||||||
case _ => None
|
case _ => List.empty
|
||||||
}
|
}
|
||||||
Sources(paths match {
|
val paths = explicitPaths match {
|
||||||
case List() => List(Paths.get(System.getenv("PWD")))
|
case List() => List(Paths.get(System.getenv("PWD")))
|
||||||
case _ => paths
|
case _ => explicitPaths
|
||||||
})
|
}
|
||||||
|
Sources(paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ object ConfigValidation {
|
||||||
override def errorMessage: String = "Bucket name is missing"
|
override def errorMessage: String = "Bucket name is missing"
|
||||||
}
|
}
|
||||||
|
|
||||||
case class ErrorReadingFile(
|
final case class ErrorReadingFile(
|
||||||
file: File,
|
file: File,
|
||||||
message: String
|
message: String
|
||||||
) extends ConfigValidation {
|
) extends ConfigValidation {
|
||||||
|
|
|
@ -3,15 +3,23 @@ package net.kemitix.thorp.config
|
||||||
import net.kemitix.thorp.domain.{Bucket, Filter, RemoteKey, SimpleLens, Sources}
|
import net.kemitix.thorp.domain.{Bucket, Filter, RemoteKey, SimpleLens, Sources}
|
||||||
|
|
||||||
private[config] final case class Configuration(
|
private[config] final case class Configuration(
|
||||||
bucket: Bucket = Bucket(""),
|
bucket: Bucket,
|
||||||
prefix: RemoteKey = RemoteKey(""),
|
prefix: RemoteKey,
|
||||||
filters: List[Filter] = List(),
|
filters: List[Filter],
|
||||||
debug: Boolean = false,
|
debug: Boolean,
|
||||||
batchMode: Boolean = false,
|
batchMode: Boolean,
|
||||||
sources: Sources = Sources(List())
|
sources: Sources
|
||||||
)
|
)
|
||||||
|
|
||||||
private[config] object Configuration {
|
private[config] object Configuration {
|
||||||
|
val empty: Configuration = Configuration(
|
||||||
|
bucket = Bucket(""),
|
||||||
|
prefix = RemoteKey(""),
|
||||||
|
filters = List.empty,
|
||||||
|
debug = false,
|
||||||
|
batchMode = false,
|
||||||
|
sources = Sources(List.empty)
|
||||||
|
)
|
||||||
val sources: SimpleLens[Configuration, Sources] =
|
val sources: SimpleLens[Configuration, Sources] =
|
||||||
SimpleLens[Configuration, Sources](_.sources, b => a => b.copy(sources = a))
|
SimpleLens[Configuration, Sources](_.sources, b => a => b.copy(sources = a))
|
||||||
val bucket: SimpleLens[Configuration, Bucket] =
|
val bucket: SimpleLens[Configuration, Bucket] =
|
||||||
|
|
|
@ -29,7 +29,7 @@ trait ConfigurationBuilder {
|
||||||
globalOpts <- globalOptions(priorityOpts ++ sourceOpts ++ userOpts)
|
globalOpts <- globalOptions(priorityOpts ++ sourceOpts ++ userOpts)
|
||||||
} yield priorityOpts ++ sourceOpts ++ userOpts ++ globalOpts
|
} yield priorityOpts ++ sourceOpts ++ userOpts ++ globalOpts
|
||||||
|
|
||||||
private val emptyConfig = ZIO.succeed(ConfigOptions())
|
private val emptyConfig = ZIO.succeed(ConfigOptions.empty)
|
||||||
|
|
||||||
private def userOptions(priorityOpts: ConfigOptions) =
|
private def userOptions(priorityOpts: ConfigOptions) =
|
||||||
if (ConfigQuery.ignoreUserOptions(priorityOpts)) emptyConfig
|
if (ConfigQuery.ignoreUserOptions(priorityOpts)) emptyConfig
|
||||||
|
@ -42,7 +42,7 @@ trait ConfigurationBuilder {
|
||||||
private def collateOptions(configOptions: ConfigOptions): Configuration =
|
private def collateOptions(configOptions: ConfigOptions): Configuration =
|
||||||
ConfigOptions.options
|
ConfigOptions.options
|
||||||
.get(configOptions)
|
.get(configOptions)
|
||||||
.foldLeft(Configuration()) { (config, configOption) =>
|
.foldLeft(Configuration.empty) { (config, configOption) =>
|
||||||
configOption.update(config)
|
configOption.update(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,21 +17,21 @@ trait ParseConfigLines {
|
||||||
private def parseLine(str: String) =
|
private def parseLine(str: String) =
|
||||||
format.matcher(str) match {
|
format.matcher(str) match {
|
||||||
case m if m.matches => parseKeyValue(m.group("key"), m.group("value"))
|
case m if m.matches => parseKeyValue(m.group("key"), m.group("value"))
|
||||||
case _ => None
|
case _ => List.empty
|
||||||
}
|
}
|
||||||
|
|
||||||
private def parseKeyValue(
|
private def parseKeyValue(
|
||||||
key: String,
|
key: String,
|
||||||
value: String
|
value: String
|
||||||
): Option[ConfigOption] =
|
): List[ConfigOption] =
|
||||||
key.toLowerCase match {
|
key.toLowerCase match {
|
||||||
case "source" => Some(Source(Paths.get(value)))
|
case "source" => List(Source(Paths.get(value)))
|
||||||
case "bucket" => Some(Bucket(value))
|
case "bucket" => List(Bucket(value))
|
||||||
case "prefix" => Some(Prefix(value))
|
case "prefix" => List(Prefix(value))
|
||||||
case "include" => Some(Include(value))
|
case "include" => List(Include(value))
|
||||||
case "exclude" => Some(Exclude(value))
|
case "exclude" => List(Exclude(value))
|
||||||
case "debug" => if (truthy(value)) Some(Debug()) else None
|
case "debug" => if (truthy(value)) List(Debug()) else List.empty
|
||||||
case _ => None
|
case _ => List.empty
|
||||||
}
|
}
|
||||||
|
|
||||||
private def truthy(value: String): Boolean =
|
private def truthy(value: String): Boolean =
|
||||||
|
|
|
@ -1,16 +1,11 @@
|
||||||
package net.kemitix.thorp.config
|
package net.kemitix.thorp.config
|
||||||
|
|
||||||
import java.io.{File, FileNotFoundException}
|
import java.io.File
|
||||||
|
|
||||||
import scala.util.Try
|
|
||||||
|
|
||||||
object Resource {
|
object Resource {
|
||||||
|
|
||||||
def apply(
|
def apply(
|
||||||
base: AnyRef,
|
base: AnyRef,
|
||||||
name: String
|
name: String
|
||||||
): File =
|
): File = new File(base.getClass.getResource(name).getPath)
|
||||||
Try {
|
|
||||||
new File(base.getClass.getResource(name).getPath)
|
|
||||||
}.getOrElse(throw new FileNotFoundException(name))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,25 +45,27 @@ class CliArgsTest extends FunSpec {
|
||||||
val strings = List("--source", pathTo("."), "--bucket", "bucket", arg)
|
val strings = List("--source", pathTo("."), "--bucket", "bucket", arg)
|
||||||
.filter(_ != "")
|
.filter(_ != "")
|
||||||
val maybeOptions = invoke(strings)
|
val maybeOptions = invoke(strings)
|
||||||
maybeOptions.getOrElse(ConfigOptions())
|
maybeOptions.getOrElse(ConfigOptions.empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val containsDebug = ConfigOptions.contains(Debug())(_)
|
||||||
|
|
||||||
describe("when no debug flag") {
|
describe("when no debug flag") {
|
||||||
val configOptions = invokeWithArgument("")
|
val configOptions = invokeWithArgument("")
|
||||||
it("debug should be false") {
|
it("debug should be false") {
|
||||||
assertResult(false)(configOptions.contains(Debug()))
|
assertResult(false)(containsDebug(configOptions))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
describe("when long debug flag") {
|
describe("when long debug flag") {
|
||||||
val configOptions = invokeWithArgument("--debug")
|
val configOptions = invokeWithArgument("--debug")
|
||||||
it("debug should be true") {
|
it("debug should be true") {
|
||||||
assert(configOptions.contains(Debug()))
|
assert(containsDebug(configOptions))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
describe("when short debug flag") {
|
describe("when short debug flag") {
|
||||||
val configOptions = invokeWithArgument("-d")
|
val configOptions = invokeWithArgument("-d")
|
||||||
it("debug should be true") {
|
it("debug should be true") {
|
||||||
assert(configOptions.contains(Debug()))
|
assert(containsDebug(configOptions))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ class ConfigOptionTest extends FunSpec with TemporaryFolder {
|
||||||
withDirectory(path1 => {
|
withDirectory(path1 => {
|
||||||
withDirectory(path2 => {
|
withDirectory(path2 => {
|
||||||
val configOptions = ConfigOptions(
|
val configOptions = ConfigOptions(
|
||||||
List(
|
List[ConfigOption](
|
||||||
ConfigOption.Source(path1),
|
ConfigOption.Source(path1),
|
||||||
ConfigOption.Source(path2),
|
ConfigOption.Source(path2),
|
||||||
ConfigOption.Bucket("bucket"),
|
ConfigOption.Bucket("bucket"),
|
||||||
|
|
|
@ -2,6 +2,7 @@ package net.kemitix.thorp.config
|
||||||
|
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
import net.kemitix.thorp.domain.NonUnit.~*
|
||||||
import net.kemitix.thorp.domain.Sources
|
import net.kemitix.thorp.domain.Sources
|
||||||
import org.scalatest.FreeSpec
|
import org.scalatest.FreeSpec
|
||||||
|
|
||||||
|
@ -75,7 +76,7 @@ class ConfigQueryTest extends FreeSpec {
|
||||||
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" - {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package net.kemitix.thorp.config
|
||||||
import java.nio.file.{Path, Paths}
|
import java.nio.file.{Path, Paths}
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.Filter.{Exclude, Include}
|
import net.kemitix.thorp.domain.Filter.{Exclude, Include}
|
||||||
|
import net.kemitix.thorp.domain.NonUnit.~*
|
||||||
import net.kemitix.thorp.domain._
|
import net.kemitix.thorp.domain._
|
||||||
import net.kemitix.thorp.filesystem.FileSystem
|
import net.kemitix.thorp.filesystem.FileSystem
|
||||||
import org.scalatest.FunSpec
|
import org.scalatest.FunSpec
|
||||||
|
@ -17,7 +18,7 @@ class ConfigurationBuilderTest extends FunSpec with TemporaryFolder {
|
||||||
|
|
||||||
private def configOptions(options: ConfigOption*): ConfigOptions =
|
private def configOptions(options: ConfigOption*): ConfigOptions =
|
||||||
ConfigOptions(
|
ConfigOptions(
|
||||||
List(
|
List[ConfigOption](
|
||||||
ConfigOption.IgnoreUserOptions,
|
ConfigOption.IgnoreUserOptions,
|
||||||
ConfigOption.IgnoreGlobalOptions
|
ConfigOption.IgnoreGlobalOptions
|
||||||
) ++ options)
|
) ++ options)
|
||||||
|
@ -34,7 +35,7 @@ class ConfigurationBuilderTest extends FunSpec with TemporaryFolder {
|
||||||
describe("with .thorp.conf") {
|
describe("with .thorp.conf") {
|
||||||
describe("with settings") {
|
describe("with settings") {
|
||||||
withDirectory(source => {
|
withDirectory(source => {
|
||||||
val configFileName = createFile(source,
|
writeFile(source,
|
||||||
thorpConfigFileName,
|
thorpConfigFileName,
|
||||||
"bucket = a-bucket",
|
"bucket = a-bucket",
|
||||||
"prefix = a-prefix",
|
"prefix = a-prefix",
|
||||||
|
@ -51,7 +52,8 @@ class ConfigurationBuilderTest extends FunSpec with TemporaryFolder {
|
||||||
}
|
}
|
||||||
it("should have filters") {
|
it("should have filters") {
|
||||||
val expected =
|
val expected =
|
||||||
Right(List(Exclude("an-exclusion"), Include("an-inclusion")))
|
Right(
|
||||||
|
List[Filter](Exclude("an-exclusion"), Include("an-inclusion")))
|
||||||
assertResult(expected)(result.map(_.filters))
|
assertResult(expected)(result.map(_.filters))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -125,14 +127,14 @@ class ConfigurationBuilderTest extends FunSpec with TemporaryFolder {
|
||||||
val expectedPrefixes = Right(RemoteKey("current-prefix"))
|
val expectedPrefixes = Right(RemoteKey("current-prefix"))
|
||||||
// should have filters from both sources
|
// should have filters from both sources
|
||||||
val expectedFilters = Right(
|
val expectedFilters = Right(
|
||||||
List(Filter.Exclude("current-exclude"),
|
List[Filter](Filter.Exclude("current-exclude"),
|
||||||
Filter.Include("current-include")))
|
Filter.Include("current-include")))
|
||||||
val options = configOptions(ConfigOption.Source(currentSource))
|
val options = configOptions(ConfigOption.Source(currentSource))
|
||||||
val result = invoke(options)
|
val result = invoke(options)
|
||||||
assertResult(expectedSources)(result.map(_.sources))
|
~*(assertResult(expectedSources)(result.map(_.sources)))
|
||||||
assertResult(expectedBuckets)(result.map(_.bucket))
|
~*(assertResult(expectedBuckets)(result.map(_.bucket)))
|
||||||
assertResult(expectedPrefixes)(result.map(_.prefix))
|
~*(assertResult(expectedPrefixes)(result.map(_.prefix)))
|
||||||
assertResult(expectedFilters)(result.map(_.filters))
|
~*(assertResult(expectedFilters)(result.map(_.filters)))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import zio.DefaultRuntime
|
||||||
|
|
||||||
class ParseConfigFileTest extends FunSpec with TemporaryFolder {
|
class ParseConfigFileTest extends FunSpec with TemporaryFolder {
|
||||||
|
|
||||||
private val empty = Right(ConfigOptions())
|
private val empty = Right(ConfigOptions.empty)
|
||||||
|
|
||||||
describe("parse a missing file") {
|
describe("parse a missing file") {
|
||||||
val file = new File("/path/to/missing/file")
|
val file = new File("/path/to/missing/file")
|
||||||
|
@ -21,7 +21,7 @@ class ParseConfigFileTest extends FunSpec with TemporaryFolder {
|
||||||
describe("parse an empty file") {
|
describe("parse an empty file") {
|
||||||
it("should return no options") {
|
it("should return no options") {
|
||||||
withDirectory(dir => {
|
withDirectory(dir => {
|
||||||
val file = writeFile(dir, "empty-file")
|
val file = createFile(dir, "empty-file")
|
||||||
assertResult(empty)(invoke(file))
|
assertResult(empty)(invoke(file))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ class ParseConfigFileTest extends FunSpec with TemporaryFolder {
|
||||||
describe("parse a file with no valid entries") {
|
describe("parse a file with no valid entries") {
|
||||||
it("should return no options") {
|
it("should return no options") {
|
||||||
withDirectory(dir => {
|
withDirectory(dir => {
|
||||||
val file = writeFile(dir, "invalid-config", "no valid = config items")
|
val file = createFile(dir, "invalid-config", "no valid = config items")
|
||||||
assertResult(empty)(invoke(file))
|
assertResult(empty)(invoke(file))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -37,10 +37,11 @@ class ParseConfigFileTest extends FunSpec with TemporaryFolder {
|
||||||
describe("parse a file with properties") {
|
describe("parse a file with properties") {
|
||||||
it("should return some options") {
|
it("should return some options") {
|
||||||
val expected = Right(
|
val expected = Right(
|
||||||
ConfigOptions(List(ConfigOption.Source(Paths.get("/path/to/source")),
|
ConfigOptions(
|
||||||
|
List[ConfigOption](ConfigOption.Source(Paths.get("/path/to/source")),
|
||||||
ConfigOption.Bucket("bucket-name"))))
|
ConfigOption.Bucket("bucket-name"))))
|
||||||
withDirectory(dir => {
|
withDirectory(dir => {
|
||||||
val file = writeFile(dir,
|
val file = createFile(dir,
|
||||||
"simple-config",
|
"simple-config",
|
||||||
"source = /path/to/source",
|
"source = /path/to/source",
|
||||||
"bucket = bucket-name")
|
"bucket = bucket-name")
|
||||||
|
|
|
@ -59,21 +59,21 @@ class ParseConfigLinesTest extends FunSpec {
|
||||||
}
|
}
|
||||||
describe("debug - false") {
|
describe("debug - false") {
|
||||||
it("should parse") {
|
it("should parse") {
|
||||||
val expected = Right(ConfigOptions())
|
val expected = Right(ConfigOptions.empty)
|
||||||
val result = invoke(List("debug = false"))
|
val result = invoke(List("debug = false"))
|
||||||
assertResult(expected)(result)
|
assertResult(expected)(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
describe("comment line") {
|
describe("comment line") {
|
||||||
it("should be ignored") {
|
it("should be ignored") {
|
||||||
val expected = Right(ConfigOptions())
|
val expected = Right(ConfigOptions.empty)
|
||||||
val result = invoke(List("# ignore me"))
|
val result = invoke(List("# ignore me"))
|
||||||
assertResult(expected)(result)
|
assertResult(expected)(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
describe("unrecognised option") {
|
describe("unrecognised option") {
|
||||||
it("should be ignored") {
|
it("should be ignored") {
|
||||||
val expected = Right(ConfigOptions())
|
val expected = Right(ConfigOptions.empty)
|
||||||
val result = invoke(List("unsupported = option"))
|
val result = invoke(List("unsupported = option"))
|
||||||
assertResult(expected)(result)
|
assertResult(expected)(result)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,17 +15,17 @@ trait Console {
|
||||||
object Console {
|
object Console {
|
||||||
|
|
||||||
trait Service {
|
trait Service {
|
||||||
def putStrLn(line: ConsoleOut): ZIO[Console, Nothing, Unit]
|
def putMessageLn(line: ConsoleOut): ZIO[Console, Nothing, Unit]
|
||||||
def putStrLn(line: String): ZIO[Console, Nothing, Unit]
|
def putStrLn(line: String): ZIO[Console, Nothing, Unit]
|
||||||
}
|
}
|
||||||
|
|
||||||
trait Live extends Console {
|
trait Live extends Console {
|
||||||
val console: Service = new Service {
|
val console: Service = new Service {
|
||||||
override def putStrLn(line: ConsoleOut): ZIO[Console, Nothing, Unit] =
|
override def putMessageLn(line: ConsoleOut): ZIO[Console, Nothing, Unit] =
|
||||||
putStrLn(line.en)
|
putStrLn(line.en)
|
||||||
override def putStrLn(line: String): ZIO[Console, Nothing, Unit] =
|
override def putStrLn(line: String): ZIO[Console, Nothing, Unit] =
|
||||||
putStrLn(SConsole.out)(line)
|
putStrLnPrintStream(SConsole.out)(line)
|
||||||
final def putStrLn(stream: PrintStream)(
|
final def putStrLnPrintStream(stream: PrintStream)(
|
||||||
line: String): ZIO[Console, Nothing, Unit] =
|
line: String): ZIO[Console, Nothing, Unit] =
|
||||||
UIO(SConsole.withOut(stream)(SConsole.println(line)))
|
UIO(SConsole.withOut(stream)(SConsole.println(line)))
|
||||||
}
|
}
|
||||||
|
@ -39,11 +39,11 @@ object Console {
|
||||||
def getOutput: List[String] = output.get
|
def getOutput: List[String] = output.get
|
||||||
|
|
||||||
val console: Service = new Service {
|
val console: Service = new Service {
|
||||||
override def putStrLn(line: ConsoleOut): ZIO[Console, Nothing, Unit] =
|
override def putMessageLn(line: ConsoleOut): ZIO[Console, Nothing, Unit] =
|
||||||
putStrLn(line.en)
|
putStrLn(line.en)
|
||||||
|
|
||||||
override def putStrLn(line: String): ZIO[Console, Nothing, Unit] = {
|
override def putStrLn(line: String): ZIO[Console, Nothing, Unit] = {
|
||||||
output.accumulateAndGet(List(line), (a, b) => a ++ b)
|
val _ = output.accumulateAndGet(List(line), (a, b) => a ++ b)
|
||||||
ZIO.succeed(())
|
ZIO.succeed(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,9 +59,9 @@ object Console {
|
||||||
ZIO.accessM(_.console putStrLn line)
|
ZIO.accessM(_.console putStrLn line)
|
||||||
|
|
||||||
final def putMessageLn(line: ConsoleOut): ZIO[Console, Nothing, Unit] =
|
final def putMessageLn(line: ConsoleOut): ZIO[Console, Nothing, Unit] =
|
||||||
ZIO.accessM(_.console putStrLn line)
|
ZIO.accessM(_.console putMessageLn line)
|
||||||
|
|
||||||
final def putMessageLn(
|
final def putMessageLnB(
|
||||||
line: ConsoleOut.WithBatchMode): ZIO[Console with Config, Nothing, Unit] =
|
line: ConsoleOut.WithBatchMode): ZIO[Console with Config, Nothing, Unit] =
|
||||||
ZIO.accessM(line() >>= _.console.putStrLn)
|
ZIO.accessM(line() >>= _.console.putStrLn)
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ object ConsoleOut {
|
||||||
if (batchMode) UIO(enBatch) else UIO(en)
|
if (batchMode) UIO(enBatch) else UIO(en)
|
||||||
}
|
}
|
||||||
|
|
||||||
case class ValidConfig(
|
final case class ValidConfig(
|
||||||
bucket: Bucket,
|
bucket: Bucket,
|
||||||
prefix: RemoteKey,
|
prefix: RemoteKey,
|
||||||
sources: Sources
|
sources: Sources
|
||||||
|
@ -36,7 +36,7 @@ object ConsoleOut {
|
||||||
.mkString(", ")
|
.mkString(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
case class UploadComplete(remoteKey: RemoteKey)
|
final case class UploadComplete(remoteKey: RemoteKey)
|
||||||
extends ConsoleOut.WithBatchMode {
|
extends ConsoleOut.WithBatchMode {
|
||||||
override def en: String =
|
override def en: String =
|
||||||
s"${GREEN}Uploaded:$RESET ${remoteKey.key}$eraseToEndOfScreen"
|
s"${GREEN}Uploaded:$RESET ${remoteKey.key}$eraseToEndOfScreen"
|
||||||
|
@ -44,7 +44,7 @@ object ConsoleOut {
|
||||||
s"Uploaded: ${remoteKey.key}"
|
s"Uploaded: ${remoteKey.key}"
|
||||||
}
|
}
|
||||||
|
|
||||||
case class CopyComplete(sourceKey: RemoteKey, targetKey: RemoteKey)
|
final case class CopyComplete(sourceKey: RemoteKey, targetKey: RemoteKey)
|
||||||
extends ConsoleOut.WithBatchMode {
|
extends ConsoleOut.WithBatchMode {
|
||||||
override def en: String =
|
override def en: String =
|
||||||
s"${GREEN}Copied:$RESET ${sourceKey.key} => ${targetKey.key}$eraseToEndOfScreen"
|
s"${GREEN}Copied:$RESET ${sourceKey.key} => ${targetKey.key}$eraseToEndOfScreen"
|
||||||
|
@ -52,7 +52,7 @@ object ConsoleOut {
|
||||||
s"Copied: ${sourceKey.key} => ${targetKey.key}"
|
s"Copied: ${sourceKey.key} => ${targetKey.key}"
|
||||||
}
|
}
|
||||||
|
|
||||||
case class DeleteComplete(remoteKey: RemoteKey)
|
final case class DeleteComplete(remoteKey: RemoteKey)
|
||||||
extends ConsoleOut.WithBatchMode {
|
extends ConsoleOut.WithBatchMode {
|
||||||
override def en: String =
|
override def en: String =
|
||||||
s"${GREEN}Deleted:$RESET ${remoteKey.key}$eraseToEndOfScreen"
|
s"${GREEN}Deleted:$RESET ${remoteKey.key}$eraseToEndOfScreen"
|
||||||
|
@ -60,7 +60,7 @@ object ConsoleOut {
|
||||||
s"Deleted: $remoteKey"
|
s"Deleted: $remoteKey"
|
||||||
}
|
}
|
||||||
|
|
||||||
case class ErrorQueueEventOccurred(action: Action, e: Throwable)
|
final case class ErrorQueueEventOccurred(action: Action, e: Throwable)
|
||||||
extends ConsoleOut.WithBatchMode {
|
extends ConsoleOut.WithBatchMode {
|
||||||
override def en: String =
|
override def en: String =
|
||||||
s"${action.name} failed: ${action.keys}: ${e.getMessage}"
|
s"${action.name} failed: ${action.keys}: ${e.getMessage}"
|
||||||
|
|
|
@ -2,15 +2,16 @@ package net.kemitix.thorp.core
|
||||||
|
|
||||||
import net.kemitix.thorp.config.Config
|
import net.kemitix.thorp.config.Config
|
||||||
import net.kemitix.thorp.core.Action.{DoNothing, ToCopy, ToUpload}
|
import net.kemitix.thorp.core.Action.{DoNothing, ToCopy, ToUpload}
|
||||||
|
import net.kemitix.thorp.domain.Implicits._
|
||||||
import net.kemitix.thorp.domain._
|
import net.kemitix.thorp.domain._
|
||||||
import zio.RIO
|
import zio.RIO
|
||||||
|
|
||||||
object ActionGenerator {
|
object ActionGenerator {
|
||||||
|
|
||||||
def createAction(
|
def createActions(
|
||||||
matchedMetadata: MatchedMetadata,
|
matchedMetadata: MatchedMetadata,
|
||||||
previousActions: Stream[Action]
|
previousActions: Stream[Action]
|
||||||
): RIO[Config, Action] =
|
): RIO[Config, Stream[Action]] =
|
||||||
for {
|
for {
|
||||||
bucket <- Config.bucket
|
bucket <- Config.bucket
|
||||||
} yield
|
} yield
|
||||||
|
@ -30,7 +31,7 @@ object ActionGenerator {
|
||||||
anyMatches)
|
anyMatches)
|
||||||
}
|
}
|
||||||
|
|
||||||
case class TaggedMetadata(
|
final case class TaggedMetadata(
|
||||||
matchedMetadata: MatchedMetadata,
|
matchedMetadata: MatchedMetadata,
|
||||||
previousActions: Stream[Action],
|
previousActions: Stream[Action],
|
||||||
remoteExists: Boolean,
|
remoteExists: Boolean,
|
||||||
|
@ -39,14 +40,15 @@ object ActionGenerator {
|
||||||
)
|
)
|
||||||
|
|
||||||
private def genAction(taggedMetadata: TaggedMetadata,
|
private def genAction(taggedMetadata: TaggedMetadata,
|
||||||
bucket: Bucket): Action = {
|
bucket: Bucket): Stream[Action] = {
|
||||||
taggedMetadata match {
|
taggedMetadata match {
|
||||||
case TaggedMetadata(md, _, exists, matches, _) if exists && matches =>
|
case TaggedMetadata(md, _, remoteExists, remoteMatches, _)
|
||||||
|
if remoteExists && remoteMatches =>
|
||||||
doNothing(bucket, md.localFile.remoteKey)
|
doNothing(bucket, md.localFile.remoteKey)
|
||||||
case TaggedMetadata(md, _, _, _, any) if any =>
|
case TaggedMetadata(md, _, _, _, anyMatches) if anyMatches =>
|
||||||
copyFile(bucket, md.localFile, md.matchByHash.head)
|
copyFile(bucket, md.localFile, md.matchByHash)
|
||||||
case TaggedMetadata(md, previous, _, _, _)
|
case TaggedMetadata(md, previous, _, _, _)
|
||||||
if isUploadAlreadyQueued(previous)(md.localFile) =>
|
if isNotUploadAlreadyQueued(previous)(md.localFile) =>
|
||||||
uploadFile(bucket, md.localFile)
|
uploadFile(bucket, md.localFile)
|
||||||
case TaggedMetadata(md, _, _, _, _) =>
|
case TaggedMetadata(md, _, _, _, _) =>
|
||||||
doNothing(bucket, md.localFile.remoteKey)
|
doNothing(bucket, md.localFile.remoteKey)
|
||||||
|
@ -55,34 +57,39 @@ object ActionGenerator {
|
||||||
|
|
||||||
private def key = LocalFile.remoteKey ^|-> RemoteKey.key
|
private def key = LocalFile.remoteKey ^|-> RemoteKey.key
|
||||||
|
|
||||||
def isUploadAlreadyQueued(
|
def isNotUploadAlreadyQueued(
|
||||||
previousActions: Stream[Action]
|
previousActions: Stream[Action]
|
||||||
)(
|
)(
|
||||||
localFile: LocalFile
|
localFile: LocalFile
|
||||||
): Boolean = !previousActions.exists {
|
): Boolean = !previousActions.exists {
|
||||||
case ToUpload(_, lf, _) => key.get(lf) equals key.get(localFile)
|
case ToUpload(_, lf, _) => key.get(lf) === key.get(localFile)
|
||||||
case _ => false
|
case _ => false
|
||||||
}
|
}
|
||||||
|
|
||||||
private def doNothing(
|
private def doNothing(
|
||||||
bucket: Bucket,
|
bucket: Bucket,
|
||||||
remoteKey: RemoteKey
|
remoteKey: RemoteKey
|
||||||
) = DoNothing(bucket, remoteKey, 0L)
|
) = Stream(DoNothing(bucket, remoteKey, 0L))
|
||||||
|
|
||||||
private def uploadFile(
|
private def uploadFile(
|
||||||
bucket: Bucket,
|
bucket: Bucket,
|
||||||
localFile: LocalFile
|
localFile: LocalFile
|
||||||
) = ToUpload(bucket, localFile, localFile.file.length)
|
) = Stream(ToUpload(bucket, localFile, localFile.file.length))
|
||||||
|
|
||||||
private def copyFile(
|
private def copyFile(
|
||||||
bucket: Bucket,
|
bucket: Bucket,
|
||||||
localFile: LocalFile,
|
localFile: LocalFile,
|
||||||
remoteMetaData: RemoteMetaData
|
remoteMetaData: Set[RemoteMetaData]
|
||||||
): Action =
|
) =
|
||||||
|
remoteMetaData
|
||||||
|
.take(1)
|
||||||
|
.toStream
|
||||||
|
.map(
|
||||||
|
other =>
|
||||||
ToCopy(bucket,
|
ToCopy(bucket,
|
||||||
remoteMetaData.remoteKey,
|
other.remoteKey,
|
||||||
remoteMetaData.hash,
|
other.hash,
|
||||||
localFile.remoteKey,
|
localFile.remoteKey,
|
||||||
localFile.file.length)
|
localFile.file.length))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,14 @@ package net.kemitix.thorp.core
|
||||||
import net.kemitix.thorp.domain.SimpleLens
|
import net.kemitix.thorp.domain.SimpleLens
|
||||||
|
|
||||||
final case class Counters(
|
final case class Counters(
|
||||||
uploaded: Int = 0,
|
uploaded: Int,
|
||||||
deleted: Int = 0,
|
deleted: Int,
|
||||||
copied: Int = 0,
|
copied: Int,
|
||||||
errors: Int = 0
|
errors: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
object Counters {
|
object Counters {
|
||||||
|
val empty: Counters = Counters(0, 0, 0, 0)
|
||||||
val uploaded: SimpleLens[Counters, Int] =
|
val uploaded: SimpleLens[Counters, Int] =
|
||||||
SimpleLens[Counters, Int](_.uploaded, b => a => b.copy(uploaded = a))
|
SimpleLens[Counters, Int](_.uploaded, b => a => b.copy(uploaded = a))
|
||||||
val deleted: SimpleLens[Counters, Int] =
|
val deleted: SimpleLens[Counters, Int] =
|
||||||
|
|
|
@ -2,7 +2,7 @@ package net.kemitix.thorp.core
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.StorageQueueEvent
|
import net.kemitix.thorp.domain.StorageQueueEvent
|
||||||
|
|
||||||
case class EventQueue(
|
final case class EventQueue(
|
||||||
events: Stream[StorageQueueEvent],
|
events: Stream[StorageQueueEvent],
|
||||||
bytesInQueue: Long
|
bytesInQueue: Long
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,9 +9,9 @@ object Filters {
|
||||||
|
|
||||||
def isIncluded(p: Path)(filters: List[Filter]): Boolean = {
|
def isIncluded(p: Path)(filters: List[Filter]): Boolean = {
|
||||||
sealed trait State
|
sealed trait State
|
||||||
case class Unknown() extends State
|
final case class Unknown() extends State
|
||||||
case class Accepted() extends State
|
final case class Accepted() extends State
|
||||||
case class Discarded() extends State
|
final case class Discarded() extends State
|
||||||
val excluded = isExcludedByFilter(p)(_)
|
val excluded = isExcludedByFilter(p)(_)
|
||||||
val included = isIncludedByFilter(p)(_)
|
val included = isIncludedByFilter(p)(_)
|
||||||
filters.foldRight(Unknown(): State)((filter, state) =>
|
filters.foldRight(Unknown(): State)((filter, state) =>
|
||||||
|
@ -33,9 +33,9 @@ object Filters {
|
||||||
}
|
}
|
||||||
|
|
||||||
def isIncludedByFilter(path: Path)(filter: Filter): Boolean =
|
def isIncludedByFilter(path: Path)(filter: Filter): Boolean =
|
||||||
filter.predicate.test(path.toString)
|
filter.predicate.test(path.toFile.getPath)
|
||||||
|
|
||||||
def isExcludedByFilter(path: Path)(filter: Filter): Boolean =
|
def isExcludedByFilter(path: Path)(filter: Filter): Boolean =
|
||||||
filter.predicate.test(path.toString)
|
filter.predicate.test(path.toFile.getPath)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,8 @@ object KeyGenerator {
|
||||||
)(path: Path): Task[RemoteKey] =
|
)(path: Path): Task[RemoteKey] =
|
||||||
Sources
|
Sources
|
||||||
.forPath(path)(sources)
|
.forPath(path)(sources)
|
||||||
.map(p => p.relativize(path.toAbsolutePath).toString)
|
.map(_.relativize(path.toAbsolutePath))
|
||||||
|
.map(_.toFile.getPath)
|
||||||
.map(RemoteKey.resolve(_)(prefix))
|
.map(RemoteKey.resolve(_)(prefix))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,9 +65,11 @@ object LocalFileStream {
|
||||||
_ <- filesMustExist(path, files)
|
_ <- filesMustExist(path, files)
|
||||||
} yield Stream(files: _*).map(_.toPath)
|
} yield Stream(files: _*).map(_.toPath)
|
||||||
|
|
||||||
private def filesMustExist(path: Path, files: Array[File]) = {
|
private def filesMustExist(path: Path, files: Array[File]) =
|
||||||
Task.when(files == null)(
|
Task {
|
||||||
Task.fail(new IllegalArgumentException(s"Directory not found $path")))
|
Option(files)
|
||||||
|
.map(_ => ())
|
||||||
|
.getOrElse(new IllegalArgumentException(s"Directory not found $path"))
|
||||||
}
|
}
|
||||||
|
|
||||||
private def isIncluded(path: Path) =
|
private def isIncluded(path: Path) =
|
||||||
|
|
|
@ -22,11 +22,11 @@ object LocalFileValidator {
|
||||||
prefix: RemoteKey
|
prefix: RemoteKey
|
||||||
): IO[Violation, LocalFile] =
|
): IO[Violation, LocalFile] =
|
||||||
for {
|
for {
|
||||||
vFile <- validateFile(path.toFile)
|
file <- validateFile(path.toFile)
|
||||||
remoteKey <- validateRemoteKey(sources, prefix, path)
|
remoteKey <- validateRemoteKey(sources, prefix, path)
|
||||||
} yield LocalFile(vFile, source, hash, remoteKey)
|
} yield LocalFile(file, source, hash, remoteKey)
|
||||||
|
|
||||||
private def validateFile(file: File) =
|
private def validateFile(file: File): IO[Violation, File] =
|
||||||
if (file.isDirectory)
|
if (file.isDirectory)
|
||||||
ZIO.fail(Violation.IsNotAFile(file))
|
ZIO.fail(Violation.IsNotAFile(file))
|
||||||
else
|
else
|
||||||
|
@ -34,7 +34,7 @@ object LocalFileValidator {
|
||||||
|
|
||||||
private def validateRemoteKey(sources: Sources,
|
private def validateRemoteKey(sources: Sources,
|
||||||
prefix: RemoteKey,
|
prefix: RemoteKey,
|
||||||
path: Path) =
|
path: Path): IO[Violation, RemoteKey] =
|
||||||
KeyGenerator
|
KeyGenerator
|
||||||
.generateKey(sources, prefix)(path)
|
.generateKey(sources, prefix)(path)
|
||||||
.mapError(e => Violation.InvalidRemoteKey(path, e))
|
.mapError(e => Violation.InvalidRemoteKey(path, e))
|
||||||
|
@ -43,10 +43,11 @@ object LocalFileValidator {
|
||||||
def getMessage: String
|
def getMessage: String
|
||||||
}
|
}
|
||||||
object Violation {
|
object Violation {
|
||||||
case class IsNotAFile(file: File) extends Violation {
|
final case class IsNotAFile(file: File) extends Violation {
|
||||||
override def getMessage: String = s"Local File must be a file: ${file}"
|
override def getMessage: String = s"Local File must be a file: ${file}"
|
||||||
}
|
}
|
||||||
case class InvalidRemoteKey(path: Path, e: Throwable) extends Violation {
|
final case class InvalidRemoteKey(path: Path, e: Throwable)
|
||||||
|
extends Violation {
|
||||||
override def getMessage: String =
|
override def getMessage: String =
|
||||||
s"Remote Key for '${path}' is invalid: ${e.getMessage}"
|
s"Remote Key for '${path}' is invalid: ${e.getMessage}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,10 @@ package net.kemitix.thorp.core
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.LocalFile
|
import net.kemitix.thorp.domain.LocalFile
|
||||||
|
|
||||||
case class LocalFiles(
|
final case class LocalFiles(
|
||||||
localFiles: Stream[LocalFile] = Stream(),
|
localFiles: Stream[LocalFile],
|
||||||
count: Long = 0,
|
count: Long,
|
||||||
totalSizeBytes: Long = 0
|
totalSizeBytes: Long
|
||||||
) {
|
) {
|
||||||
def ++(append: LocalFiles): LocalFiles =
|
def ++(append: LocalFiles): LocalFiles =
|
||||||
copy(
|
copy(
|
||||||
|
@ -16,8 +16,9 @@ case class LocalFiles(
|
||||||
}
|
}
|
||||||
|
|
||||||
object LocalFiles {
|
object LocalFiles {
|
||||||
|
val empty: LocalFiles = LocalFiles(Stream.empty, 0L, 0L)
|
||||||
def reduce: Stream[LocalFiles] => LocalFiles =
|
def reduce: Stream[LocalFiles] => LocalFiles =
|
||||||
list => list.foldLeft(LocalFiles())((acc, lf) => acc ++ lf)
|
list => list.foldLeft(LocalFiles.empty)((acc, lf) => acc ++ lf)
|
||||||
def one(localFile: LocalFile): LocalFiles =
|
def one(localFile: LocalFile): LocalFiles =
|
||||||
LocalFiles(Stream(localFile), 1, localFile.file.length)
|
LocalFiles(Stream(localFile), 1, localFile.file.length)
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,10 @@ object PlanBuilder {
|
||||||
createActions(remoteObjects, localData.localFiles)
|
createActions(remoteObjects, localData.localFiles)
|
||||||
.map(_.filter(doesSomething).sortBy(SequencePlan.order))
|
.map(_.filter(doesSomething).sortBy(SequencePlan.order))
|
||||||
.map(
|
.map(
|
||||||
SyncPlan(_, SyncTotals(localData.count, localData.totalSizeBytes)))
|
SyncPlan
|
||||||
|
.create(_,
|
||||||
|
SyncTotals
|
||||||
|
.create(localData.count, localData.totalSizeBytes, 0L)))
|
||||||
}
|
}
|
||||||
|
|
||||||
private def createActions(
|
private def createActions(
|
||||||
|
@ -57,14 +60,14 @@ object PlanBuilder {
|
||||||
localFiles: Stream[LocalFile]
|
localFiles: Stream[LocalFile]
|
||||||
) =
|
) =
|
||||||
ZIO.foldLeft(localFiles)(Stream.empty[Action])((acc, localFile) =>
|
ZIO.foldLeft(localFiles)(Stream.empty[Action])((acc, localFile) =>
|
||||||
createActionFromLocalFile(remoteObjects, acc, localFile).map(_ #:: acc))
|
createActionsFromLocalFile(remoteObjects, acc, localFile).map(_ #::: acc))
|
||||||
|
|
||||||
private def createActionFromLocalFile(
|
private def createActionsFromLocalFile(
|
||||||
remoteObjects: RemoteObjects,
|
remoteObjects: RemoteObjects,
|
||||||
previousActions: Stream[Action],
|
previousActions: Stream[Action],
|
||||||
localFile: LocalFile
|
localFile: LocalFile
|
||||||
) =
|
) =
|
||||||
ActionGenerator.createAction(
|
ActionGenerator.createActions(
|
||||||
S3MetaDataEnricher.getMetadata(localFile, remoteObjects),
|
S3MetaDataEnricher.getMetadata(localFile, remoteObjects),
|
||||||
previousActions)
|
previousActions)
|
||||||
|
|
||||||
|
@ -72,12 +75,13 @@ object PlanBuilder {
|
||||||
ZIO.foldLeft(remoteKeys)(Stream.empty[Action])((acc, remoteKey) =>
|
ZIO.foldLeft(remoteKeys)(Stream.empty[Action])((acc, remoteKey) =>
|
||||||
createActionFromRemoteKey(remoteKey).map(_ #:: acc))
|
createActionFromRemoteKey(remoteKey).map(_ #:: acc))
|
||||||
|
|
||||||
private def createActionFromRemoteKey(remoteKey: RemoteKey) =
|
private def createActionFromRemoteKey(
|
||||||
|
remoteKey: RemoteKey): ZIO[FileSystem with Config, Throwable, Action] =
|
||||||
for {
|
for {
|
||||||
bucket <- Config.bucket
|
bucket <- Config.bucket
|
||||||
prefix <- Config.prefix
|
prefix <- Config.prefix
|
||||||
sources <- Config.sources
|
sources <- Config.sources
|
||||||
needsDeleted <- Remote.isMissingLocally(sources, prefix)(remoteKey)
|
needsDeleted <- Remote.isMissingLocally(sources, prefix, remoteKey)
|
||||||
} yield
|
} yield
|
||||||
if (needsDeleted) ToDelete(bucket, remoteKey, 0L)
|
if (needsDeleted) ToDelete(bucket, remoteKey, 0L)
|
||||||
else DoNothing(bucket, remoteKey, 0L)
|
else DoNothing(bucket, remoteKey, 0L)
|
||||||
|
|
|
@ -8,9 +8,9 @@ import zio.{RIO, ZIO}
|
||||||
|
|
||||||
object Remote {
|
object Remote {
|
||||||
|
|
||||||
def isMissingLocally(sources: Sources, prefix: RemoteKey)(
|
def isMissingLocally(sources: Sources,
|
||||||
remoteKey: RemoteKey
|
prefix: RemoteKey,
|
||||||
): RIO[FileSystem, Boolean] =
|
remoteKey: RemoteKey): RIO[FileSystem, Boolean] =
|
||||||
existsLocally(sources, prefix)(remoteKey)
|
existsLocally(sources, prefix)(remoteKey)
|
||||||
.map(exists => !exists)
|
.map(exists => !exists)
|
||||||
|
|
||||||
|
|
|
@ -11,11 +11,11 @@ object S3MetaDataEnricher {
|
||||||
val (keyMatches, hashMatches) = getS3Status(localFile, remoteObjects)
|
val (keyMatches, hashMatches) = getS3Status(localFile, remoteObjects)
|
||||||
MatchedMetadata(
|
MatchedMetadata(
|
||||||
localFile,
|
localFile,
|
||||||
matchByKey = keyMatches.map { hm =>
|
matchByKey = keyMatches.map { hash =>
|
||||||
RemoteMetaData(localFile.remoteKey, hm.hash, hm.modified)
|
RemoteMetaData(localFile.remoteKey, hash)
|
||||||
},
|
},
|
||||||
matchByHash = hashMatches.map {
|
matchByHash = hashMatches.map {
|
||||||
case (hash, km) => RemoteMetaData(km.key, hash, km.modified)
|
case (key, hash) => RemoteMetaData(key, hash)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -23,14 +23,14 @@ object S3MetaDataEnricher {
|
||||||
def getS3Status(
|
def getS3Status(
|
||||||
localFile: LocalFile,
|
localFile: LocalFile,
|
||||||
remoteObjects: RemoteObjects
|
remoteObjects: RemoteObjects
|
||||||
): (Option[HashModified], Set[(MD5Hash, KeyModified)]) = {
|
): (Option[MD5Hash], Set[(RemoteKey, MD5Hash)]) = {
|
||||||
val matchingByKey = remoteObjects.byKey.get(localFile.remoteKey)
|
val matchingByKey = remoteObjects.byKey.get(localFile.remoteKey)
|
||||||
val matchingByHash = localFile.hashes
|
val matchingByHash = localFile.hashes
|
||||||
.map {
|
.map {
|
||||||
case (_, md5Hash) =>
|
case (_, md5Hash) =>
|
||||||
remoteObjects.byHash
|
remoteObjects.byHash
|
||||||
.getOrElse(md5Hash, Set())
|
.getOrElse(md5Hash, Set())
|
||||||
.map(km => (md5Hash, km))
|
.map(key => (key, md5Hash))
|
||||||
}
|
}
|
||||||
.flatten
|
.flatten
|
||||||
.toSet
|
.toSet
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package net.kemitix.thorp.core
|
package net.kemitix.thorp.core
|
||||||
|
|
||||||
case class SequencedAction(
|
final case class SequencedAction(
|
||||||
action: Action,
|
action: Action,
|
||||||
index: Int
|
index: Int
|
||||||
)
|
)
|
||||||
|
|
|
@ -32,7 +32,7 @@ trait SyncLogging {
|
||||||
def logRunFinished(
|
def logRunFinished(
|
||||||
actions: Stream[StorageQueueEvent]
|
actions: Stream[StorageQueueEvent]
|
||||||
): ZIO[Console, Nothing, Unit] = {
|
): ZIO[Console, Nothing, Unit] = {
|
||||||
val counters = actions.foldLeft(Counters())(countActivities)
|
val counters = actions.foldLeft(Counters.empty)(countActivities)
|
||||||
Console.putStrLn(eraseToEndOfScreen) *>
|
Console.putStrLn(eraseToEndOfScreen) *>
|
||||||
Console.putStrLn(s"Uploaded ${counters.uploaded} files") *>
|
Console.putStrLn(s"Uploaded ${counters.uploaded} files") *>
|
||||||
Console.putStrLn(s"Copied ${counters.copied} files") *>
|
Console.putStrLn(s"Copied ${counters.copied} files") *>
|
||||||
|
|
|
@ -2,7 +2,13 @@ package net.kemitix.thorp.core
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.SyncTotals
|
import net.kemitix.thorp.domain.SyncTotals
|
||||||
|
|
||||||
case class SyncPlan(
|
final case class SyncPlan private (
|
||||||
actions: Stream[Action] = Stream(),
|
actions: Stream[Action],
|
||||||
syncTotals: SyncTotals = SyncTotals()
|
syncTotals: SyncTotals
|
||||||
)
|
)
|
||||||
|
|
||||||
|
object SyncPlan {
|
||||||
|
val empty: SyncPlan = SyncPlan(Stream.empty, SyncTotals.empty)
|
||||||
|
def create(actions: Stream[Action], syncTotals: SyncTotals): SyncPlan =
|
||||||
|
SyncPlan(actions, syncTotals)
|
||||||
|
}
|
||||||
|
|
|
@ -24,13 +24,13 @@ trait ThorpArchive {
|
||||||
event: StorageQueueEvent): RIO[Console with Config, StorageQueueEvent] =
|
event: StorageQueueEvent): RIO[Console with Config, StorageQueueEvent] =
|
||||||
event match {
|
event match {
|
||||||
case UploadQueueEvent(remoteKey, _) =>
|
case UploadQueueEvent(remoteKey, _) =>
|
||||||
ZIO(event) <* Console.putMessageLn(UploadComplete(remoteKey))
|
ZIO(event) <* Console.putMessageLnB(UploadComplete(remoteKey))
|
||||||
case CopyQueueEvent(sourceKey, targetKey) =>
|
case CopyQueueEvent(sourceKey, targetKey) =>
|
||||||
ZIO(event) <* Console.putMessageLn(CopyComplete(sourceKey, targetKey))
|
ZIO(event) <* Console.putMessageLnB(CopyComplete(sourceKey, targetKey))
|
||||||
case DeleteQueueEvent(remoteKey) =>
|
case DeleteQueueEvent(remoteKey) =>
|
||||||
ZIO(event) <* Console.putMessageLn(DeleteComplete(remoteKey))
|
ZIO(event) <* Console.putMessageLnB(DeleteComplete(remoteKey))
|
||||||
case ErrorQueueEvent(action, _, e) =>
|
case ErrorQueueEvent(action, _, e) =>
|
||||||
ZIO(event) <* Console.putMessageLn(ErrorQueueEventOccurred(action, e))
|
ZIO(event) <* Console.putMessageLnB(ErrorQueueEventOccurred(action, e))
|
||||||
case DoNothingQueueEvent(_) => ZIO(event)
|
case DoNothingQueueEvent(_) => ZIO(event)
|
||||||
case ShutdownQueueEvent() => ZIO(event)
|
case ShutdownQueueEvent() => ZIO(event)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import net.kemitix.thorp.domain._
|
||||||
import net.kemitix.thorp.storage.api.Storage
|
import net.kemitix.thorp.storage.api.Storage
|
||||||
import zio.{Task, RIO}
|
import zio.{Task, RIO}
|
||||||
|
|
||||||
case class UnversionedMirrorArchive(syncTotals: SyncTotals)
|
final case class UnversionedMirrorArchive(syncTotals: SyncTotals)
|
||||||
extends ThorpArchive {
|
extends ThorpArchive {
|
||||||
|
|
||||||
override def update(
|
override def update(
|
||||||
|
|
|
@ -48,7 +48,7 @@ private object MD5HashGenerator {
|
||||||
offset: Long,
|
offset: Long,
|
||||||
endOffset: Long
|
endOffset: Long
|
||||||
) =
|
) =
|
||||||
FileSystem.open(file, offset) >>= { managedFileInputStream =>
|
FileSystem.openAtOffset(file, offset) >>= { managedFileInputStream =>
|
||||||
managedFileInputStream.use { fileInputStream =>
|
managedFileInputStream.use { fileInputStream =>
|
||||||
digestFile(fileInputStream, offset, endOffset)
|
digestFile(fileInputStream, offset, endOffset)
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ private object MD5HashGenerator {
|
||||||
if (nextBufferSize(currentOffset, endOffset) < maxBufferSize)
|
if (nextBufferSize(currentOffset, endOffset) < maxBufferSize)
|
||||||
new Array[Byte](nextBufferSize(currentOffset, endOffset))
|
new Array[Byte](nextBufferSize(currentOffset, endOffset))
|
||||||
else defaultBuffer
|
else defaultBuffer
|
||||||
fis read buffer
|
val _ = fis read buffer
|
||||||
buffer
|
buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package net.kemitix.thorp.core
|
package net.kemitix.thorp.core
|
||||||
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
import net.kemitix.thorp.config._
|
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
|
||||||
|
@ -11,14 +9,13 @@ import org.scalatest.FunSpec
|
||||||
import zio.DefaultRuntime
|
import zio.DefaultRuntime
|
||||||
|
|
||||||
class ActionGeneratorSuite extends FunSpec {
|
class ActionGeneratorSuite extends FunSpec {
|
||||||
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 sources = Sources(List(sourcePath))
|
||||||
private val prefix = RemoteKey("prefix")
|
private val prefix = RemoteKey("prefix")
|
||||||
private val bucket = Bucket("bucket")
|
private val bucket = Bucket("bucket")
|
||||||
private val configOptions = ConfigOptions(
|
private val configOptions = ConfigOptions(
|
||||||
List(
|
List[ConfigOption](
|
||||||
ConfigOption.Bucket("bucket"),
|
ConfigOption.Bucket("bucket"),
|
||||||
ConfigOption.Prefix("prefix"),
|
ConfigOption.Prefix("prefix"),
|
||||||
ConfigOption.Source(sourcePath),
|
ConfigOption.Source(sourcePath),
|
||||||
|
@ -38,9 +35,7 @@ class ActionGeneratorSuite extends FunSpec {
|
||||||
sourcePath,
|
sourcePath,
|
||||||
sources,
|
sources,
|
||||||
prefix)
|
prefix)
|
||||||
theRemoteMetadata = RemoteMetaData(theFile.remoteKey,
|
theRemoteMetadata = RemoteMetaData(theFile.remoteKey, theHash)
|
||||||
theHash,
|
|
||||||
lastModified)
|
|
||||||
input = MatchedMetadata(
|
input = MatchedMetadata(
|
||||||
theFile, // local exists
|
theFile, // local exists
|
||||||
matchByHash = Set(theRemoteMetadata), // remote matches
|
matchByHash = Set(theRemoteMetadata), // remote matches
|
||||||
|
@ -69,9 +64,7 @@ class ActionGeneratorSuite extends FunSpec {
|
||||||
prefix)
|
prefix)
|
||||||
theRemoteKey = theFile.remoteKey
|
theRemoteKey = theFile.remoteKey
|
||||||
otherRemoteKey = RemoteKey.resolve("other-key")(prefix)
|
otherRemoteKey = RemoteKey.resolve("other-key")(prefix)
|
||||||
otherRemoteMetadata = RemoteMetaData(otherRemoteKey,
|
otherRemoteMetadata = RemoteMetaData(otherRemoteKey, theHash)
|
||||||
theHash,
|
|
||||||
lastModified)
|
|
||||||
input = MatchedMetadata(
|
input = MatchedMetadata(
|
||||||
theFile, // local exists
|
theFile, // local exists
|
||||||
matchByHash = Set(otherRemoteMetadata), // other matches
|
matchByHash = Set(otherRemoteMetadata), // other matches
|
||||||
|
@ -128,12 +121,10 @@ class ActionGeneratorSuite extends FunSpec {
|
||||||
theRemoteKey = theFile.remoteKey
|
theRemoteKey = theFile.remoteKey
|
||||||
oldHash = MD5Hash("old-hash")
|
oldHash = MD5Hash("old-hash")
|
||||||
otherRemoteKey = RemoteKey.resolve("other-key")(prefix)
|
otherRemoteKey = RemoteKey.resolve("other-key")(prefix)
|
||||||
otherRemoteMetadata = RemoteMetaData(otherRemoteKey,
|
otherRemoteMetadata = RemoteMetaData(otherRemoteKey, theHash)
|
||||||
theHash,
|
|
||||||
lastModified)
|
|
||||||
oldRemoteMetadata = RemoteMetaData(theRemoteKey,
|
oldRemoteMetadata = RemoteMetaData(theRemoteKey,
|
||||||
hash = oldHash, // remote no match
|
hash = oldHash // remote no match
|
||||||
lastModified = lastModified)
|
)
|
||||||
input = MatchedMetadata(
|
input = MatchedMetadata(
|
||||||
theFile, // local exists
|
theFile, // local exists
|
||||||
matchByHash = Set(otherRemoteMetadata), // other matches
|
matchByHash = Set(otherRemoteMetadata), // other matches
|
||||||
|
@ -166,7 +157,7 @@ class ActionGeneratorSuite extends FunSpec {
|
||||||
prefix)
|
prefix)
|
||||||
theRemoteKey = theFile.remoteKey
|
theRemoteKey = theFile.remoteKey
|
||||||
oldHash = MD5Hash("old-hash")
|
oldHash = MD5Hash("old-hash")
|
||||||
theRemoteMetadata = RemoteMetaData(theRemoteKey, oldHash, lastModified)
|
theRemoteMetadata = RemoteMetaData(theRemoteKey, oldHash)
|
||||||
input = MatchedMetadata(
|
input = MatchedMetadata(
|
||||||
theFile, // local exists
|
theFile, // local exists
|
||||||
matchByHash = Set.empty, // remote no match, other no match
|
matchByHash = Set.empty, // remote no match, other no match
|
||||||
|
@ -206,7 +197,7 @@ class ActionGeneratorSuite extends FunSpec {
|
||||||
for {
|
for {
|
||||||
config <- ConfigurationBuilder.buildConfig(configOptions)
|
config <- ConfigurationBuilder.buildConfig(configOptions)
|
||||||
_ <- Config.set(config)
|
_ <- Config.set(config)
|
||||||
actions <- ActionGenerator.createAction(input, previousActions)
|
actions <- ActionGenerator.createActions(input, previousActions)
|
||||||
} yield actions
|
} yield actions
|
||||||
|
|
||||||
new DefaultRuntime {}.unsafeRunSync {
|
new DefaultRuntime {}.unsafeRunSync {
|
||||||
|
|
|
@ -7,7 +7,8 @@ import net.kemitix.thorp.domain._
|
||||||
import net.kemitix.thorp.storage.api.Storage
|
import net.kemitix.thorp.storage.api.Storage
|
||||||
import zio.{RIO, UIO}
|
import zio.{RIO, UIO}
|
||||||
|
|
||||||
case class DummyStorageService(remoteObjects: RemoteObjects,
|
final case class DummyStorageService(
|
||||||
|
remoteObjects: RemoteObjects,
|
||||||
uploadFiles: Map[File, (RemoteKey, MD5Hash)])
|
uploadFiles: Map[File, (RemoteKey, MD5Hash)])
|
||||||
extends Storage.Service {
|
extends Storage.Service {
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ class FiltersSuite extends FunSpec {
|
||||||
describe("Include") {
|
describe("Include") {
|
||||||
|
|
||||||
describe("default filter") {
|
describe("default filter") {
|
||||||
val include = Include()
|
val include = Include.all
|
||||||
it("should include files") {
|
it("should include files") {
|
||||||
paths.foreach(path =>
|
paths.foreach(path =>
|
||||||
assertResult(true)(Filters.isIncludedByFilter(path)(include)))
|
assertResult(true)(Filters.isIncludedByFilter(path)(include)))
|
||||||
|
@ -43,10 +43,12 @@ class FiltersSuite extends FunSpec {
|
||||||
val matching = Paths.get("/upload/root-file")
|
val matching = Paths.get("/upload/root-file")
|
||||||
assertResult(true)(Filters.isIncludedByFilter(matching)(include))
|
assertResult(true)(Filters.isIncludedByFilter(matching)(include))
|
||||||
}
|
}
|
||||||
it("exclude non-matching files 'test-file-for-hash.txt' & '/upload/subdir/leaf-file'") {
|
it("exclude non-matching files 'test-file-for-hash.txt'") {
|
||||||
val nonMatching1 = Paths.get("/test-file-for-hash.txt")
|
val nonMatching1 = Paths.get("/test-file-for-hash.txt")
|
||||||
val nonMatching2 = Paths.get("/upload/subdir/leaf-file")
|
|
||||||
assertResult(false)(Filters.isIncludedByFilter(nonMatching1)(include))
|
assertResult(false)(Filters.isIncludedByFilter(nonMatching1)(include))
|
||||||
|
}
|
||||||
|
it("exclude non-matching files '/upload/subdir/leaf-file'") {
|
||||||
|
val nonMatching2 = Paths.get("/upload/subdir/leaf-file")
|
||||||
assertResult(false)(Filters.isIncludedByFilter(nonMatching2)(include))
|
assertResult(false)(Filters.isIncludedByFilter(nonMatching2)(include))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,10 +80,12 @@ class FiltersSuite extends FunSpec {
|
||||||
val matching = Paths.get("/upload/root-file")
|
val matching = Paths.get("/upload/root-file")
|
||||||
assertResult(true)(Filters.isExcludedByFilter(matching)(exclude))
|
assertResult(true)(Filters.isExcludedByFilter(matching)(exclude))
|
||||||
}
|
}
|
||||||
it("include non-matching files 'test-file-for-hash.txt' & '/upload/subdir/leaf-file'") {
|
it("include non-matching files 'test-file-for-hash.txt'") {
|
||||||
val nonMatching1 = Paths.get("/test-file-for-hash.txt")
|
val nonMatching1 = Paths.get("/test-file-for-hash.txt")
|
||||||
val nonMatching2 = Paths.get("/upload/subdir/leaf-file")
|
|
||||||
assertResult(false)(Filters.isExcludedByFilter(nonMatching1)(exclude))
|
assertResult(false)(Filters.isExcludedByFilter(nonMatching1)(exclude))
|
||||||
|
}
|
||||||
|
it("include non-matching files '/upload/subdir/leaf-file'") {
|
||||||
|
val nonMatching2 = Paths.get("/upload/subdir/leaf-file")
|
||||||
assertResult(false)(Filters.isExcludedByFilter(nonMatching2)(exclude))
|
assertResult(false)(Filters.isExcludedByFilter(nonMatching2)(exclude))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,7 +120,7 @@ class FiltersSuite extends FunSpec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
describe("when include .txt files, but then exclude everything trumps all") {
|
describe("when include .txt files, but then exclude everything trumps all") {
|
||||||
val filters = List(Include(".txt"), Exclude(".*"))
|
val filters = List[Filter](Include(".txt"), Exclude(".*"))
|
||||||
it("should include nothing") {
|
it("should include nothing") {
|
||||||
val expected = List()
|
val expected = List()
|
||||||
val result = invoke(filters)
|
val result = invoke(filters)
|
||||||
|
@ -124,7 +128,7 @@ class FiltersSuite extends FunSpec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
describe("when exclude everything except .txt files") {
|
describe("when exclude everything except .txt files") {
|
||||||
val filters = List(Exclude(".*"), Include(".txt"))
|
val filters = List[Filter](Exclude(".*"), Include(".txt"))
|
||||||
it("should include only the .txt files") {
|
it("should include only the .txt files") {
|
||||||
val expected = List(path2, path3).map(Paths.get(_))
|
val expected = List(path2, path3).map(Paths.get(_))
|
||||||
val result = invoke(filters)
|
val result = invoke(filters)
|
||||||
|
|
|
@ -32,7 +32,7 @@ class LocalFileStreamSuite extends FunSpec {
|
||||||
val result =
|
val result =
|
||||||
invoke()
|
invoke()
|
||||||
.map(_.localFiles)
|
.map(_.localFiles)
|
||||||
.map(_.map(LocalFile.relativeToSource(_).toString))
|
.map(_.map(LocalFile.relativeToSource(_).toFile.getPath))
|
||||||
.map(_.toSet)
|
.map(_.toSet)
|
||||||
assertResult(expected)(result)
|
assertResult(expected)(result)
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ class LocalFileStreamSuite extends FunSpec {
|
||||||
file("subdir/leaf-file") -> Map(MD5 -> MD5HashData.Leaf.hash)
|
file("subdir/leaf-file") -> Map(MD5 -> MD5HashData.Leaf.hash)
|
||||||
))
|
))
|
||||||
val configOptions = ConfigOptions(
|
val configOptions = ConfigOptions(
|
||||||
List(
|
List[ConfigOption](
|
||||||
ConfigOption.IgnoreGlobalOptions,
|
ConfigOption.IgnoreGlobalOptions,
|
||||||
ConfigOption.IgnoreUserOptions,
|
ConfigOption.IgnoreUserOptions,
|
||||||
ConfigOption.Source(sourcePath),
|
ConfigOption.Source(sourcePath),
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package net.kemitix.thorp.core
|
package net.kemitix.thorp.core
|
||||||
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
import net.kemitix.thorp.config.Resource
|
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
|
||||||
|
@ -9,22 +7,19 @@ import net.kemitix.thorp.domain._
|
||||||
import org.scalatest.FunSpec
|
import org.scalatest.FunSpec
|
||||||
|
|
||||||
class MatchedMetadataEnricherSuite extends FunSpec {
|
class MatchedMetadataEnricherSuite extends FunSpec {
|
||||||
private 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 sources = Sources(List(sourcePath))
|
||||||
private val prefix = RemoteKey("prefix")
|
private val prefix = RemoteKey("prefix")
|
||||||
|
|
||||||
def getMatchesByKey(
|
def getMatchesByKey(
|
||||||
status: (Option[HashModified], Set[(MD5Hash, KeyModified)]))
|
status: (Option[MD5Hash], Set[(RemoteKey, MD5Hash)])): Option[MD5Hash] = {
|
||||||
: Option[HashModified] = {
|
|
||||||
val (byKey, _) = status
|
val (byKey, _) = status
|
||||||
byKey
|
byKey
|
||||||
}
|
}
|
||||||
|
|
||||||
def getMatchesByHash(
|
def getMatchesByHash(status: (Option[MD5Hash], Set[(RemoteKey, MD5Hash)]))
|
||||||
status: (Option[HashModified], Set[(MD5Hash, KeyModified)]))
|
: Set[(RemoteKey, MD5Hash)] = {
|
||||||
: Set[(MD5Hash, KeyModified)] = {
|
|
||||||
val (_, byHash) = status
|
val (_, byHash) = status
|
||||||
byHash
|
byHash
|
||||||
}
|
}
|
||||||
|
@ -42,10 +37,10 @@ class MatchedMetadataEnricherSuite extends FunSpec {
|
||||||
prefix)
|
prefix)
|
||||||
theRemoteKey = theFile.remoteKey
|
theRemoteKey = theFile.remoteKey
|
||||||
remoteObjects = RemoteObjects(
|
remoteObjects = RemoteObjects(
|
||||||
byHash = Map(theHash -> Set(KeyModified(theRemoteKey, lastModified))),
|
byHash = Map(theHash -> Set(theRemoteKey)),
|
||||||
byKey = Map(theRemoteKey -> HashModified(theHash, lastModified))
|
byKey = Map(theRemoteKey -> theHash)
|
||||||
)
|
)
|
||||||
theRemoteMetadata = RemoteMetaData(theRemoteKey, theHash, lastModified)
|
theRemoteMetadata = RemoteMetaData(theRemoteKey, theHash)
|
||||||
} yield (theFile, theRemoteMetadata, remoteObjects)
|
} yield (theFile, theRemoteMetadata, remoteObjects)
|
||||||
it("generates valid metadata") {
|
it("generates valid metadata") {
|
||||||
env.map({
|
env.map({
|
||||||
|
@ -70,10 +65,10 @@ class MatchedMetadataEnricherSuite extends FunSpec {
|
||||||
prefix)
|
prefix)
|
||||||
theRemoteKey: RemoteKey = RemoteKey.resolve("the-file")(prefix)
|
theRemoteKey: RemoteKey = RemoteKey.resolve("the-file")(prefix)
|
||||||
remoteObjects = RemoteObjects(
|
remoteObjects = RemoteObjects(
|
||||||
byHash = Map(theHash -> Set(KeyModified(theRemoteKey, lastModified))),
|
byHash = Map(theHash -> Set(theRemoteKey)),
|
||||||
byKey = Map(theRemoteKey -> HashModified(theHash, lastModified))
|
byKey = Map(theRemoteKey -> theHash)
|
||||||
)
|
)
|
||||||
theRemoteMetadata = RemoteMetaData(theRemoteKey, theHash, lastModified)
|
theRemoteMetadata = RemoteMetaData(theRemoteKey, theHash)
|
||||||
} yield (theFile, theRemoteMetadata, remoteObjects)
|
} yield (theFile, theRemoteMetadata, remoteObjects)
|
||||||
it("generates valid metadata") {
|
it("generates valid metadata") {
|
||||||
env.map({
|
env.map({
|
||||||
|
@ -98,13 +93,10 @@ class MatchedMetadataEnricherSuite extends FunSpec {
|
||||||
prefix)
|
prefix)
|
||||||
otherRemoteKey = RemoteKey("other-key")
|
otherRemoteKey = RemoteKey("other-key")
|
||||||
remoteObjects = RemoteObjects(
|
remoteObjects = RemoteObjects(
|
||||||
byHash =
|
byHash = Map(theHash -> Set(otherRemoteKey)),
|
||||||
Map(theHash -> Set(KeyModified(otherRemoteKey, lastModified))),
|
byKey = Map(otherRemoteKey -> theHash)
|
||||||
byKey = Map(otherRemoteKey -> HashModified(theHash, lastModified))
|
|
||||||
)
|
)
|
||||||
otherRemoteMetadata = RemoteMetaData(otherRemoteKey,
|
otherRemoteMetadata = RemoteMetaData(otherRemoteKey, theHash)
|
||||||
theHash,
|
|
||||||
lastModified)
|
|
||||||
} yield (theFile, otherRemoteMetadata, remoteObjects)
|
} yield (theFile, otherRemoteMetadata, remoteObjects)
|
||||||
it("generates valid metadata") {
|
it("generates valid metadata") {
|
||||||
env.map({
|
env.map({
|
||||||
|
@ -128,7 +120,7 @@ class MatchedMetadataEnricherSuite extends FunSpec {
|
||||||
sourcePath,
|
sourcePath,
|
||||||
sources,
|
sources,
|
||||||
prefix)
|
prefix)
|
||||||
remoteObjects = RemoteObjects()
|
remoteObjects = RemoteObjects.empty
|
||||||
} yield (theFile, remoteObjects)
|
} yield (theFile, remoteObjects)
|
||||||
it("generates valid metadata") {
|
it("generates valid metadata") {
|
||||||
env.map({
|
env.map({
|
||||||
|
@ -157,17 +149,14 @@ class MatchedMetadataEnricherSuite extends FunSpec {
|
||||||
otherRemoteKey = RemoteKey.resolve("other-key")(prefix)
|
otherRemoteKey = RemoteKey.resolve("other-key")(prefix)
|
||||||
remoteObjects = RemoteObjects(
|
remoteObjects = RemoteObjects(
|
||||||
byHash =
|
byHash =
|
||||||
Map(oldHash -> Set(KeyModified(theRemoteKey, lastModified)),
|
Map(oldHash -> Set(theRemoteKey), theHash -> Set(otherRemoteKey)),
|
||||||
theHash -> Set(KeyModified(otherRemoteKey, lastModified))),
|
|
||||||
byKey = Map(
|
byKey = Map(
|
||||||
theRemoteKey -> HashModified(oldHash, lastModified),
|
theRemoteKey -> oldHash,
|
||||||
otherRemoteKey -> HashModified(theHash, lastModified)
|
otherRemoteKey -> theHash
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
theRemoteMetadata = RemoteMetaData(theRemoteKey, oldHash, lastModified)
|
theRemoteMetadata = RemoteMetaData(theRemoteKey, oldHash)
|
||||||
otherRemoteMetadata = RemoteMetaData(otherRemoteKey,
|
otherRemoteMetadata = RemoteMetaData(otherRemoteKey, theHash)
|
||||||
theHash,
|
|
||||||
lastModified)
|
|
||||||
} yield (theFile, theRemoteMetadata, otherRemoteMetadata, remoteObjects)
|
} yield (theFile, theRemoteMetadata, otherRemoteMetadata, remoteObjects)
|
||||||
it("generates valid metadata") {
|
it("generates valid metadata") {
|
||||||
env.map({
|
env.map({
|
||||||
|
@ -197,13 +186,10 @@ class MatchedMetadataEnricherSuite extends FunSpec {
|
||||||
theRemoteKey = theFile.remoteKey
|
theRemoteKey = theFile.remoteKey
|
||||||
oldHash = MD5Hash("old-hash")
|
oldHash = MD5Hash("old-hash")
|
||||||
remoteObjects = RemoteObjects(
|
remoteObjects = RemoteObjects(
|
||||||
byHash = Map(oldHash -> Set(KeyModified(theRemoteKey, lastModified)),
|
byHash = Map(oldHash -> Set(theRemoteKey), theHash -> Set.empty),
|
||||||
theHash -> Set.empty),
|
byKey = Map(theRemoteKey -> oldHash)
|
||||||
byKey = Map(
|
|
||||||
theRemoteKey -> HashModified(oldHash, lastModified)
|
|
||||||
)
|
)
|
||||||
)
|
theRemoteMetadata = RemoteMetaData(theRemoteKey, oldHash)
|
||||||
theRemoteMetadata = RemoteMetaData(theRemoteKey, oldHash, lastModified)
|
|
||||||
} yield (theFile, theRemoteMetadata, remoteObjects)
|
} yield (theFile, theRemoteMetadata, remoteObjects)
|
||||||
it("generates valid metadata") {
|
it("generates valid metadata") {
|
||||||
env.map({
|
env.map({
|
||||||
|
@ -243,17 +229,15 @@ class MatchedMetadataEnricherSuite extends FunSpec {
|
||||||
sourcePath,
|
sourcePath,
|
||||||
sources,
|
sources,
|
||||||
prefix)
|
prefix)
|
||||||
lastModified = LastModified(Instant.now)
|
|
||||||
remoteObjects = RemoteObjects(
|
remoteObjects = RemoteObjects(
|
||||||
byHash = Map(
|
byHash = Map(
|
||||||
hash -> Set(KeyModified(key, lastModified),
|
hash -> Set(key, keyOtherKey.remoteKey),
|
||||||
KeyModified(keyOtherKey.remoteKey, lastModified)),
|
diffHash -> Set(keyDiffHash.remoteKey)
|
||||||
diffHash -> Set(KeyModified(keyDiffHash.remoteKey, lastModified))
|
|
||||||
),
|
),
|
||||||
byKey = Map(
|
byKey = Map(
|
||||||
key -> HashModified(hash, lastModified),
|
key -> hash,
|
||||||
keyOtherKey.remoteKey -> HashModified(hash, lastModified),
|
keyOtherKey.remoteKey -> hash,
|
||||||
keyDiffHash.remoteKey -> HashModified(diffHash, lastModified)
|
keyDiffHash.remoteKey -> diffHash
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} yield (remoteObjects, localFile, keyDiffHash, diffHash)
|
} yield (remoteObjects, localFile, keyDiffHash, diffHash)
|
||||||
|
@ -267,7 +251,7 @@ class MatchedMetadataEnricherSuite extends FunSpec {
|
||||||
env.map({
|
env.map({
|
||||||
case (remoteObjects, localFile: LocalFile, _, _) =>
|
case (remoteObjects, localFile: LocalFile, _, _) =>
|
||||||
val result = getMatchesByKey(invoke(localFile, remoteObjects))
|
val result = getMatchesByKey(invoke(localFile, remoteObjects))
|
||||||
assert(result.contains(HashModified(hash, lastModified)))
|
assert(result.contains(hash))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -307,17 +291,15 @@ class MatchedMetadataEnricherSuite extends FunSpec {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("when remote key exists and no others match hash") {
|
describe("when remote key exists and no others match hash") {
|
||||||
env.map({
|
val _ = env.map({
|
||||||
case (remoteObjects, _, keyDiffHash, diffHash) => {
|
case (remoteObjects, _, keyDiffHash, diffHash) => {
|
||||||
it("should return match by key") {
|
it("should return match by key") {
|
||||||
val result = getMatchesByKey(invoke(keyDiffHash, remoteObjects))
|
val result = getMatchesByKey(invoke(keyDiffHash, remoteObjects))
|
||||||
assert(result.contains(HashModified(diffHash, lastModified)))
|
assert(result.contains(diffHash))
|
||||||
}
|
}
|
||||||
it("should return only itself in match by hash") {
|
it("should return only itself in match by hash") {
|
||||||
val result = getMatchesByHash(invoke(keyDiffHash, remoteObjects))
|
val result = getMatchesByHash(invoke(keyDiffHash, remoteObjects))
|
||||||
assert(
|
assert(result === Set((keyDiffHash.remoteKey, diffHash)))
|
||||||
result.equals(Set(
|
|
||||||
(diffHash, KeyModified(keyDiffHash.remoteKey, lastModified)))))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -21,8 +21,7 @@ import zio.{DefaultRuntime, Task, UIO}
|
||||||
|
|
||||||
class PlanBuilderTest extends FreeSpec with TemporaryFolder {
|
class PlanBuilderTest extends FreeSpec with TemporaryFolder {
|
||||||
|
|
||||||
private val lastModified: LastModified = LastModified()
|
private val emptyRemoteObjects = RemoteObjects.empty
|
||||||
private val emptyRemoteObjects = RemoteObjects()
|
|
||||||
|
|
||||||
"create a plan" - {
|
"create a plan" - {
|
||||||
|
|
||||||
|
@ -63,9 +62,8 @@ class PlanBuilderTest extends FreeSpec with TemporaryFolder {
|
||||||
val anOtherKey = RemoteKey("other")
|
val anOtherKey = RemoteKey("other")
|
||||||
val expected = Right(List(toCopy(anOtherKey, aHash, remoteKey)))
|
val expected = Right(List(toCopy(anOtherKey, aHash, remoteKey)))
|
||||||
val remoteObjects = RemoteObjects(
|
val remoteObjects = RemoteObjects(
|
||||||
byHash =
|
byHash = Map(aHash -> Set(anOtherKey)),
|
||||||
Map(aHash -> Set(KeyModified(anOtherKey, lastModified))),
|
byKey = Map(anOtherKey -> aHash)
|
||||||
byKey = Map(anOtherKey -> HashModified(aHash, lastModified))
|
|
||||||
)
|
)
|
||||||
val result =
|
val result =
|
||||||
invoke(options(source),
|
invoke(options(source),
|
||||||
|
@ -86,9 +84,8 @@ class PlanBuilderTest extends FreeSpec with TemporaryFolder {
|
||||||
// DoNothing actions should have been filtered out of the plan
|
// DoNothing actions should have been filtered out of the plan
|
||||||
val expected = Right(List())
|
val expected = Right(List())
|
||||||
val remoteObjects = RemoteObjects(
|
val remoteObjects = RemoteObjects(
|
||||||
byHash =
|
byHash = Map(hash -> Set(remoteKey)),
|
||||||
Map(hash -> Set(KeyModified(remoteKey, lastModified))),
|
byKey = Map(remoteKey -> hash)
|
||||||
byKey = Map(remoteKey -> HashModified(hash, lastModified))
|
|
||||||
)
|
)
|
||||||
val result =
|
val result =
|
||||||
invoke(options(source),
|
invoke(options(source),
|
||||||
|
@ -108,10 +105,8 @@ class PlanBuilderTest extends FreeSpec with TemporaryFolder {
|
||||||
val expected =
|
val expected =
|
||||||
Right(List(toUpload(remoteKey, currentHash, source, file)))
|
Right(List(toUpload(remoteKey, currentHash, source, file)))
|
||||||
val remoteObjects = RemoteObjects(
|
val remoteObjects = RemoteObjects(
|
||||||
byHash = Map(originalHash -> Set(
|
byHash = Map(originalHash -> Set(remoteKey)),
|
||||||
KeyModified(remoteKey, lastModified))),
|
byKey = Map(remoteKey -> originalHash)
|
||||||
byKey =
|
|
||||||
Map(remoteKey -> HashModified(originalHash, lastModified))
|
|
||||||
)
|
)
|
||||||
val result =
|
val result =
|
||||||
invoke(options(source),
|
invoke(options(source),
|
||||||
|
@ -129,9 +124,8 @@ class PlanBuilderTest extends FreeSpec with TemporaryFolder {
|
||||||
val sourceKey = RemoteKey("other-key")
|
val sourceKey = RemoteKey("other-key")
|
||||||
val expected = Right(List(toCopy(sourceKey, hash, remoteKey)))
|
val expected = Right(List(toCopy(sourceKey, hash, remoteKey)))
|
||||||
val remoteObjects = RemoteObjects(
|
val remoteObjects = RemoteObjects(
|
||||||
byHash =
|
byHash = Map(hash -> Set(sourceKey)),
|
||||||
Map(hash -> Set(KeyModified(sourceKey, lastModified))),
|
byKey = Map.empty
|
||||||
byKey = Map()
|
|
||||||
)
|
)
|
||||||
val result =
|
val result =
|
||||||
invoke(options(source),
|
invoke(options(source),
|
||||||
|
@ -155,8 +149,8 @@ class PlanBuilderTest extends FreeSpec with TemporaryFolder {
|
||||||
// DoNothing actions should have been filtered out of the plan
|
// DoNothing actions should have been filtered out of the plan
|
||||||
val expected = Right(List())
|
val expected = Right(List())
|
||||||
val remoteObjects = RemoteObjects(
|
val remoteObjects = RemoteObjects(
|
||||||
byHash = Map(hash -> Set(KeyModified(remoteKey, lastModified))),
|
byHash = Map(hash -> Set(remoteKey)),
|
||||||
byKey = Map(remoteKey -> HashModified(hash, lastModified))
|
byKey = Map(remoteKey -> hash)
|
||||||
)
|
)
|
||||||
val result =
|
val result =
|
||||||
invoke(options(source),
|
invoke(options(source),
|
||||||
|
@ -172,8 +166,8 @@ class PlanBuilderTest extends FreeSpec with TemporaryFolder {
|
||||||
val hash = MD5Hash("file-content")
|
val hash = MD5Hash("file-content")
|
||||||
val expected = Right(List(toDelete(remoteKey)))
|
val expected = Right(List(toDelete(remoteKey)))
|
||||||
val remoteObjects = RemoteObjects(
|
val remoteObjects = RemoteObjects(
|
||||||
byHash = Map(hash -> Set(KeyModified(remoteKey, lastModified))),
|
byHash = Map(hash -> Set(remoteKey)),
|
||||||
byKey = Map(remoteKey -> HashModified(hash, lastModified))
|
byKey = Map(remoteKey -> hash)
|
||||||
)
|
)
|
||||||
val result =
|
val result =
|
||||||
invoke(options(source),
|
invoke(options(source),
|
||||||
|
@ -208,7 +202,7 @@ class PlanBuilderTest extends FreeSpec with TemporaryFolder {
|
||||||
createFile(secondSource, filename2, "file-2-content")
|
createFile(secondSource, filename2, "file-2-content")
|
||||||
val hash2 = md5Hash(fileInSecondSource)
|
val hash2 = md5Hash(fileInSecondSource)
|
||||||
val expected = Right(
|
val expected = Right(
|
||||||
List(
|
Set(
|
||||||
toUpload(remoteKey2, hash2, secondSource, fileInSecondSource),
|
toUpload(remoteKey2, hash2, secondSource, fileInSecondSource),
|
||||||
toUpload(remoteKey1, hash1, firstSource, fileInFirstSource)
|
toUpload(remoteKey1, hash1, firstSource, fileInFirstSource)
|
||||||
))
|
))
|
||||||
|
@ -219,7 +213,7 @@ class PlanBuilderTest extends FreeSpec with TemporaryFolder {
|
||||||
UIO.succeed(
|
UIO.succeed(
|
||||||
Map(fileInFirstSource.toPath -> fileInFirstSource,
|
Map(fileInFirstSource.toPath -> fileInFirstSource,
|
||||||
fileInSecondSource.toPath -> fileInSecondSource))
|
fileInSecondSource.toPath -> fileInSecondSource))
|
||||||
)
|
).map(_.toSet)
|
||||||
assertResult(expected)(result)
|
assertResult(expected)(result)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -228,11 +222,11 @@ class PlanBuilderTest extends FreeSpec with TemporaryFolder {
|
||||||
"same filename in both" - {
|
"same filename in both" - {
|
||||||
"only upload file in first source" in {
|
"only upload file in first source" in {
|
||||||
withDirectory(firstSource => {
|
withDirectory(firstSource => {
|
||||||
val fileInFirstSource: File =
|
val fileInFirstSource =
|
||||||
createFile(firstSource, filename1, "file-1-content")
|
createFile(firstSource, filename1, "file-1-content")
|
||||||
val hash1 = md5Hash(fileInFirstSource)
|
val hash1 = md5Hash(fileInFirstSource)
|
||||||
withDirectory(secondSource => {
|
withDirectory(secondSource => {
|
||||||
val fileInSecondSource: File =
|
val fileInSecondSource =
|
||||||
createFile(secondSource, filename1, "file-2-content")
|
createFile(secondSource, filename1, "file-2-content")
|
||||||
val hash2 = md5Hash(fileInSecondSource)
|
val hash2 = md5Hash(fileInSecondSource)
|
||||||
val expected = Right(List(
|
val expected = Right(List(
|
||||||
|
@ -258,10 +252,9 @@ class PlanBuilderTest extends FreeSpec with TemporaryFolder {
|
||||||
createFile(secondSource, filename2, "file-2-content")
|
createFile(secondSource, filename2, "file-2-content")
|
||||||
val hash2 = md5Hash(fileInSecondSource)
|
val hash2 = md5Hash(fileInSecondSource)
|
||||||
val expected = Right(List())
|
val expected = Right(List())
|
||||||
val remoteObjects = RemoteObjects(
|
val remoteObjects =
|
||||||
byHash =
|
RemoteObjects(byHash = Map(hash2 -> Set(remoteKey2)),
|
||||||
Map(hash2 -> Set(KeyModified(remoteKey2, lastModified))),
|
byKey = Map(remoteKey2 -> hash2))
|
||||||
byKey = Map(remoteKey2 -> HashModified(hash2, lastModified)))
|
|
||||||
val result =
|
val result =
|
||||||
invoke(options(firstSource)(secondSource),
|
invoke(options(firstSource)(secondSource),
|
||||||
UIO.succeed(remoteObjects),
|
UIO.succeed(remoteObjects),
|
||||||
|
@ -280,10 +273,9 @@ class PlanBuilderTest extends FreeSpec with TemporaryFolder {
|
||||||
val hash1 = md5Hash(fileInFirstSource)
|
val hash1 = md5Hash(fileInFirstSource)
|
||||||
withDirectory(secondSource => {
|
withDirectory(secondSource => {
|
||||||
val expected = Right(List())
|
val expected = Right(List())
|
||||||
val remoteObjects = RemoteObjects(
|
val remoteObjects =
|
||||||
byHash =
|
RemoteObjects(byHash = Map(hash1 -> Set(remoteKey1)),
|
||||||
Map(hash1 -> Set(KeyModified(remoteKey1, lastModified))),
|
byKey = Map(remoteKey1 -> hash1))
|
||||||
byKey = Map(remoteKey1 -> HashModified(hash1, lastModified)))
|
|
||||||
val result =
|
val result =
|
||||||
invoke(options(firstSource)(secondSource),
|
invoke(options(firstSource)(secondSource),
|
||||||
UIO.succeed(remoteObjects),
|
UIO.succeed(remoteObjects),
|
||||||
|
@ -299,8 +291,9 @@ class PlanBuilderTest extends FreeSpec with TemporaryFolder {
|
||||||
withDirectory(firstSource => {
|
withDirectory(firstSource => {
|
||||||
withDirectory(secondSource => {
|
withDirectory(secondSource => {
|
||||||
val expected = Right(List(toDelete(remoteKey1)))
|
val expected = Right(List(toDelete(remoteKey1)))
|
||||||
val remoteObjects = RemoteObjects(byKey =
|
val remoteObjects =
|
||||||
Map(remoteKey1 -> HashModified(MD5Hash(""), lastModified)))
|
RemoteObjects(byHash = Map.empty,
|
||||||
|
byKey = Map(remoteKey1 -> MD5Hash("")))
|
||||||
val result =
|
val result =
|
||||||
invoke(options(firstSource)(secondSource),
|
invoke(options(firstSource)(secondSource),
|
||||||
UIO.succeed(remoteObjects),
|
UIO.succeed(remoteObjects),
|
||||||
|
@ -336,7 +329,7 @@ class PlanBuilderTest extends FreeSpec with TemporaryFolder {
|
||||||
("upload",
|
("upload",
|
||||||
remoteKey.key,
|
remoteKey.key,
|
||||||
MD5Hash.hash(md5Hash),
|
MD5Hash.hash(md5Hash),
|
||||||
source.toString,
|
source.toFile.getPath,
|
||||||
file.toString)
|
file.toString)
|
||||||
|
|
||||||
private def toCopy(
|
private def toCopy(
|
||||||
|
@ -398,6 +391,6 @@ class PlanBuilderTest extends FreeSpec with TemporaryFolder {
|
||||||
("copy", sourceKey.key, MD5Hash.hash(hash), targetKey.key, "")
|
("copy", sourceKey.key, MD5Hash.hash(hash), targetKey.key, "")
|
||||||
case DoNothing(_, remoteKey, _) =>
|
case DoNothing(_, remoteKey, _) =>
|
||||||
("do-nothing", remoteKey.key, "", "", "")
|
("do-nothing", remoteKey.key, "", "", "")
|
||||||
case x => ("other", x.toString, "", "", "")
|
case x => ("other", "", "", "", "")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ class SequencePlanTest extends FreeSpec {
|
||||||
val source = new File("source")
|
val source = new File("source")
|
||||||
val localFile1 =
|
val localFile1 =
|
||||||
LocalFile(file1, source, hashes, remoteKey1)
|
LocalFile(file1, source, hashes, remoteKey1)
|
||||||
val localFile2 =
|
val _ =
|
||||||
LocalFile(file2, source, hashes, remoteKey2)
|
LocalFile(file2, source, hashes, remoteKey2)
|
||||||
val copy1 = ToCopy(bucket, remoteKey1, hash, remoteKey2, size)
|
val copy1 = ToCopy(bucket, remoteKey1, hash, remoteKey2, size)
|
||||||
val copy2 = ToCopy(bucket, remoteKey2, hash, remoteKey1, size)
|
val copy2 = ToCopy(bucket, remoteKey2, hash, remoteKey1, size)
|
||||||
|
@ -36,8 +36,10 @@ class SequencePlanTest extends FreeSpec {
|
||||||
val delete1 = ToDelete(bucket, remoteKey1, size)
|
val delete1 = ToDelete(bucket, remoteKey1, size)
|
||||||
val delete2 = ToDelete(bucket, remoteKey2, size)
|
val delete2 = ToDelete(bucket, remoteKey2, size)
|
||||||
"should be in correct order" in {
|
"should be in correct order" in {
|
||||||
val actions = List(copy1, delete1, upload1, delete2, upload2, copy2)
|
val actions =
|
||||||
val expected = List(copy1, copy2, upload1, upload2, delete1, delete2)
|
List[Action](copy1, delete1, upload1, delete2, upload2, copy2)
|
||||||
|
val expected =
|
||||||
|
List[Action](copy1, copy2, upload1, upload2, delete1, delete2)
|
||||||
val result = actions.sortBy(SequencePlan.order)
|
val result = actions.sortBy(SequencePlan.order)
|
||||||
assertResult(expected)(result)
|
assertResult(expected)(result)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,10 +8,13 @@ sealed trait Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
object Filter {
|
object Filter {
|
||||||
case class Include(include: String = ".*") extends Filter {
|
final case class Include(include: String) extends Filter {
|
||||||
lazy val predicate: Predicate[String] = Pattern.compile(include).asPredicate
|
lazy val predicate: Predicate[String] = Pattern.compile(include).asPredicate
|
||||||
}
|
}
|
||||||
case class Exclude(exclude: String) extends Filter {
|
object Include {
|
||||||
|
def all: Include = Include(".*")
|
||||||
|
}
|
||||||
|
final case class Exclude(exclude: String) extends Filter {
|
||||||
lazy val predicate: Predicate[String] =
|
lazy val predicate: Predicate[String] =
|
||||||
Pattern.compile(exclude).asPredicate()
|
Pattern.compile(exclude).asPredicate()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
final case class HashModified(
|
|
||||||
hash: MD5Hash,
|
|
||||||
modified: LastModified
|
|
||||||
)
|
|
|
@ -6,7 +6,7 @@ trait HexEncoder {
|
||||||
|
|
||||||
def encode(bytes: Array[Byte]): String =
|
def encode(bytes: Array[Byte]): String =
|
||||||
String
|
String
|
||||||
.format("%0" + (bytes.length << 1) + "x", new BigInteger(1, bytes))
|
.format(s"%0${bytes.length << 1}x", new BigInteger(1, bytes))
|
||||||
.toUpperCase
|
.toUpperCase
|
||||||
|
|
||||||
def decode(hexString: String): Array[Byte] =
|
def decode(hexString: String): Array[Byte] =
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
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,6 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
final case class KeyModified(
|
|
||||||
key: RemoteKey,
|
|
||||||
modified: LastModified
|
|
||||||
)
|
|
|
@ -1,7 +0,0 @@
|
||||||
package net.kemitix.thorp.domain
|
|
||||||
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
final case class LastModified(
|
|
||||||
when: Instant = Instant.now
|
|
||||||
)
|
|
|
@ -4,6 +4,7 @@ import java.io.File
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.HashType.MD5
|
import net.kemitix.thorp.domain.HashType.MD5
|
||||||
|
import Implicits._
|
||||||
|
|
||||||
final case class LocalFile private (
|
final case class LocalFile private (
|
||||||
file: File,
|
file: File,
|
||||||
|
@ -20,7 +21,7 @@ object LocalFile {
|
||||||
def relativeToSource(localFile: LocalFile): Path =
|
def relativeToSource(localFile: LocalFile): Path =
|
||||||
localFile.source.toPath.relativize(localFile.file.toPath)
|
localFile.source.toPath.relativize(localFile.file.toPath)
|
||||||
def matchesHash(localFile: LocalFile)(other: MD5Hash): Boolean =
|
def matchesHash(localFile: LocalFile)(other: MD5Hash): Boolean =
|
||||||
localFile.hashes.values.exists(other equals _)
|
localFile.hashes.values.exists(other === _)
|
||||||
def md5base64(localFile: LocalFile): Option[String] =
|
def md5base64(localFile: LocalFile): Option[String] =
|
||||||
localFile.hashes.get(MD5).map(MD5Hash.hash64)
|
localFile.hashes.get(MD5).map(MD5Hash.hash64)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,6 @@ package net.kemitix.thorp.domain
|
||||||
// For the LocalFile, the set of matching S3 objects with the same MD5Hash, and any S3 object with the same remote key
|
// For the LocalFile, the set of matching S3 objects with the same MD5Hash, and any S3 object with the same remote key
|
||||||
final case class MatchedMetadata(
|
final case class MatchedMetadata(
|
||||||
localFile: LocalFile,
|
localFile: LocalFile,
|
||||||
matchByHash: Set[RemoteMetaData],
|
matchByHash: Set[RemoteMetaData], //TODO Can this be an Option?
|
||||||
matchByKey: Option[RemoteMetaData]
|
matchByKey: Option[RemoteMetaData]
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package net.kemitix.thorp.domain
|
||||||
|
|
||||||
|
object NonUnit {
|
||||||
|
@specialized def ~*[A](evaluateForSideEffectOnly: A): Unit = {
|
||||||
|
val _: A = evaluateForSideEffectOnly
|
||||||
|
() //Return unit to prevent warning due to discarding value
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
package net.kemitix.thorp.domain
|
package net.kemitix.thorp.domain
|
||||||
|
|
||||||
|
import Implicits._
|
||||||
|
|
||||||
object QuoteStripper {
|
object QuoteStripper {
|
||||||
|
|
||||||
def stripQuotes: Char => Boolean = _ != '"'
|
def stripQuotes: Char => Boolean = _ =/= '"'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package net.kemitix.thorp.domain
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.file.{Path, Paths}
|
import java.nio.file.{Path, Paths}
|
||||||
|
import Implicits._
|
||||||
|
|
||||||
final case class RemoteKey(key: String)
|
final case class RemoteKey(key: String)
|
||||||
|
|
||||||
|
@ -10,7 +11,7 @@ object RemoteKey {
|
||||||
SimpleLens[RemoteKey, String](_.key, b => a => b.copy(key = a))
|
SimpleLens[RemoteKey, String](_.key, b => a => b.copy(key = a))
|
||||||
def asFile(source: Path, prefix: RemoteKey)(
|
def asFile(source: Path, prefix: RemoteKey)(
|
||||||
remoteKey: RemoteKey): Option[File] =
|
remoteKey: RemoteKey): Option[File] =
|
||||||
if (remoteKey.key.length == 0) None
|
if (remoteKey.key.length === 0) None
|
||||||
else Some(source.resolve(RemoteKey.relativeTo(prefix)(remoteKey)).toFile)
|
else Some(source.resolve(RemoteKey.relativeTo(prefix)(remoteKey)).toFile)
|
||||||
def relativeTo(prefix: RemoteKey)(remoteKey: RemoteKey): Path = {
|
def relativeTo(prefix: RemoteKey)(remoteKey: RemoteKey): Path = {
|
||||||
prefix match {
|
prefix match {
|
||||||
|
|
|
@ -2,6 +2,5 @@ package net.kemitix.thorp.domain
|
||||||
|
|
||||||
final case class RemoteMetaData(
|
final case class RemoteMetaData(
|
||||||
remoteKey: RemoteKey,
|
remoteKey: RemoteKey,
|
||||||
hash: MD5Hash,
|
hash: MD5Hash
|
||||||
lastModified: LastModified
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,7 +3,14 @@ package net.kemitix.thorp.domain
|
||||||
/**
|
/**
|
||||||
* A list of objects and their MD5 hash values.
|
* A list of objects and their MD5 hash values.
|
||||||
*/
|
*/
|
||||||
final case class RemoteObjects(
|
final case class RemoteObjects private (
|
||||||
byHash: Map[MD5Hash, Set[KeyModified]] = Map.empty,
|
byHash: Map[MD5Hash, Set[RemoteKey]],
|
||||||
byKey: Map[RemoteKey, HashModified] = Map.empty
|
byKey: Map[RemoteKey, MD5Hash]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
object RemoteObjects {
|
||||||
|
val empty: RemoteObjects = RemoteObjects(Map.empty, Map.empty)
|
||||||
|
def create(byHash: Map[MD5Hash, Set[RemoteKey]],
|
||||||
|
byKey: Map[RemoteKey, MD5Hash]): RemoteObjects =
|
||||||
|
RemoteObjects(byHash, byKey)
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package net.kemitix.thorp.domain
|
package net.kemitix.thorp.domain
|
||||||
|
|
||||||
case class SimpleLens[A, B](field: A => B, update: A => B => A) {
|
final case class SimpleLens[A, B](field: A => B, update: A => B => A) {
|
||||||
|
|
||||||
def composeLens[C](other: SimpleLens[B, C]): SimpleLens[A, C] =
|
def composeLens[C](other: SimpleLens[B, C]): SimpleLens[A, C] =
|
||||||
SimpleLens[A, C](
|
SimpleLens[A, C](
|
||||||
|
|
|
@ -2,9 +2,9 @@ package net.kemitix.thorp.domain
|
||||||
|
|
||||||
object SizeTranslation {
|
object SizeTranslation {
|
||||||
|
|
||||||
val kbLimit = 10240L
|
val kbLimit: Long = 10240L
|
||||||
val mbLimit = kbLimit * 1024
|
val mbLimit: Long = kbLimit * 1024
|
||||||
val gbLimit = mbLimit * 1024
|
val gbLimit: Long = mbLimit * 1024
|
||||||
|
|
||||||
def sizeInEnglish(length: Long): String =
|
def sizeInEnglish(length: Long): String =
|
||||||
length.toDouble match {
|
length.toDouble match {
|
||||||
|
|
|
@ -14,24 +14,17 @@ import zio.{Task, ZIO}
|
||||||
*
|
*
|
||||||
* A path should only occur once in paths.
|
* A path should only occur once in paths.
|
||||||
*/
|
*/
|
||||||
case class Sources(
|
final case class Sources(
|
||||||
paths: List[Path]
|
paths: List[Path]
|
||||||
) {
|
) {
|
||||||
def +(path: Path)(implicit m: Monoid[Sources]): Sources = this ++ List(path)
|
def +(path: Path): Sources = this ++ List(path)
|
||||||
def ++(otherPaths: List[Path])(implicit m: Monoid[Sources]): Sources =
|
def ++(otherPaths: List[Path]): Sources =
|
||||||
m.op(this, Sources(otherPaths))
|
Sources(otherPaths.foldLeft(paths)((acc, path) =>
|
||||||
|
if (acc contains path) acc else acc ++ List(path)))
|
||||||
}
|
}
|
||||||
|
|
||||||
object Sources {
|
object Sources {
|
||||||
final val emptySources = Sources(List.empty)
|
val emptySources: Sources = Sources(List.empty)
|
||||||
implicit def sourcesAppendMonoid: Monoid[Sources] = new Monoid[Sources] {
|
|
||||||
override def zero: Sources = emptySources
|
|
||||||
override def op(t1: Sources, t2: Sources): Sources =
|
|
||||||
Sources(t2.paths.foldLeft(t1.paths) { (acc, path) =>
|
|
||||||
if (acc.contains(path)) acc
|
|
||||||
else acc ++ List(path)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the source path for the given path.
|
* Returns the source path for the given path.
|
||||||
|
|
|
@ -35,13 +35,13 @@ object StorageQueueEvent {
|
||||||
val keys: String
|
val keys: String
|
||||||
}
|
}
|
||||||
object Action {
|
object Action {
|
||||||
case class Copy(keys: String) extends Action {
|
final case class Copy(keys: String) extends Action {
|
||||||
override val name: String = "Copy"
|
override val name: String = "Copy"
|
||||||
}
|
}
|
||||||
case class Upload(keys: String) extends Action {
|
final case class Upload(keys: String) extends Action {
|
||||||
override val name: String = "Upload"
|
override val name: String = "Upload"
|
||||||
}
|
}
|
||||||
case class Delete(keys: String) extends Action {
|
final case class Delete(keys: String) extends Action {
|
||||||
override val name: String = "Delete"
|
override val name: String = "Delete"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
package net.kemitix.thorp.domain
|
package net.kemitix.thorp.domain
|
||||||
|
|
||||||
case class SyncTotals(
|
final case class SyncTotals private (
|
||||||
count: Long = 0L,
|
count: Long,
|
||||||
totalSizeBytes: Long = 0L,
|
totalSizeBytes: Long,
|
||||||
sizeUploadedBytes: Long = 0L
|
sizeUploadedBytes: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
|
object SyncTotals {
|
||||||
|
val empty: SyncTotals = SyncTotals(0L, 0L, 0L)
|
||||||
|
def create(count: Long,
|
||||||
|
totalSizeBytes: Long,
|
||||||
|
sizeUploadedBytes: Long): SyncTotals =
|
||||||
|
SyncTotals(count, totalSizeBytes, sizeUploadedBytes)
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package net.kemitix.thorp.domain
|
package net.kemitix.thorp.domain
|
||||||
|
|
||||||
|
import Implicits._
|
||||||
|
|
||||||
object Terminal {
|
object Terminal {
|
||||||
|
|
||||||
val esc: String = "\u001B"
|
val esc: String = "\u001B"
|
||||||
|
@ -75,58 +77,58 @@ object Terminal {
|
||||||
*
|
*
|
||||||
* Stops at the edge of the screen.
|
* Stops at the edge of the screen.
|
||||||
*/
|
*/
|
||||||
def cursorUp(lines: Int = 1): String = csi + lines + "A"
|
def cursorUp(lines: Int): String = s"${csi}${lines}A"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the cursor down, default 1 line.
|
* Move the cursor down, default 1 line.
|
||||||
*
|
*
|
||||||
* Stops at the edge of the screen.
|
* Stops at the edge of the screen.
|
||||||
*/
|
*/
|
||||||
def cursorDown(lines: Int = 1): String = csi + lines + "B"
|
def cursorDown(lines: Int): String = s"${csi}${lines}B"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the cursor forward, default 1 column.
|
* Move the cursor forward, default 1 column.
|
||||||
*
|
*
|
||||||
* Stops at the edge of the screen.
|
* Stops at the edge of the screen.
|
||||||
*/
|
*/
|
||||||
def cursorForward(cols: Int = 1): String = csi + cols + "C"
|
def cursorForward(cols: Int): String = s"${csi}${cols}C"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the cursor back, default 1 column,
|
* Move the cursor back, default 1 column,
|
||||||
*
|
*
|
||||||
* Stops at the edge of the screen.
|
* Stops at the edge of the screen.
|
||||||
*/
|
*/
|
||||||
def cursorBack(cols: Int = 1): String = csi + cols + "D"
|
def cursorBack(cols: Int): String = s"${csi}${cols}D"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the cursor to the beginning of the line, default 1, down.
|
* Move the cursor to the beginning of the line, default 1, down.
|
||||||
*/
|
*/
|
||||||
def cursorNextLine(lines: Int = 1): String = csi + lines + "E"
|
def cursorNextLine(lines: Int): String = s"${csi}${lines}E"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the cursor to the beginning of the line, default 1, up.
|
* Move the cursor to the beginning of the line, default 1, up.
|
||||||
*/
|
*/
|
||||||
def cursorPrevLine(lines: Int = 1): String = csi + lines + "F"
|
def cursorPrevLine(lines: Int): String = s"${csi}${lines}F"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the cursor to the column on the current line.
|
* Move the cursor to the column on the current line.
|
||||||
*/
|
*/
|
||||||
def cursorHorizAbs(col: Int): String = csi + col + "G"
|
def cursorHorizAbs(col: Int): String = s"${csi}${col}G"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the cursor to the position on screen (1,1 is the top-left).
|
* Move the cursor to the position on screen (1,1 is the top-left).
|
||||||
*/
|
*/
|
||||||
def cursorPosition(row: Int, col: Int): String = csi + row + ";" + col + "H"
|
def cursorPosition(row: Int, col: Int): String = s"${csi}${row};${col}H"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scroll page up, default 1, lines.
|
* Scroll page up, default 1, lines.
|
||||||
*/
|
*/
|
||||||
def scrollUp(lines: Int = 1): String = csi + lines + "S"
|
def scrollUp(lines: Int): String = s"${csi}${lines}S"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scroll page down, default 1, lines.
|
* Scroll page down, default 1, lines.
|
||||||
*/
|
*/
|
||||||
def scrollDown(lines: Int = 1): String = csi + lines + "T"
|
def scrollDown(lines: Int): String = s"${csi}${lines}T"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Width of the terminal, as reported by the COLUMNS environment variable.
|
* The Width of the terminal, as reported by the COLUMNS environment variable.
|
||||||
|
@ -154,7 +156,7 @@ object Terminal {
|
||||||
val pxDone = pxWidth * ratio
|
val pxDone = pxWidth * ratio
|
||||||
val fullHeadSize: Int = (pxDone / phases).toInt
|
val fullHeadSize: Int = (pxDone / phases).toInt
|
||||||
val part = (pxDone % phases).toInt
|
val part = (pxDone % phases).toInt
|
||||||
val partial = if (part != 0) subBars.getOrElse(part, "") else ""
|
val partial = if (part =/= 0) subBars.getOrElse(part, "") else ""
|
||||||
val head = ("█" * fullHeadSize) + partial
|
val head = ("█" * fullHeadSize) + partial
|
||||||
val tailSize = barWidth - head.length
|
val tailSize = barWidth - head.length
|
||||||
val tail = " " * tailSize
|
val tail = " " * tailSize
|
||||||
|
|
|
@ -1,31 +1,34 @@
|
||||||
package net.kemitix.thorp.domain
|
package net.kemitix.thorp.domain
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.UploadEvent.RequestEvent
|
import net.kemitix.thorp.domain.UploadEvent.RequestEvent
|
||||||
import net.kemitix.thorp.domain.UploadEventLogger.RequestCycle
|
import net.kemitix.thorp.domain.UploadEventLogger.RequestCycle
|
||||||
|
|
||||||
object UploadEventListener {
|
object UploadEventListener {
|
||||||
|
|
||||||
case class Settings(
|
final case class Settings(
|
||||||
localFile: LocalFile,
|
localFile: LocalFile,
|
||||||
index: Int,
|
index: Int,
|
||||||
syncTotals: SyncTotals,
|
syncTotals: SyncTotals,
|
||||||
totalBytesSoFar: Long
|
totalBytesSoFar: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
def apply(settings: Settings): UploadEvent => Unit =
|
def listener(settings: Settings): UploadEvent => Unit = {
|
||||||
uploadEvent => {
|
val bytesTransferred = new AtomicLong(0L)
|
||||||
var bytesTransferred = 0L
|
event =>
|
||||||
uploadEvent match {
|
{
|
||||||
|
event match {
|
||||||
case e: RequestEvent =>
|
case e: RequestEvent =>
|
||||||
bytesTransferred += e.transferred
|
|
||||||
UploadEventLogger(
|
UploadEventLogger(
|
||||||
RequestCycle(settings.localFile,
|
RequestCycle(settings.localFile,
|
||||||
bytesTransferred,
|
bytesTransferred.addAndGet(e.transferred),
|
||||||
settings.index,
|
settings.index,
|
||||||
settings.syncTotals,
|
settings.syncTotals,
|
||||||
settings.totalBytesSoFar))
|
settings.totalBytesSoFar))
|
||||||
case _ => ()
|
case _ => ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import scala.io.AnsiColor._
|
||||||
|
|
||||||
object UploadEventLogger {
|
object UploadEventLogger {
|
||||||
|
|
||||||
case class RequestCycle(
|
final case class RequestCycle(
|
||||||
localFile: LocalFile,
|
localFile: LocalFile,
|
||||||
bytesTransferred: Long,
|
bytesTransferred: Long,
|
||||||
index: Int,
|
index: Int,
|
||||||
|
|
|
@ -3,24 +3,24 @@ package net.kemitix.thorp.domain
|
||||||
object MD5HashData {
|
object MD5HashData {
|
||||||
|
|
||||||
object Root {
|
object Root {
|
||||||
val hash = MD5Hash("a3a6ac11a0eb577b81b3bb5c95cc8a6e")
|
val hash: MD5Hash = MD5Hash("a3a6ac11a0eb577b81b3bb5c95cc8a6e")
|
||||||
val base64 = "o6asEaDrV3uBs7tclcyKbg=="
|
val base64: String = "o6asEaDrV3uBs7tclcyKbg=="
|
||||||
}
|
}
|
||||||
object Leaf {
|
object Leaf {
|
||||||
val hash = MD5Hash("208386a650bdec61cfcd7bd8dcb6b542")
|
val hash: MD5Hash = MD5Hash("208386a650bdec61cfcd7bd8dcb6b542")
|
||||||
val base64 = "IIOGplC97GHPzXvY3La1Qg=="
|
val base64: String = "IIOGplC97GHPzXvY3La1Qg=="
|
||||||
}
|
}
|
||||||
object BigFile {
|
object BigFile {
|
||||||
val hash = MD5Hash("b1ab1f7680138e6db7309200584e35d8")
|
val hash: MD5Hash = MD5Hash("b1ab1f7680138e6db7309200584e35d8")
|
||||||
object Part1 {
|
object Part1 {
|
||||||
val offset = 0
|
val offset: Int = 0
|
||||||
val size = 1048576
|
val size: Int = 1048576
|
||||||
val hash = MD5Hash("39d4a9c78b9cfddf6d241a201a4ab726")
|
val hash: MD5Hash = MD5Hash("39d4a9c78b9cfddf6d241a201a4ab726")
|
||||||
}
|
}
|
||||||
object Part2 {
|
object Part2 {
|
||||||
val offset = 1048576
|
val offset: Int = 1048576
|
||||||
val size = 1048576
|
val size: Int = 1048576
|
||||||
val hash = MD5Hash("af5876f3a3bc6e66f4ae96bb93d8dae0")
|
val hash: MD5Hash = MD5Hash("af5876f3a3bc6e66f4ae96bb93d8dae0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,22 +4,27 @@ import java.io.{File, IOException, PrintWriter}
|
||||||
import java.nio.file.attribute.BasicFileAttributes
|
import java.nio.file.attribute.BasicFileAttributes
|
||||||
import java.nio.file.{FileVisitResult, Files, Path, SimpleFileVisitor}
|
import java.nio.file.{FileVisitResult, Files, Path, SimpleFileVisitor}
|
||||||
|
|
||||||
|
import net.kemitix.thorp.domain.NonUnit.~*
|
||||||
|
|
||||||
import scala.util.Try
|
import scala.util.Try
|
||||||
|
|
||||||
trait TemporaryFolder {
|
trait TemporaryFolder {
|
||||||
|
|
||||||
def withDirectory(testCode: Path => Any): Any = {
|
@SuppressWarnings(Array("org.wartremover.warts.TryPartial"))
|
||||||
|
def withDirectory(testCode: Path => Any): Unit = {
|
||||||
val dir: Path = Files.createTempDirectory("thorp-temp")
|
val dir: Path = Files.createTempDirectory("thorp-temp")
|
||||||
val t = Try(testCode(dir))
|
val t = Try(testCode(dir))
|
||||||
remove(dir)
|
remove(dir)
|
||||||
t.get
|
~*(t.get)
|
||||||
}
|
}
|
||||||
|
|
||||||
def remove(root: Path): Unit = {
|
def remove(root: Path): Unit = {
|
||||||
|
~*(
|
||||||
Files.walkFileTree(
|
Files.walkFileTree(
|
||||||
root,
|
root,
|
||||||
new SimpleFileVisitor[Path] {
|
new SimpleFileVisitor[Path] {
|
||||||
override def visitFile(file: Path,
|
override def visitFile(
|
||||||
|
file: Path,
|
||||||
attrs: BasicFileAttributes): FileVisitResult = {
|
attrs: BasicFileAttributes): FileVisitResult = {
|
||||||
Files.delete(file)
|
Files.delete(file)
|
||||||
FileVisitResult.CONTINUE
|
FileVisitResult.CONTINUE
|
||||||
|
@ -30,16 +35,11 @@ trait TemporaryFolder {
|
||||||
FileVisitResult.CONTINUE
|
FileVisitResult.CONTINUE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
def createFile(path: Path, name: String, content: String*): File = {
|
def createFile(directory: Path, name: String, contents: String*): File = {
|
||||||
writeFile(path, name, content: _*)
|
val _ = directory.toFile.mkdirs
|
||||||
path.resolve(name).toFile
|
|
||||||
}
|
|
||||||
|
|
||||||
def writeFile(directory: Path, name: String, contents: String*): File = {
|
|
||||||
directory.toFile.mkdirs
|
|
||||||
val file = directory.resolve(name).toFile
|
val file = directory.resolve(name).toFile
|
||||||
val writer = new PrintWriter(file, "UTF-8")
|
val writer = new PrintWriter(file, "UTF-8")
|
||||||
contents.foreach(writer.println)
|
contents.foreach(writer.println)
|
||||||
|
@ -47,4 +47,7 @@ trait TemporaryFolder {
|
||||||
file
|
file
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def writeFile(directory: Path, name: String, contents: String*): Unit =
|
||||||
|
~*(createFile(directory, name, contents: _*))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ object FileSystem {
|
||||||
|
|
||||||
trait Service {
|
trait Service {
|
||||||
def fileExists(file: File): ZIO[FileSystem, Throwable, Boolean]
|
def fileExists(file: File): ZIO[FileSystem, Throwable, Boolean]
|
||||||
def openManagedFileInputStream(file: File, offset: Long = 0L)
|
def openManagedFileInputStream(file: File, offset: Long)
|
||||||
: RIO[FileSystem, ZManaged[Any, Throwable, FileInputStream]]
|
: RIO[FileSystem, ZManaged[Any, Throwable, FileInputStream]]
|
||||||
def fileLines(file: File): RIO[FileSystem, Seq[String]]
|
def fileLines(file: File): RIO[FileSystem, Seq[String]]
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ object FileSystem {
|
||||||
override val filesystem: Service = new Service {
|
override val filesystem: Service = new Service {
|
||||||
override def fileExists(
|
override def fileExists(
|
||||||
file: File
|
file: File
|
||||||
): ZIO[FileSystem, Throwable, Boolean] = ZIO(file.exists)
|
): RIO[FileSystem, Boolean] = ZIO(file.exists)
|
||||||
|
|
||||||
override def openManagedFileInputStream(file: File, offset: Long)
|
override def openManagedFileInputStream(file: File, offset: Long)
|
||||||
: RIO[FileSystem, ZManaged[Any, Throwable, FileInputStream]] = {
|
: RIO[FileSystem, ZManaged[Any, Throwable, FileInputStream]] = {
|
||||||
|
@ -32,7 +32,7 @@ object FileSystem {
|
||||||
def acquire =
|
def acquire =
|
||||||
Task {
|
Task {
|
||||||
val stream = new FileInputStream(file)
|
val stream = new FileInputStream(file)
|
||||||
stream skip offset
|
val _ = stream.skip(offset)
|
||||||
stream
|
stream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ object FileSystem {
|
||||||
|
|
||||||
override val filesystem: Service = new Service {
|
override val filesystem: Service = new Service {
|
||||||
|
|
||||||
override def fileExists(file: File): ZIO[FileSystem, Throwable, Boolean] =
|
override def fileExists(file: File): RIO[FileSystem, Boolean] =
|
||||||
fileExistsResultMap.map(m => m.keys.exists(_ equals file.toPath))
|
fileExistsResultMap.map(m => m.keys.exists(_ equals file.toPath))
|
||||||
|
|
||||||
override def openManagedFileInputStream(file: File, offset: Long)
|
override def openManagedFileInputStream(file: File, offset: Long)
|
||||||
|
@ -71,13 +71,17 @@ object FileSystem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final def exists(file: File): ZIO[FileSystem, Throwable, Boolean] =
|
final def exists(file: File): RIO[FileSystem, Boolean] =
|
||||||
ZIO.accessM(_.filesystem fileExists file)
|
ZIO.accessM(_.filesystem fileExists file)
|
||||||
|
|
||||||
final def open(file: File, offset: Long = 0)
|
final def openAtOffset(file: File, offset: Long)
|
||||||
: RIO[FileSystem, ZManaged[FileSystem, Throwable, FileInputStream]] =
|
: RIO[FileSystem, ZManaged[FileSystem, Throwable, FileInputStream]] =
|
||||||
ZIO.accessM(_.filesystem openManagedFileInputStream (file, offset))
|
ZIO.accessM(_.filesystem openManagedFileInputStream (file, offset))
|
||||||
|
|
||||||
|
final def open(file: File)
|
||||||
|
: RIO[FileSystem, ZManaged[FileSystem, Throwable, FileInputStream]] =
|
||||||
|
ZIO.accessM(_.filesystem openManagedFileInputStream (file, 0L))
|
||||||
|
|
||||||
final def lines(file: File): RIO[FileSystem, Seq[String]] =
|
final def lines(file: File): RIO[FileSystem, Seq[String]] =
|
||||||
ZIO.accessM(_.filesystem fileLines (file))
|
ZIO.accessM(_.filesystem fileLines (file))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.3.2")
|
addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.3.2")
|
||||||
addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.2.6")
|
addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.2.6")
|
||||||
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.9.0")
|
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.9.0")
|
||||||
|
addSbtPlugin("org.wartremover" % "sbt-wartremover" % "2.4.2")
|
||||||
|
|
|
@ -18,7 +18,7 @@ object AmazonS3 {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case class ClientImpl(amazonS3: AmazonS3Client) extends Client {
|
final case class ClientImpl(amazonS3: AmazonS3Client) extends Client {
|
||||||
|
|
||||||
def shutdown(): UIO[Unit] =
|
def shutdown(): UIO[Unit] =
|
||||||
UIO {
|
UIO {
|
||||||
|
|
|
@ -8,7 +8,15 @@ import net.kemitix.thorp.storage.aws.AmazonUpload.{
|
||||||
}
|
}
|
||||||
import zio.{Task, UIO}
|
import zio.{Task, UIO}
|
||||||
|
|
||||||
case class AmazonTransferManager(transferManager: TransferManager) {
|
trait AmazonTransferManager {
|
||||||
|
def shutdownNow(now: Boolean): UIO[Unit]
|
||||||
|
def upload: PutObjectRequest => Task[InProgress]
|
||||||
|
}
|
||||||
|
|
||||||
|
object AmazonTransferManager {
|
||||||
|
|
||||||
|
final case class Wrapper(transferManager: TransferManager)
|
||||||
|
extends AmazonTransferManager {
|
||||||
def shutdownNow(now: Boolean): UIO[Unit] =
|
def shutdownNow(now: Boolean): UIO[Unit] =
|
||||||
UIO(transferManager.shutdownNow(now))
|
UIO(transferManager.shutdownNow(now))
|
||||||
|
|
||||||
|
@ -18,3 +26,5 @@ case class AmazonTransferManager(transferManager: TransferManager) {
|
||||||
.map(CompletableUpload)
|
.map(CompletableUpload)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ object AmazonUpload {
|
||||||
def waitForUploadResult: UploadResult
|
def waitForUploadResult: UploadResult
|
||||||
}
|
}
|
||||||
|
|
||||||
case class CompletableUpload(upload: Upload) extends InProgress {
|
final case class CompletableUpload(upload: Upload) extends InProgress {
|
||||||
override def waitForUploadResult: UploadResult =
|
override def waitForUploadResult: UploadResult =
|
||||||
upload.waitForUploadResult()
|
upload.waitForUploadResult()
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import net.kemitix.thorp.domain.StorageQueueEvent.{
|
||||||
ErrorQueueEvent
|
ErrorQueueEvent
|
||||||
}
|
}
|
||||||
import net.kemitix.thorp.domain.{Bucket, RemoteKey, StorageQueueEvent}
|
import net.kemitix.thorp.domain.{Bucket, RemoteKey, StorageQueueEvent}
|
||||||
import zio.{Task, UIO}
|
import zio.{Task, UIO, ZIO}
|
||||||
|
|
||||||
trait Deleter {
|
trait Deleter {
|
||||||
|
|
||||||
|
@ -16,16 +16,15 @@ trait Deleter {
|
||||||
remoteKey: RemoteKey
|
remoteKey: RemoteKey
|
||||||
): UIO[StorageQueueEvent] =
|
): UIO[StorageQueueEvent] =
|
||||||
deleteObject(amazonS3)(bucket, remoteKey)
|
deleteObject(amazonS3)(bucket, remoteKey)
|
||||||
.map(_ => DeleteQueueEvent(remoteKey))
|
|
||||||
.catchAll(e =>
|
.catchAll(e =>
|
||||||
UIO(ErrorQueueEvent(Action.Delete(remoteKey.key), remoteKey, e)))
|
UIO(ErrorQueueEvent(Action.Delete(remoteKey.key), remoteKey, e)))
|
||||||
|
|
||||||
private def deleteObject(amazonS3: AmazonS3.Client)(
|
private def deleteObject(amazonS3: AmazonS3.Client)(
|
||||||
bucket: Bucket,
|
bucket: Bucket,
|
||||||
remoteKey: RemoteKey
|
remoteKey: RemoteKey
|
||||||
): Task[Unit] =
|
): Task[StorageQueueEvent] =
|
||||||
amazonS3.deleteObject(new DeleteObjectRequest(bucket.name, remoteKey.key))
|
(amazonS3.deleteObject(new DeleteObjectRequest(bucket.name, remoteKey.key))
|
||||||
|
*> ZIO(DeleteQueueEvent(remoteKey)))
|
||||||
}
|
}
|
||||||
|
|
||||||
object Deleter extends Deleter
|
object Deleter extends Deleter
|
||||||
|
|
|
@ -48,7 +48,7 @@ trait Lister {
|
||||||
|
|
||||||
fetch(request)
|
fetch(request)
|
||||||
.map(summaries => {
|
.map(summaries => {
|
||||||
RemoteObjects(byHash(summaries), byKey(summaries))
|
RemoteObjects.create(byHash(summaries), byKey(summaries))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,11 @@ object S3ClientException {
|
||||||
override def getMessage: String =
|
override def getMessage: String =
|
||||||
"The hash of the object to be overwritten did not match the the expected value"
|
"The hash of the object to be overwritten did not match the the expected value"
|
||||||
}
|
}
|
||||||
case class CopyError(error: Throwable) extends S3ClientException {
|
final case class CopyError(error: Throwable) extends S3ClientException {
|
||||||
override def getMessage: String =
|
override def getMessage: String =
|
||||||
"The hash of the object to be overwritten did not match the the expected value"
|
"The hash of the object to be overwritten did not match the the expected value"
|
||||||
}
|
}
|
||||||
case class S3Exception(message: String) extends S3ClientException {
|
final case class S3Exception(message: String) extends S3ClientException {
|
||||||
override def getMessage: String = message
|
override def getMessage: String = message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,17 @@
|
||||||
package net.kemitix.thorp.storage.aws
|
package net.kemitix.thorp.storage.aws
|
||||||
|
|
||||||
import com.amazonaws.services.s3.model.S3ObjectSummary
|
import com.amazonaws.services.s3.model.S3ObjectSummary
|
||||||
import net.kemitix.thorp.domain.{KeyModified, LastModified, MD5Hash, RemoteKey}
|
import net.kemitix.thorp.domain.{MD5Hash, RemoteKey}
|
||||||
|
|
||||||
object S3ObjectsByHash {
|
object S3ObjectsByHash {
|
||||||
|
|
||||||
def byHash(
|
def byHash(
|
||||||
os: Stream[S3ObjectSummary]
|
os: Stream[S3ObjectSummary]
|
||||||
): Map[MD5Hash, Set[KeyModified]] = {
|
): Map[MD5Hash, Set[RemoteKey]] = {
|
||||||
val mD5HashToS3Objects: Map[MD5Hash, Stream[S3ObjectSummary]] =
|
val mD5HashToS3Objects: Map[MD5Hash, Stream[S3ObjectSummary]] =
|
||||||
os.groupBy(o => MD5Hash(o.getETag.filter(_ != '"')))
|
os.groupBy(o => MD5Hash(o.getETag.filter(_ != '"')))
|
||||||
mD5HashToS3Objects.mapValues { os =>
|
mD5HashToS3Objects.mapValues { os =>
|
||||||
os.map { o =>
|
os.map(_.getKey).map(RemoteKey(_)).toSet
|
||||||
KeyModified(
|
|
||||||
RemoteKey(o.getKey),
|
|
||||||
LastModified(o.getLastModified.toInstant)
|
|
||||||
)
|
|
||||||
}.toSet
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
package net.kemitix.thorp.storage.aws
|
package net.kemitix.thorp.storage.aws
|
||||||
|
|
||||||
import com.amazonaws.services.s3.model.S3ObjectSummary
|
import com.amazonaws.services.s3.model.S3ObjectSummary
|
||||||
import net.kemitix.thorp.domain.{HashModified, LastModified, MD5Hash, RemoteKey}
|
import net.kemitix.thorp.domain.{MD5Hash, RemoteKey}
|
||||||
|
|
||||||
object S3ObjectsByKey {
|
object S3ObjectsByKey {
|
||||||
|
|
||||||
def byKey(os: Stream[S3ObjectSummary]): Map[RemoteKey, HashModified] =
|
def byKey(os: Stream[S3ObjectSummary]): Map[RemoteKey, MD5Hash] =
|
||||||
os.map { o =>
|
os.map { o =>
|
||||||
{
|
{
|
||||||
val remoteKey = RemoteKey(o.getKey)
|
val remoteKey = RemoteKey(o.getKey)
|
||||||
val hash = MD5Hash(o.getETag)
|
val hash = MD5Hash(o.getETag)
|
||||||
val lastModified = LastModified(o.getLastModified.toInstant)
|
(remoteKey, hash)
|
||||||
(remoteKey, HashModified(hash, lastModified))
|
|
||||||
}
|
}
|
||||||
}.toMap
|
}.toMap
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,8 @@ object S3Storage {
|
||||||
private val client: AmazonS3.Client =
|
private val client: AmazonS3.Client =
|
||||||
AmazonS3.ClientImpl(AmazonS3ClientBuilder.defaultClient)
|
AmazonS3.ClientImpl(AmazonS3ClientBuilder.defaultClient)
|
||||||
private val transferManager: AmazonTransferManager =
|
private val transferManager: AmazonTransferManager =
|
||||||
AmazonTransferManager(TransferManagerBuilder.defaultTransferManager)
|
AmazonTransferManager.Wrapper(
|
||||||
|
TransferManagerBuilder.defaultTransferManager)
|
||||||
|
|
||||||
override def listObjects(bucket: Bucket,
|
override def listObjects(bucket: Bucket,
|
||||||
prefix: RemoteKey): RIO[Console, RemoteObjects] =
|
prefix: RemoteKey): RIO[Console, RemoteObjects] =
|
||||||
|
@ -42,7 +43,7 @@ object S3Storage {
|
||||||
Deleter.delete(client)(bucket, remoteKey)
|
Deleter.delete(client)(bucket, remoteKey)
|
||||||
|
|
||||||
override def shutdown: UIO[StorageQueueEvent] = {
|
override def shutdown: UIO[StorageQueueEvent] = {
|
||||||
transferManager.shutdownNow(true)
|
transferManager.shutdownNow(true) *>
|
||||||
client.shutdown().map(_ => ShutdownQueueEvent())
|
client.shutdown().map(_ => ShutdownQueueEvent())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package net.kemitix.thorp.storage.aws
|
package net.kemitix.thorp.storage.aws
|
||||||
|
|
||||||
import com.amazonaws.event.{ProgressEvent, ProgressEventType, ProgressListener}
|
import com.amazonaws.event.ProgressEventType.RESPONSE_BYTE_TRANSFER_EVENT
|
||||||
|
import com.amazonaws.event.{ProgressEvent, ProgressListener}
|
||||||
import com.amazonaws.services.s3.model.{ObjectMetadata, PutObjectRequest}
|
import com.amazonaws.services.s3.model.{ObjectMetadata, PutObjectRequest}
|
||||||
import net.kemitix.thorp.config.Config
|
import net.kemitix.thorp.config.Config
|
||||||
|
import net.kemitix.thorp.domain.Implicits._
|
||||||
import net.kemitix.thorp.domain.StorageQueueEvent.{
|
import net.kemitix.thorp.domain.StorageQueueEvent.{
|
||||||
Action,
|
Action,
|
||||||
ErrorQueueEvent,
|
ErrorQueueEvent,
|
||||||
|
@ -14,22 +16,18 @@ import net.kemitix.thorp.domain.UploadEvent.{
|
||||||
TransferEvent
|
TransferEvent
|
||||||
}
|
}
|
||||||
import net.kemitix.thorp.domain.{StorageQueueEvent, _}
|
import net.kemitix.thorp.domain.{StorageQueueEvent, _}
|
||||||
|
import net.kemitix.thorp.storage.aws.Uploader.Request
|
||||||
import zio.{UIO, ZIO}
|
import zio.{UIO, ZIO}
|
||||||
|
|
||||||
trait Uploader {
|
trait Uploader {
|
||||||
|
|
||||||
case class Request(
|
|
||||||
localFile: LocalFile,
|
|
||||||
bucket: Bucket,
|
|
||||||
uploadEventListener: UploadEventListener.Settings
|
|
||||||
)
|
|
||||||
|
|
||||||
def upload(transferManager: => AmazonTransferManager)(
|
def upload(transferManager: => AmazonTransferManager)(
|
||||||
request: Request): ZIO[Config, Nothing, StorageQueueEvent] =
|
request: Request): ZIO[Config, Nothing, StorageQueueEvent] =
|
||||||
transfer(transferManager)(request)
|
transfer(transferManager)(request)
|
||||||
.catchAll(handleError(request.localFile.remoteKey))
|
.catchAll(handleError(request.localFile.remoteKey))
|
||||||
|
|
||||||
private def handleError(remoteKey: RemoteKey)(e: Throwable) =
|
private def handleError(remoteKey: RemoteKey)(
|
||||||
|
e: Throwable): UIO[StorageQueueEvent] =
|
||||||
UIO(ErrorQueueEvent(Action.Upload(remoteKey.key), remoteKey, e))
|
UIO(ErrorQueueEvent(Action.Upload(remoteKey.key), remoteKey, e))
|
||||||
|
|
||||||
private def transfer(transferManager: => AmazonTransferManager)(
|
private def transfer(transferManager: => AmazonTransferManager)(
|
||||||
|
@ -77,15 +75,15 @@ trait Uploader {
|
||||||
listenerSettings =>
|
listenerSettings =>
|
||||||
new ProgressListener {
|
new ProgressListener {
|
||||||
override def progressChanged(progressEvent: ProgressEvent): Unit =
|
override def progressChanged(progressEvent: ProgressEvent): Unit =
|
||||||
UploadEventListener(listenerSettings)(eventHandler(progressEvent))
|
UploadEventListener.listener(listenerSettings)(
|
||||||
|
eventHandler(progressEvent))
|
||||||
|
|
||||||
private def eventHandler: ProgressEvent => UploadEvent =
|
private def eventHandler: ProgressEvent => UploadEvent =
|
||||||
progressEvent => {
|
progressEvent => {
|
||||||
def isTransfer: ProgressEvent => Boolean =
|
def isTransfer: ProgressEvent => Boolean =
|
||||||
_.getEventType.isTransferEvent
|
_.getEventType.isTransferEvent
|
||||||
def isByteTransfer: ProgressEvent => Boolean =
|
def isByteTransfer: ProgressEvent => Boolean =
|
||||||
_.getEventType.equals(
|
(_.getEventType === RESPONSE_BYTE_TRANSFER_EVENT)
|
||||||
ProgressEventType.RESPONSE_BYTE_TRANSFER_EVENT)
|
|
||||||
progressEvent match {
|
progressEvent match {
|
||||||
case e: ProgressEvent if isTransfer(e) =>
|
case e: ProgressEvent if isTransfer(e) =>
|
||||||
TransferEvent(e.getEventType.name)
|
TransferEvent(e.getEventType.name)
|
||||||
|
@ -101,4 +99,10 @@ trait Uploader {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Uploader extends Uploader
|
object Uploader extends Uploader {
|
||||||
|
final case class Request(
|
||||||
|
localFile: LocalFile,
|
||||||
|
bucket: Bucket,
|
||||||
|
uploadEventListener: UploadEventListener.Settings
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -10,8 +10,11 @@ import zio.{RIO, UIO, ZIO}
|
||||||
|
|
||||||
trait AmazonS3ClientTestFixture extends MockFactory {
|
trait AmazonS3ClientTestFixture extends MockFactory {
|
||||||
|
|
||||||
val fixture: Fixture =
|
@SuppressWarnings(Array("org.wartremover.warts.PublicInference"))
|
||||||
Fixture(stub[AmazonS3.Client], stub[AmazonTransferManager])
|
private val manager = stub[AmazonTransferManager]
|
||||||
|
@SuppressWarnings(Array("org.wartremover.warts.PublicInference"))
|
||||||
|
private val client = stub[AmazonS3.Client]
|
||||||
|
val fixture: Fixture = Fixture(client, manager)
|
||||||
|
|
||||||
case class Fixture(
|
case class Fixture(
|
||||||
amazonS3Client: AmazonS3.Client,
|
amazonS3Client: AmazonS3.Client,
|
||||||
|
@ -53,7 +56,7 @@ trait AmazonS3ClientTestFixture extends MockFactory {
|
||||||
Deleter.delete(client)(bucket, remoteKey)
|
Deleter.delete(client)(bucket, remoteKey)
|
||||||
|
|
||||||
override def shutdown: UIO[StorageQueueEvent] = {
|
override def shutdown: UIO[StorageQueueEvent] = {
|
||||||
transferManager.shutdownNow(true)
|
transferManager.shutdownNow(true) *>
|
||||||
client.shutdown().map(_ => ShutdownQueueEvent())
|
client.shutdown().map(_ => ShutdownQueueEvent())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package net.kemitix.thorp.storage.aws
|
||||||
|
|
||||||
import com.amazonaws.services.s3.model.{AmazonS3Exception, CopyObjectResult}
|
import com.amazonaws.services.s3.model.{AmazonS3Exception, CopyObjectResult}
|
||||||
import net.kemitix.thorp.console.Console
|
import net.kemitix.thorp.console.Console
|
||||||
|
import net.kemitix.thorp.domain.NonUnit.~*
|
||||||
import net.kemitix.thorp.domain.StorageQueueEvent.{Action, ErrorQueueEvent}
|
import net.kemitix.thorp.domain.StorageQueueEvent.{Action, ErrorQueueEvent}
|
||||||
import net.kemitix.thorp.domain._
|
import net.kemitix.thorp.domain._
|
||||||
import net.kemitix.thorp.storage.aws.S3ClientException.{CopyError, HashError}
|
import net.kemitix.thorp.storage.aws.S3ClientException.{CopyError, HashError}
|
||||||
|
@ -24,34 +25,36 @@ class CopierTest extends FreeSpec {
|
||||||
val event = StorageQueueEvent.CopyQueueEvent(sourceKey, targetKey)
|
val event = StorageQueueEvent.CopyQueueEvent(sourceKey, targetKey)
|
||||||
val expected = Right(event)
|
val expected = Right(event)
|
||||||
new AmazonS3ClientTestFixture {
|
new AmazonS3ClientTestFixture {
|
||||||
|
~*(
|
||||||
(fixture.amazonS3Client.copyObject _)
|
(fixture.amazonS3Client.copyObject _)
|
||||||
.when()
|
.when()
|
||||||
.returns(_ => Task.succeed(Some(new CopyObjectResult)))
|
.returns(_ => Task.succeed(Some(new CopyObjectResult))))
|
||||||
private val result =
|
private val result =
|
||||||
invoke(bucket, sourceKey, hash, targetKey, fixture.amazonS3Client)
|
invoke(bucket, sourceKey, hash, targetKey, fixture.amazonS3Client)
|
||||||
assertResult(expected)(result)
|
~*(assertResult(expected)(result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"when source hash does not match" - {
|
"when source hash does not match" - {
|
||||||
"skip the file with an error" in {
|
"skip the file with an error" in {
|
||||||
new AmazonS3ClientTestFixture {
|
new AmazonS3ClientTestFixture {
|
||||||
|
~*(
|
||||||
(fixture.amazonS3Client.copyObject _)
|
(fixture.amazonS3Client.copyObject _)
|
||||||
.when()
|
.when()
|
||||||
.returns(_ => Task.succeed(None))
|
.returns(_ => Task.succeed(None)))
|
||||||
private val result =
|
private val result =
|
||||||
invoke(bucket, sourceKey, hash, targetKey, fixture.amazonS3Client)
|
invoke(bucket, sourceKey, hash, targetKey, fixture.amazonS3Client)
|
||||||
result match {
|
~*(result match {
|
||||||
case Right(
|
case Right(
|
||||||
ErrorQueueEvent(Action.Copy("sourceKey => targetKey"),
|
ErrorQueueEvent(Action.Copy("sourceKey => targetKey"),
|
||||||
RemoteKey("targetKey"),
|
RemoteKey("targetKey"),
|
||||||
e)) =>
|
e)) =>
|
||||||
e match {
|
e match {
|
||||||
case HashError => assert(true)
|
case HashError => assert(true)
|
||||||
case _ => fail("Not a HashError: " + e)
|
case _ => fail(s"Not a HashError: ${e.getMessage}")
|
||||||
}
|
|
||||||
case e => fail("Not an ErrorQueueEvent: " + e)
|
|
||||||
}
|
}
|
||||||
|
case e => fail(s"Not an ErrorQueueEvent: $e")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,12 +62,12 @@ class CopierTest extends FreeSpec {
|
||||||
"skip the file with an error" in {
|
"skip the file with an error" in {
|
||||||
new AmazonS3ClientTestFixture {
|
new AmazonS3ClientTestFixture {
|
||||||
private val expectedMessage = "The specified key does not exist"
|
private val expectedMessage = "The specified key does not exist"
|
||||||
(fixture.amazonS3Client.copyObject _)
|
~*((fixture.amazonS3Client.copyObject _)
|
||||||
.when()
|
.when()
|
||||||
.returns(_ => Task.fail(new AmazonS3Exception(expectedMessage)))
|
.returns(_ => Task.fail(new AmazonS3Exception(expectedMessage))))
|
||||||
private val result =
|
private val result =
|
||||||
invoke(bucket, sourceKey, hash, targetKey, fixture.amazonS3Client)
|
invoke(bucket, sourceKey, hash, targetKey, fixture.amazonS3Client)
|
||||||
result match {
|
~*(result match {
|
||||||
case Right(
|
case Right(
|
||||||
ErrorQueueEvent(Action.Copy("sourceKey => targetKey"),
|
ErrorQueueEvent(Action.Copy("sourceKey => targetKey"),
|
||||||
RemoteKey("targetKey"),
|
RemoteKey("targetKey"),
|
||||||
|
@ -72,10 +75,10 @@ class CopierTest extends FreeSpec {
|
||||||
e match {
|
e match {
|
||||||
case CopyError(cause) =>
|
case CopyError(cause) =>
|
||||||
assert(cause.getMessage.startsWith(expectedMessage))
|
assert(cause.getMessage.startsWith(expectedMessage))
|
||||||
case _ => fail("Not a CopyError: " + e)
|
case _ => fail(s"Not a CopyError: ${e.getMessage}")
|
||||||
}
|
|
||||||
case e => fail("Not an ErrorQueueEvent: " + e)
|
|
||||||
}
|
}
|
||||||
|
case e => fail(s"Not an ErrorQueueEvent: ${e}")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import net.kemitix.thorp.domain.{Bucket, RemoteKey}
|
||||||
import org.scalatest.FreeSpec
|
import org.scalatest.FreeSpec
|
||||||
import zio.internal.PlatformLive
|
import zio.internal.PlatformLive
|
||||||
import zio.{Runtime, Task, UIO}
|
import zio.{Runtime, Task, UIO}
|
||||||
|
import net.kemitix.thorp.domain.NonUnit.~*
|
||||||
|
|
||||||
class DeleterTest extends FreeSpec {
|
class DeleterTest extends FreeSpec {
|
||||||
|
|
||||||
|
@ -23,11 +24,12 @@ class DeleterTest extends FreeSpec {
|
||||||
"when no errors" in {
|
"when no errors" in {
|
||||||
val expected = Right(DeleteQueueEvent(remoteKey))
|
val expected = Right(DeleteQueueEvent(remoteKey))
|
||||||
new AmazonS3ClientTestFixture {
|
new AmazonS3ClientTestFixture {
|
||||||
|
~*(
|
||||||
(fixture.amazonS3Client.deleteObject _)
|
(fixture.amazonS3Client.deleteObject _)
|
||||||
.when()
|
.when()
|
||||||
.returns(_ => UIO.succeed(()))
|
.returns(_ => UIO.succeed(())))
|
||||||
private val result = invoke(fixture.amazonS3Client)(bucket, remoteKey)
|
private val result = invoke(fixture.amazonS3Client)(bucket, remoteKey)
|
||||||
assertResult(expected)(result)
|
~*(assertResult(expected)(result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"when Amazon Service Exception" in {
|
"when Amazon Service Exception" in {
|
||||||
|
@ -36,11 +38,12 @@ class DeleterTest extends FreeSpec {
|
||||||
Right(
|
Right(
|
||||||
ErrorQueueEvent(Action.Delete(remoteKey.key), remoteKey, exception))
|
ErrorQueueEvent(Action.Delete(remoteKey.key), remoteKey, exception))
|
||||||
new AmazonS3ClientTestFixture {
|
new AmazonS3ClientTestFixture {
|
||||||
|
~*(
|
||||||
(fixture.amazonS3Client.deleteObject _)
|
(fixture.amazonS3Client.deleteObject _)
|
||||||
.when()
|
.when()
|
||||||
.returns(_ => Task.fail(exception))
|
.returns(_ => Task.fail(exception)))
|
||||||
private val result = invoke(fixture.amazonS3Client)(bucket, remoteKey)
|
private val result = invoke(fixture.amazonS3Client)(bucket, remoteKey)
|
||||||
assertResult(expected)(result)
|
~*(assertResult(expected)(result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"when Amazon SDK Client Exception" in {
|
"when Amazon SDK Client Exception" in {
|
||||||
|
@ -49,11 +52,12 @@ class DeleterTest extends FreeSpec {
|
||||||
Right(
|
Right(
|
||||||
ErrorQueueEvent(Action.Delete(remoteKey.key), remoteKey, exception))
|
ErrorQueueEvent(Action.Delete(remoteKey.key), remoteKey, exception))
|
||||||
new AmazonS3ClientTestFixture {
|
new AmazonS3ClientTestFixture {
|
||||||
|
~*(
|
||||||
(fixture.amazonS3Client.deleteObject _)
|
(fixture.amazonS3Client.deleteObject _)
|
||||||
.when()
|
.when()
|
||||||
.returns(_ => Task.fail(exception))
|
.returns(_ => Task.fail(exception)))
|
||||||
private val result = invoke(fixture.amazonS3Client)(bucket, remoteKey)
|
private val result = invoke(fixture.amazonS3Client)(bucket, remoteKey)
|
||||||
assertResult(expected)(result)
|
~*(assertResult(expected)(result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
def invoke(amazonS3Client: AmazonS3.Client)(bucket: Bucket,
|
def invoke(amazonS3Client: AmazonS3.Client)(bucket: Bucket,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import com.amazonaws.services.s3.model.{
|
||||||
S3ObjectSummary
|
S3ObjectSummary
|
||||||
}
|
}
|
||||||
import net.kemitix.thorp.console._
|
import net.kemitix.thorp.console._
|
||||||
|
import net.kemitix.thorp.domain.NonUnit.~*
|
||||||
import net.kemitix.thorp.domain._
|
import net.kemitix.thorp.domain._
|
||||||
import org.scalatest.FreeSpec
|
import org.scalatest.FreeSpec
|
||||||
import zio.internal.PlatformLive
|
import zio.internal.PlatformLive
|
||||||
|
@ -24,58 +25,50 @@ class ListerTest extends FreeSpec {
|
||||||
"when no errors" - {
|
"when no errors" - {
|
||||||
"when single fetch required" in {
|
"when single fetch required" in {
|
||||||
val nowDate = new Date
|
val nowDate = new Date
|
||||||
val nowInstant = nowDate.toInstant
|
|
||||||
val key = "key"
|
val key = "key"
|
||||||
val etag = "etag"
|
val etag = "etag"
|
||||||
val expectedHashMap = Map(
|
val expectedHashMap = Map(MD5Hash(etag) -> Set(RemoteKey(key)))
|
||||||
MD5Hash(etag) -> Set(
|
val expectedKeyMap = Map(RemoteKey(key) -> MD5Hash(etag))
|
||||||
KeyModified(RemoteKey(key), LastModified(nowInstant))))
|
|
||||||
val expectedKeyMap = Map(
|
|
||||||
RemoteKey(key) -> HashModified(MD5Hash(etag),
|
|
||||||
LastModified(nowInstant))
|
|
||||||
)
|
|
||||||
val expected = Right(RemoteObjects(expectedHashMap, expectedKeyMap))
|
val expected = Right(RemoteObjects(expectedHashMap, expectedKeyMap))
|
||||||
new AmazonS3ClientTestFixture {
|
new AmazonS3ClientTestFixture {
|
||||||
|
~*(
|
||||||
(fixture.amazonS3Client.listObjectsV2 _)
|
(fixture.amazonS3Client.listObjectsV2 _)
|
||||||
.when()
|
.when()
|
||||||
.returns(_ => {
|
.returns(_ => {
|
||||||
UIO.succeed(objectResults(nowDate, key, etag, false))
|
UIO.succeed(objectResults(nowDate, key, etag, false))
|
||||||
})
|
}))
|
||||||
private val result = invoke(fixture.amazonS3Client)(bucket, prefix)
|
private val result = invoke(fixture.amazonS3Client)(bucket, prefix)
|
||||||
assertResult(expected)(result)
|
~*(assertResult(expected)(result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"when second fetch required" in {
|
"when second fetch required" in {
|
||||||
val nowDate = new Date
|
val nowDate = new Date
|
||||||
val nowInstant = nowDate.toInstant
|
|
||||||
val key1 = "key1"
|
val key1 = "key1"
|
||||||
val etag1 = "etag1"
|
val etag1 = "etag1"
|
||||||
val key2 = "key2"
|
val key2 = "key2"
|
||||||
val etag2 = "etag2"
|
val etag2 = "etag2"
|
||||||
val expectedHashMap = Map(
|
val expectedHashMap = Map(
|
||||||
MD5Hash(etag1) -> Set(
|
MD5Hash(etag1) -> Set(RemoteKey(key1)),
|
||||||
KeyModified(RemoteKey(key1), LastModified(nowInstant))),
|
MD5Hash(etag2) -> Set(RemoteKey(key2))
|
||||||
MD5Hash(etag2) -> Set(
|
|
||||||
KeyModified(RemoteKey(key2), LastModified(nowInstant)))
|
|
||||||
)
|
)
|
||||||
val expectedKeyMap = Map(
|
val expectedKeyMap = Map(
|
||||||
RemoteKey(key1) -> HashModified(MD5Hash(etag1),
|
RemoteKey(key1) -> MD5Hash(etag1),
|
||||||
LastModified(nowInstant)),
|
RemoteKey(key2) -> MD5Hash(etag2)
|
||||||
RemoteKey(key2) -> HashModified(MD5Hash(etag2),
|
|
||||||
LastModified(nowInstant))
|
|
||||||
)
|
)
|
||||||
val expected = Right(RemoteObjects(expectedHashMap, expectedKeyMap))
|
val expected = Right(RemoteObjects(expectedHashMap, expectedKeyMap))
|
||||||
new AmazonS3ClientTestFixture {
|
new AmazonS3ClientTestFixture {
|
||||||
|
~*(
|
||||||
(fixture.amazonS3Client.listObjectsV2 _)
|
(fixture.amazonS3Client.listObjectsV2 _)
|
||||||
.when()
|
.when()
|
||||||
.returns(_ => UIO(objectResults(nowDate, key1, etag1, true)))
|
.returns(_ => UIO(objectResults(nowDate, key1, etag1, true)))
|
||||||
.noMoreThanOnce()
|
.noMoreThanOnce())
|
||||||
|
~*(
|
||||||
(fixture.amazonS3Client.listObjectsV2 _)
|
(fixture.amazonS3Client.listObjectsV2 _)
|
||||||
.when()
|
.when()
|
||||||
.returns(_ => UIO(objectResults(nowDate, key2, etag2, false)))
|
.returns(_ => UIO(objectResults(nowDate, key2, etag2, false))))
|
||||||
private val result = invoke(fixture.amazonS3Client)(bucket, prefix)
|
private val result = invoke(fixture.amazonS3Client)(bucket, prefix)
|
||||||
assertResult(expected)(result)
|
~*(assertResult(expected)(result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +85,7 @@ class ListerTest extends FreeSpec {
|
||||||
etag: String,
|
etag: String,
|
||||||
truncated: Boolean) = {
|
truncated: Boolean) = {
|
||||||
val result = new ListObjectsV2Result
|
val result = new ListObjectsV2Result
|
||||||
result.getObjectSummaries.add(objectSummary(key, etag, nowDate))
|
~*(result.getObjectSummaries.add(objectSummary(key, etag, nowDate)))
|
||||||
result.setTruncated(truncated)
|
result.setTruncated(truncated)
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
@ -101,9 +94,10 @@ class ListerTest extends FreeSpec {
|
||||||
"when Amazon Service Exception" in {
|
"when Amazon Service Exception" in {
|
||||||
val exception = new AmazonS3Exception("message")
|
val exception = new AmazonS3Exception("message")
|
||||||
new AmazonS3ClientTestFixture {
|
new AmazonS3ClientTestFixture {
|
||||||
|
~*(
|
||||||
(fixture.amazonS3Client.listObjectsV2 _)
|
(fixture.amazonS3Client.listObjectsV2 _)
|
||||||
.when()
|
.when()
|
||||||
.returns(_ => Task.fail(exception))
|
.returns(_ => Task.fail(exception)))
|
||||||
private val result = invoke(fixture.amazonS3Client)(bucket, prefix)
|
private val result = invoke(fixture.amazonS3Client)(bucket, prefix)
|
||||||
assert(result.isLeft)
|
assert(result.isLeft)
|
||||||
}
|
}
|
||||||
|
@ -111,9 +105,10 @@ class ListerTest extends FreeSpec {
|
||||||
"when Amazon SDK Client Exception" in {
|
"when Amazon SDK Client Exception" in {
|
||||||
val exception = new SdkClientException("message")
|
val exception = new SdkClientException("message")
|
||||||
new AmazonS3ClientTestFixture {
|
new AmazonS3ClientTestFixture {
|
||||||
|
~*(
|
||||||
(fixture.amazonS3Client.listObjectsV2 _)
|
(fixture.amazonS3Client.listObjectsV2 _)
|
||||||
.when()
|
.when()
|
||||||
.returns(_ => Task.fail(exception))
|
.returns(_ => Task.fail(exception)))
|
||||||
private val result = invoke(fixture.amazonS3Client)(bucket, prefix)
|
private val result = invoke(fixture.amazonS3Client)(bucket, prefix)
|
||||||
assert(result.isLeft)
|
assert(result.isLeft)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
package net.kemitix.thorp.storage.aws
|
package net.kemitix.thorp.storage.aws
|
||||||
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.temporal.ChronoUnit
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
import com.amazonaws.services.s3.model.S3ObjectSummary
|
import com.amazonaws.services.s3.model.S3ObjectSummary
|
||||||
import net.kemitix.thorp.domain.{KeyModified, LastModified, MD5Hash, RemoteKey}
|
import net.kemitix.thorp.domain.{MD5Hash, RemoteKey}
|
||||||
import org.scalatest.FunSpec
|
import org.scalatest.FunSpec
|
||||||
|
|
||||||
class S3ObjectsByHashSuite extends FunSpec {
|
class S3ObjectsByHashSuite extends FunSpec {
|
||||||
|
@ -14,14 +10,12 @@ class S3ObjectsByHashSuite extends FunSpec {
|
||||||
val hash = MD5Hash("hash")
|
val hash = MD5Hash("hash")
|
||||||
val key1 = RemoteKey("key-1")
|
val key1 = RemoteKey("key-1")
|
||||||
val key2 = RemoteKey("key-2")
|
val key2 = RemoteKey("key-2")
|
||||||
val lastModified = LastModified(Instant.now.truncatedTo(ChronoUnit.MILLIS))
|
val o1 = s3object(hash, key1)
|
||||||
val o1 = s3object(hash, key1, lastModified)
|
val o2 = s3object(hash, key2)
|
||||||
val o2 = s3object(hash, key2, lastModified)
|
|
||||||
val os = Stream(o1, o2)
|
val os = Stream(o1, o2)
|
||||||
it("should group by the hash value") {
|
it("should group by the hash value") {
|
||||||
val expected: Map[MD5Hash, Set[KeyModified]] = Map(
|
val expected: Map[MD5Hash, Set[RemoteKey]] = Map(
|
||||||
hash -> Set(KeyModified(key1, lastModified),
|
hash -> Set(key1, key2)
|
||||||
KeyModified(key2, lastModified))
|
|
||||||
)
|
)
|
||||||
val result = S3ObjectsByHash.byHash(os)
|
val result = S3ObjectsByHash.byHash(os)
|
||||||
assertResult(expected)(result)
|
assertResult(expected)(result)
|
||||||
|
@ -29,12 +23,10 @@ class S3ObjectsByHashSuite extends FunSpec {
|
||||||
}
|
}
|
||||||
|
|
||||||
private def s3object(md5Hash: MD5Hash,
|
private def s3object(md5Hash: MD5Hash,
|
||||||
remoteKey: RemoteKey,
|
remoteKey: RemoteKey): S3ObjectSummary = {
|
||||||
lastModified: LastModified): S3ObjectSummary = {
|
|
||||||
val summary = new S3ObjectSummary()
|
val summary = new S3ObjectSummary()
|
||||||
summary.setETag(MD5Hash.hash(md5Hash))
|
summary.setETag(MD5Hash.hash(md5Hash))
|
||||||
summary.setKey(remoteKey.key)
|
summary.setKey(remoteKey.key)
|
||||||
summary.setLastModified(Date.from(lastModified.when))
|
|
||||||
summary
|
summary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package net.kemitix.thorp.storage.aws
|
package net.kemitix.thorp.storage.aws
|
||||||
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
import net.kemitix.thorp.config.Resource
|
import net.kemitix.thorp.config.Resource
|
||||||
import net.kemitix.thorp.core.{LocalFileValidator, S3MetaDataEnricher}
|
import net.kemitix.thorp.core.{LocalFileValidator, S3MetaDataEnricher}
|
||||||
import net.kemitix.thorp.domain.HashType.MD5
|
import net.kemitix.thorp.domain.HashType.MD5
|
||||||
|
@ -37,23 +35,20 @@ class StorageServiceSuite extends FunSpec with MockFactory {
|
||||||
sourcePath,
|
sourcePath,
|
||||||
sources,
|
sources,
|
||||||
prefix)
|
prefix)
|
||||||
lastModified = LastModified(Instant.now)
|
|
||||||
s3ObjectsData = RemoteObjects(
|
s3ObjectsData = RemoteObjects(
|
||||||
byHash = Map(
|
byHash = Map(
|
||||||
hash -> Set(KeyModified(key, lastModified),
|
hash -> Set(key, keyOtherKey.remoteKey),
|
||||||
KeyModified(keyOtherKey.remoteKey, lastModified)),
|
diffHash -> Set(keyDiffHash.remoteKey)
|
||||||
diffHash -> Set(KeyModified(keyDiffHash.remoteKey, lastModified))
|
|
||||||
),
|
),
|
||||||
byKey = Map(
|
byKey = Map(
|
||||||
key -> HashModified(hash, lastModified),
|
key -> hash,
|
||||||
keyOtherKey.remoteKey -> HashModified(hash, lastModified),
|
keyOtherKey.remoteKey -> hash,
|
||||||
keyDiffHash.remoteKey -> HashModified(diffHash, lastModified)
|
keyDiffHash.remoteKey -> diffHash
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} yield
|
} yield
|
||||||
(s3ObjectsData,
|
(s3ObjectsData,
|
||||||
localFile: LocalFile,
|
localFile: LocalFile,
|
||||||
lastModified,
|
|
||||||
keyOtherKey,
|
keyOtherKey,
|
||||||
keyDiffHash,
|
keyDiffHash,
|
||||||
diffHash,
|
diffHash,
|
||||||
|
@ -62,16 +57,14 @@ class StorageServiceSuite extends FunSpec with MockFactory {
|
||||||
def invoke(localFile: LocalFile, s3ObjectsData: RemoteObjects) =
|
def invoke(localFile: LocalFile, s3ObjectsData: RemoteObjects) =
|
||||||
S3MetaDataEnricher.getS3Status(localFile, s3ObjectsData)
|
S3MetaDataEnricher.getS3Status(localFile, s3ObjectsData)
|
||||||
|
|
||||||
def getMatchesByKey(
|
def getMatchesByKey(status: (Option[MD5Hash], Set[(RemoteKey, MD5Hash)]))
|
||||||
status: (Option[HashModified], Set[(MD5Hash, KeyModified)]))
|
: Option[MD5Hash] = {
|
||||||
: Option[HashModified] = {
|
|
||||||
val (byKey, _) = status
|
val (byKey, _) = status
|
||||||
byKey
|
byKey
|
||||||
}
|
}
|
||||||
|
|
||||||
def getMatchesByHash(
|
def getMatchesByHash(status: (Option[MD5Hash], Set[(RemoteKey, MD5Hash)]))
|
||||||
status: (Option[HashModified], Set[(MD5Hash, KeyModified)]))
|
: Set[(RemoteKey, MD5Hash)] = {
|
||||||
: Set[(MD5Hash, KeyModified)] = {
|
|
||||||
val (_, byHash) = status
|
val (_, byHash) = status
|
||||||
byHash
|
byHash
|
||||||
}
|
}
|
||||||
|
@ -80,25 +73,18 @@ class StorageServiceSuite extends FunSpec with MockFactory {
|
||||||
"when remote key exists, unmodified and other key matches the hash") {
|
"when remote key exists, unmodified and other key matches the hash") {
|
||||||
it("should return the match by key") {
|
it("should return the match by key") {
|
||||||
env.map({
|
env.map({
|
||||||
case (s3ObjectsData, localFile, lastModified, _, _, _, _) => {
|
case (s3ObjectsData, localFile, _, _, _, _) => {
|
||||||
val result = getMatchesByKey(invoke(localFile, s3ObjectsData))
|
val result = getMatchesByKey(invoke(localFile, s3ObjectsData))
|
||||||
assert(result.contains(HashModified(hash, lastModified)))
|
assert(result.contains(hash))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
it("should return both matches for the hash") {
|
it("should return both matches for the hash") {
|
||||||
env.map({
|
env.map({
|
||||||
case (s3ObjectsData,
|
case (s3ObjectsData, localFile, keyOtherKey, _, _, key) => {
|
||||||
localFile,
|
|
||||||
lastModified,
|
|
||||||
keyOtherKey,
|
|
||||||
_,
|
|
||||||
_,
|
|
||||||
key) => {
|
|
||||||
val result = getMatchesByHash(invoke(localFile, s3ObjectsData))
|
val result = getMatchesByHash(invoke(localFile, s3ObjectsData))
|
||||||
assertResult(
|
assertResult(
|
||||||
Set((hash, KeyModified(key, lastModified)),
|
Set((hash, key), (hash, keyOtherKey.remoteKey))
|
||||||
(hash, KeyModified(keyOtherKey.remoteKey, lastModified)))
|
|
||||||
)(result)
|
)(result)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -115,7 +101,7 @@ class StorageServiceSuite extends FunSpec with MockFactory {
|
||||||
it("should return no matches by key") {
|
it("should return no matches by key") {
|
||||||
env2.map(localFile => {
|
env2.map(localFile => {
|
||||||
env.map({
|
env.map({
|
||||||
case (s3ObjectsData, _, _, _, _, _, _) => {
|
case (s3ObjectsData, _, _, _, _, _) => {
|
||||||
val result = getMatchesByKey(invoke(localFile, s3ObjectsData))
|
val result = getMatchesByKey(invoke(localFile, s3ObjectsData))
|
||||||
assert(result.isEmpty)
|
assert(result.isEmpty)
|
||||||
}
|
}
|
||||||
|
@ -125,7 +111,7 @@ class StorageServiceSuite extends FunSpec with MockFactory {
|
||||||
it("should return no matches by hash") {
|
it("should return no matches by hash") {
|
||||||
env2.map(localFile => {
|
env2.map(localFile => {
|
||||||
env.map({
|
env.map({
|
||||||
case (s3ObjectsData, _, _, _, _, _, _) => {
|
case (s3ObjectsData, _, _, _, _, _) => {
|
||||||
val result = getMatchesByHash(invoke(localFile, s3ObjectsData))
|
val result = getMatchesByHash(invoke(localFile, s3ObjectsData))
|
||||||
assert(result.isEmpty)
|
assert(result.isEmpty)
|
||||||
}
|
}
|
||||||
|
@ -137,31 +123,18 @@ class StorageServiceSuite extends FunSpec with MockFactory {
|
||||||
describe("when remote key exists and no others match hash") {
|
describe("when remote key exists and no others match hash") {
|
||||||
it("should return the match by key") {
|
it("should return the match by key") {
|
||||||
env.map({
|
env.map({
|
||||||
case (s3ObjectsData,
|
case (s3ObjectsData, _, _, keyDiffHash, diffHash, _) => {
|
||||||
_,
|
|
||||||
lastModified,
|
|
||||||
_,
|
|
||||||
keyDiffHash,
|
|
||||||
diffHash,
|
|
||||||
_) => {
|
|
||||||
val result = getMatchesByKey(invoke(keyDiffHash, s3ObjectsData))
|
val result = getMatchesByKey(invoke(keyDiffHash, s3ObjectsData))
|
||||||
assert(result.contains(HashModified(diffHash, lastModified)))
|
assert(result.contains(diffHash))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
it("should return one match by hash") {
|
it("should return one match by hash") {
|
||||||
env.map({
|
env.map({
|
||||||
case (s3ObjectsData,
|
case (s3ObjectsData, _, _, keyDiffHash, diffHash, _) => {
|
||||||
_,
|
|
||||||
lastModified,
|
|
||||||
_,
|
|
||||||
keyDiffHash,
|
|
||||||
diffHash,
|
|
||||||
_) => {
|
|
||||||
val result = getMatchesByHash(invoke(keyDiffHash, s3ObjectsData))
|
val result = getMatchesByHash(invoke(keyDiffHash, s3ObjectsData))
|
||||||
assertResult(
|
assertResult(
|
||||||
Set(
|
Set((diffHash, keyDiffHash.remoteKey))
|
||||||
(diffHash, KeyModified(keyDiffHash.remoteKey, lastModified)))
|
|
||||||
)(result)
|
)(result)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,6 +16,7 @@ import net.kemitix.thorp.domain._
|
||||||
import org.scalamock.scalatest.MockFactory
|
import org.scalamock.scalatest.MockFactory
|
||||||
import org.scalatest.FreeSpec
|
import org.scalatest.FreeSpec
|
||||||
import zio.{DefaultRuntime, Task}
|
import zio.{DefaultRuntime, Task}
|
||||||
|
import net.kemitix.thorp.domain.NonUnit.~*
|
||||||
|
|
||||||
class UploaderTest extends FreeSpec with MockFactory {
|
class UploaderTest extends FreeSpec with MockFactory {
|
||||||
|
|
||||||
|
@ -39,16 +40,17 @@ class UploaderTest extends FreeSpec with MockFactory {
|
||||||
val expected =
|
val expected =
|
||||||
Right(UploadQueueEvent(remoteKey, aHash))
|
Right(UploadQueueEvent(remoteKey, aHash))
|
||||||
new AmazonS3ClientTestFixture {
|
new AmazonS3ClientTestFixture {
|
||||||
|
~*(
|
||||||
(fixture.amazonS3TransferManager.upload _)
|
(fixture.amazonS3TransferManager.upload _)
|
||||||
.when()
|
.when()
|
||||||
.returns(_ => Task.succeed(inProgress))
|
.returns(_ => Task.succeed(inProgress)))
|
||||||
private val result =
|
private val result =
|
||||||
invoke(fixture.amazonS3TransferManager)(
|
invoke(fixture.amazonS3TransferManager)(
|
||||||
localFile,
|
localFile,
|
||||||
bucket,
|
bucket,
|
||||||
listenerSettings
|
listenerSettings
|
||||||
)
|
)
|
||||||
assertResult(expected)(result)
|
~*(assertResult(expected)(result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"when Amazon Service Exception" in {
|
"when Amazon Service Exception" in {
|
||||||
|
@ -57,16 +59,17 @@ class UploaderTest extends FreeSpec with MockFactory {
|
||||||
Right(
|
Right(
|
||||||
ErrorQueueEvent(Action.Upload(remoteKey.key), remoteKey, exception))
|
ErrorQueueEvent(Action.Upload(remoteKey.key), remoteKey, exception))
|
||||||
new AmazonS3ClientTestFixture {
|
new AmazonS3ClientTestFixture {
|
||||||
|
~*(
|
||||||
(fixture.amazonS3TransferManager.upload _)
|
(fixture.amazonS3TransferManager.upload _)
|
||||||
.when()
|
.when()
|
||||||
.returns(_ => Task.fail(exception))
|
.returns(_ => Task.fail(exception)))
|
||||||
private val result =
|
private val result =
|
||||||
invoke(fixture.amazonS3TransferManager)(
|
invoke(fixture.amazonS3TransferManager)(
|
||||||
localFile,
|
localFile,
|
||||||
bucket,
|
bucket,
|
||||||
listenerSettings
|
listenerSettings
|
||||||
)
|
)
|
||||||
assertResult(expected)(result)
|
~*(assertResult(expected)(result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"when Amazon SDK Client Exception" in {
|
"when Amazon SDK Client Exception" in {
|
||||||
|
@ -75,16 +78,17 @@ class UploaderTest extends FreeSpec with MockFactory {
|
||||||
Right(
|
Right(
|
||||||
ErrorQueueEvent(Action.Upload(remoteKey.key), remoteKey, exception))
|
ErrorQueueEvent(Action.Upload(remoteKey.key), remoteKey, exception))
|
||||||
new AmazonS3ClientTestFixture {
|
new AmazonS3ClientTestFixture {
|
||||||
|
~*(
|
||||||
(fixture.amazonS3TransferManager.upload _)
|
(fixture.amazonS3TransferManager.upload _)
|
||||||
.when()
|
.when()
|
||||||
.returns(_ => Task.fail(exception))
|
.returns(_ => Task.fail(exception)))
|
||||||
private val result =
|
private val result =
|
||||||
invoke(fixture.amazonS3TransferManager)(
|
invoke(fixture.amazonS3TransferManager)(
|
||||||
localFile,
|
localFile,
|
||||||
bucket,
|
bucket,
|
||||||
listenerSettings
|
listenerSettings
|
||||||
)
|
)
|
||||||
assertResult(expected)(result)
|
~*(assertResult(expected)(result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
def invoke(transferManager: AmazonTransferManager)(
|
def invoke(transferManager: AmazonTransferManager)(
|
||||||
|
|
Loading…
Reference in a new issue