|
#!/usr/bin/amm |
|
|
|
import ammonite.ops._ |
|
|
|
// config format |
|
|
|
import $ivy.`io.circe::circe-core:0.13.0`, io.circe._, io.circe.syntax._ |
|
import $ivy.`io.circe::circe-parser:0.13.0`, io.circe.parser._ |
|
import $ivy.`io.circe::circe-generic-extras:0.13.0`, io.circe.generic.extras._, io.circe.generic.extras.semiauto._ |
|
import $ivy.`io.circe::circe-yaml:0.12.0`, io.circe.yaml, io.circe.yaml.syntax._ |
|
|
|
implicit val codecConfiguration: Configuration = |
|
Configuration.default.withKebabCaseConstructorNames |
|
|
|
final case class Rules( |
|
include: List[String] = Nil, |
|
exclude: List[String] = Nil |
|
) { |
|
|
|
def ++(rules: Rules): Rules = Rules(include ++ rules.include, exclude ++ rules.exclude) |
|
} |
|
object Rules { |
|
implicit val decoder: Decoder[Rules] = deriveConfiguredDecoder[Rules] |
|
implicit val encoder: Encoder[Rules] = deriveConfiguredEncoder[Rules] |
|
} |
|
|
|
final case class DotSyncConfig( |
|
backup: String = ".dotsync_bck", |
|
global: Rules = Rules(), |
|
local: Map[String, Rules] = Map.empty |
|
) |
|
object DotSyncConfig { |
|
implicit val decoder: Decoder[DotSyncConfig] = deriveConfiguredDecoder[DotSyncConfig] |
|
implicit val encoder: Encoder[DotSyncConfig] = deriveConfiguredEncoder[DotSyncConfig] |
|
|
|
def readConfig(dir: os.Path): DotSyncConfig = { |
|
val configLocation = dir/".dotsync.yml" |
|
if (!(exists! configLocation)) { |
|
println(s"$configLocation not found - generating empty config") |
|
write(configLocation, DotSyncConfig().asJson.asYaml.spaces2) |
|
DotSyncConfig() |
|
} else { |
|
println(s"$configLocation found, reading") |
|
yaml.parser.parse(read! configLocation).flatMap(Decoder[DotSyncConfig].decodeJson).right.get |
|
} |
|
} |
|
} |
|
|
|
sealed trait OperationType |
|
object OperationType { |
|
case object Backup extends OperationType |
|
case object Restore extends OperationType |
|
case object Status extends OperationType |
|
case object Push extends OperationType |
|
case object Pull extends OperationType |
|
} |
|
|
|
// backup -- restore |
|
|
|
def backupOrRestore(restore: Boolean, |
|
dir: os.Path, |
|
backupDir: os.Path, |
|
global: Rules, |
|
local: Map[String, Rules], |
|
locals: List[String]): Unit = { |
|
val (in, out) = if (restore) (backupDir, dir) else (dir, backupDir) |
|
|
|
val Rules(includeRules, excludeRules) = |
|
(global :: local.collect { case (tag, rule) if locals.contains(tag) => rule }.toList).reduce(_ ++ _) |
|
|
|
def allFiles(file: os.Path): LazyList[os.Path] = |
|
if (file.isDir) { |
|
LazyList |
|
.from(file.toIO.listFiles) |
|
.map(FilePath(_)) |
|
.collect{ case p: Path => p } |
|
.flatMap(allFiles) |
|
} |
|
else if (file.isFile) LazyList(file) |
|
else LazyList.empty |
|
|
|
val included = LazyList.from(includeRules).map(os.RelPath(_)).map(in / _).flatMap(allFiles(_)) |
|
val excluded = LazyList.from(excludeRules).map(os.RelPath(_)).map(in / _).flatMap(allFiles(_)) |
|
|
|
included.filterNot { i => |
|
excluded.exists(i.startsWith _) |
|
}.foreach { inFile => |
|
try { |
|
val outFile = out / inFile.relativeTo(in) |
|
println(s"cp: $inFile -> $outFile") |
|
val parent = outFile.toIO.getParentFile |
|
if (!parent.exists) { |
|
mkdir! FilePath(parent).asInstanceOf[os.Path] |
|
} |
|
cp.over(inFile, outFile) |
|
} catch { |
|
case e: Throwable => |
|
e.printStackTrace |
|
println("Error during sync - skipping") |
|
} |
|
} |
|
} |
|
|
|
// status |
|
|
|
def gitStatus(backupDir: os.Path): Unit = { |
|
println(backupDir) |
|
%.apply("git", "status")(backupDir) |
|
} |
|
|
|
// push |
|
|
|
def gitPushChanges(backupDir: os.Path, msg: String): Unit = { |
|
%%.apply("git", "add", ".", ".*")(backupDir) |
|
%%.apply("git", "commit", "-m", msg)(backupDir) |
|
try { |
|
%%.apply("git", "push")(backupDir) |
|
} catch { |
|
case _ => println("git push failed, skipping") |
|
} |
|
} |
|
|
|
// pull |
|
|
|
def gitPullChanges(backupDir: os.Path): Unit = { |
|
try { |
|
%%.apply("git", "pull")(backupDir) |
|
} catch { |
|
case _ => println("git pull failed, skipping") |
|
} |
|
} |
|
|
|
// dispatch |
|
|
|
def dispatcher(operation: OperationType, |
|
dir: os.Path, |
|
locals: List[String] = Nil, |
|
msg: String = ""): Unit = { |
|
val DotSyncConfig(backupDirRel, global, local) = DotSyncConfig.readConfig(dir) |
|
val backupDir = dir / backupDirRel |
|
|
|
println(s"Backup dir resolved to $backupDir") |
|
if (!backupDir.toIO.exists) { |
|
println(s"$backupDir doesn't exists, creating") |
|
mkdir! backupDir |
|
} |
|
if (!(backupDir / ".git").toIO.exists) { |
|
println(s"Initialize git respository in $backupDir") |
|
%%.apply("git", "init")(backupDir) |
|
println(s"Don't forget to set up git remote in $backupDir to enable push and pull!") |
|
} |
|
|
|
operation match { |
|
case OperationType.Backup => backupOrRestore(false, dir, backupDir, global, local, locals.toList) |
|
case OperationType.Restore => backupOrRestore(true, dir, backupDir, global, local, locals.toList) |
|
case OperationType.Status => gitStatus(backupDir) |
|
case OperationType.Push => gitPushChanges(backupDir, msg) |
|
case OperationType.Pull => gitPullChanges(backupDir) |
|
} |
|
} |
|
|
|
// main |
|
|
|
@main |
|
@doc("copy files from dir to backup dir (included, not excluded)") |
|
def backup( |
|
@doc("directory of .dotsync.yml, backup directory will be resolved in relation to it") |
|
dir: os.Path, |
|
@doc("list of locals to process") |
|
locals: String* |
|
): Unit = dispatcher(OperationType.Backup, dir, locals = locals.toList) |
|
|
|
@main |
|
@doc("copy files from backup dir to dir (included, not excluded)") |
|
def restore( |
|
@doc("directory of .dotsync.yml, backup directory will be resolved in relation to it") |
|
dir: os.Path, |
|
@doc("list of locals to process") |
|
locals: String* |
|
): Unit = dispatcher(OperationType.Restore, dir, locals = locals.toList) |
|
|
|
@main |
|
@doc("check git status of backup dir") |
|
def status( |
|
@doc("directory of .dotsync.yml, backup directory will be resolved in relation to it") |
|
dir: os.Path |
|
): Unit = dispatcher(OperationType.Status, dir) |
|
|
|
@main |
|
@doc("commit all changes and push them to remote repository if possible") |
|
def push( |
|
@doc("directory of .dotsync.yml, backup directory will be resolved in relation to it") |
|
dir: os.Path, |
|
@doc("message for git commit") |
|
msg: String |
|
): Unit = dispatcher(OperationType.Push, dir, msg = msg) |
|
|
|
@main |
|
@doc("pull changes from remote respository") |
|
def pull( |
|
@doc("directory of .dotsync.yml, backup directory will be resolved in relation to it") |
|
dir: os.Path |
|
): Unit = dispatcher(OperationType.Pull, dir) |