/* Copyright 2010 Aaron J. Radke Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cc.drx /** Rsync is not available on windows and does not provide a generic transformation and skip api*/ object Sync{ /**default with ansi progress display*/ def apply(transform:File => Action):Sync = Sync(transform, _.print) /**default direct mapping with ansi progress display*/ def apply(srcDir:File, dstDir:File, dryRun:Boolean=false):Summary = Sync(f => Copy(f), _.print).apply(srcDir, dstDir, dryRun) //--support types abstract sealed trait Action{ def dst:File def reason:Option[Symbol] def sync(src:File, dryRun:Boolean):Unit //TODO return the success or failure to be reported... } //Action classes case class Skip(dst:File, reason:Option[Symbol]=None) extends Action { def sync(src:File, dryRun:Boolean) = {} //sync operations for Skip are no-ops } case class Copy(dst:File, reason:Option[Symbol]=None) extends Action { def sync(src:File, dryRun:Boolean) = if(!dryRun){ val res = src.copyTo(dst) //use smart copy and create parent directories if they don't exists if(res.isFailure) println(Red(s"could not copy $src to $dst; $res")) } } //Alternative constructors object Skip{def apply(dst:File, reason:Symbol):Skip = Skip(dst, Some(reason))} object Copy{def apply(dst:File, reason:Symbol):Copy = Copy(dst, Some(reason))} case class ReasonCount(reasons:Map[Option[Symbol], Int]=Map.empty){ def +(reason:Option[Symbol]) = { val count:Int = reasons.getOrElse(reason, 0)+1 ReasonCount( reasons + (reason -> count) ) } lazy val total:Int = reasons.values.sum override def toString = reasons.map{case (k,v) => (k getOrElse Symbol("Unspecified")).name.replace(" ","_") + ":" + v }.mkString(" ") } case class Summary(copyCount:ReasonCount, skipCount:ReasonCount, action:Action){ import Color._ def ansi:String = Ansi.kson("Sync", "copy" -> s"{$copyCount}", "skip" -> s"{$skipCount}") override def toString = Ansi.strip(ansi) /**nice ansi progress print*/ def print:Unit = print(false) def printVerbose:Unit = print(true) def print(verbose:Boolean):Unit = { val reasonString = action.reason.map{s=>s" (${s.name})"}.getOrElse("") action match { case _:Skip => if(verbose) println(s"Sync skip "+ action.dst + reasonString) else Ansi.printtmp(ansi) //don't print all the cases case Copy(dst,reason) => println(s"Sync copy to "+ action.dst + reasonString) } } def +(action:Action):Summary = action match { case _:Copy => Summary(copyCount+action.reason, skipCount, action) case _:Skip => Summary(copyCount, skipCount+action.reason, action) } def total = copyCount.total + skipCount.total } } import Sync._ /**Simple file sync with generic transformation definition*/ case class Sync(transform:File => Action, progress:Summary => Unit){ /**use the default verbose file printing progress*/ def verbose:Sync = Sync(transform, _.printVerbose) def apply(srcDir:File, dstDir:File, dryRun:Boolean=false):Summary = { //TODO make this also work if the src is a File instead of a directory //immediately find the canonical version of the src and dst, (for windows machines this is especially important to expand things like ~/ for the home directory) val srcDirCanon = srcDir.canon //now the user passed in variables can be masked val dstDirCanon = dstDir.canon var summary = Summary(ReasonCount(),ReasonCount(),Copy(dstDir)) //initial sync summary zero and remote root dir // Macro.pp(srcDir, srcDirCanon, srcDirCanon.isFile, srcDirCanon.isDir) Timer.withHeader(Ansi.kson("Sync"+(if(dryRun)"(dry)" else ""), "src" -> srcDir, "dst" -> dstDir)){ for(src <- srcDirCanon.walk if !src.isDir){ //TODO add walkSkipIf to skip entire dirs val srcRelative = src relativeTo srcDirCanon //-- determine the action type val action = transform(srcRelative) match { case Copy(dstRelative,reason) => { val dst = dstDirCanon / dstRelative val reasonString = reason.map{"."+_.name}.getOrElse("") //add .subcategory reason def copy(reason:Symbol):Copy = Copy(dst, Symbol(reason.name + reasonString)) // -- needs update tests on the absolute file locations // Essentially skip copy unless files are possibly different then force overwrite // Note: doing a diff(or sha1) is on the order of time to do a copy so just do a copy if (!dst.isFile) copy(Symbol("Missing")) else if(src.size != dst.size) copy(Symbol("DifferentSize")) else if(src.modified > dst.modified) copy(Symbol("Older")) else Skip(dst, Symbol("Exists" + reasonString)) } case x => x //this skip reason from the relative transform should just pass through } progress(summary) //call the updated summary with the progress callback (report before action) action.sync(src,dryRun) // apply the sync action (skips are essentially null ops) summary += action // update the summary with a new count of the action types } println(summary.ansi) summary } } }