Skip to content

Commit 31f346a

Browse files
authored
File sync (#4)
1 parent 841e89e commit 31f346a

File tree

12 files changed

+258
-7
lines changed

12 files changed

+258
-7
lines changed

.scalafmt.conf

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1-
version = "3.5.9"
1+
version = 3.9.5
22
runner.dialect = scala3
33

44
maxColumn = 120
55
project.git = true
6+
7+
indent.main = 2
8+
indent.callSite = 2
9+
indent.ctorSite = 2
10+
indent.ctrlSite = 2
11+
indent.defnSite = 2
12+
indent.matchSite = 2
13+
indent.significant = 2
14+
615
align.preset = more
16+
align.stripMargin = true
17+
docstrings.style = Asterisk
18+
assumeStandardLibraryStripMargin = true
719

820
align.tokens = [
921
"=>", "->", "<-", ":=", "//", "%", "%%", "%%%", "+=",
@@ -19,4 +31,5 @@ align.tokens = [
1931
"code" = "=>"
2032
"owners" = [{ regex = "(Importee.Rename|Case)" }]
2133
}
22-
]
34+
]
35+

chat-app/.scalafmt.conf

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# DO NOT EDIT THIS FILE WITHOUT CONSENT OF TEAM.
21
version = 3.9.5
32
runner.dialect = scala3
43

@@ -13,11 +12,10 @@ indent.defnSite = 2
1312
indent.matchSite = 2
1413
indent.significant = 2
1514

16-
docstrings.style = keep
17-
assumeStandardLibraryStripMargin = true
18-
align.stripMargin = true
19-
2015
align.preset = more
16+
align.stripMargin = true
17+
docstrings.style = Asterisk
18+
assumeStandardLibraryStripMargin = true
2119

2220
align.tokens = [
2321
"=>", "->", "<-", ":=", "//", "%", "%%", "%%%", "+=",

filesync/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# File Sync
2+
3+
NOTE: Sometimes, this app throws error when running from IntelliJ. This is because the `agent` is bundled as a resource in the `Sync` project, which requires running `sbt assembly` at least once. So run `sbt assembly` in the terminal before running the app from IntelliJ.
4+
5+
Also, when running the app from IntelliJ, configure the run configuration for `sync.Sync` to pass program arguments (source and destination paths).
6+
7+
## Exercises
8+
9+
- Syncing folders/sub-folders
10+
11+
Track `Rpc.CreateFolder` case class
12+
13+
- Syncing deleted files/folders
14+
15+
Track `Rpc.DeletePath` case class
16+
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package sync
2+
3+
import java.io.{DataInputStream, DataOutputStream}
4+
5+
object Agent {
6+
def main(args: Array[String]): Unit = {
7+
val input = new DataInputStream(System.in)
8+
val output = new DataOutputStream(System.out)
9+
10+
while (true) try {
11+
val rpc = Shared.receive[Rpc](input)
12+
System.err.println("Agent handling: " + rpc)
13+
14+
rpc match {
15+
case Rpc.StatPath(path) =>
16+
Shared.send(output, Rpc.StatInfo(path, Shared.hashPath(os.pwd / path)))
17+
case Rpc.WriteOver(bytes, path) =>
18+
os.remove.all(os.pwd / path)
19+
os.write.over(os.pwd / path, bytes, createFolders = true)
20+
case Rpc.DeletePath(path) =>
21+
os.remove.all(os.pwd / path)
22+
case sync.Rpc.CreateFolder(path) =>
23+
os.makeDir.all(os.pwd / path)
24+
}
25+
} catch {
26+
case e: java.io.EOFException =>
27+
System.err.println("+--------------------------------------------------+")
28+
e.printStackTrace(System.err)
29+
System.err.println("+--------------------------------------------------+")
30+
System.exit(0)
31+
}
32+
}
33+
}

filesync/build.sbt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import java.nio.file.Paths
2+
3+
ThisBuild / version := "0.1.0-SNAPSHOT"
4+
ThisBuild / scalaVersion := "3.7.0"
5+
6+
ThisBuild / libraryDependencies ++=
7+
"com.lihaoyi" %% "upickle" % "4.1.0" ::
8+
"com.lihaoyi" %% "os-lib" % "0.11.4" ::
9+
"com.lihaoyi" %% "os-lib-watch" % "0.11.4" ::
10+
"com.lihaoyi" %% "castor" % "0.3.0" ::
11+
Nil
12+
13+
lazy val shared = project
14+
15+
lazy val agent =
16+
project
17+
.dependsOn(shared)
18+
.settings(
19+
assembly / mainClass := Some("sync.Agent"),
20+
assembly / assemblyJarName := "agent.jar"
21+
)
22+
23+
lazy val sync =
24+
project
25+
.dependsOn(shared, agent)
26+
.settings(
27+
Compile / resources := (Compile / resources).value ++ Seq(
28+
{
29+
val agentJar = (agent / assembly / assemblyOutputPath).value
30+
println(s"************************************** $agentJar")
31+
/*val destPath = target.value / "agent2.jar"
32+
IO.copyFile(agentJar, destPath)*/
33+
file(agentJar.getAbsolutePath)
34+
}
35+
)
36+
)

filesync/project/build.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
sbt.version=1.10.7
2+
build.properties=1.10.11

filesync/project/plugins.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.1")
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package sync
2+
3+
import os.SubPath
4+
import upickle.default.{ReadWriter, macroRW, readwriter}
5+
6+
sealed trait Rpc
7+
8+
object Rpc {
9+
implicit val subPathRw: ReadWriter[SubPath] = readwriter[String].bimap[os.SubPath](_.toString, os.SubPath(_))
10+
11+
case class StatPath(path: os.SubPath) extends Rpc
12+
implicit val statPathRw: ReadWriter[StatPath] = macroRW
13+
14+
case class WriteOver(src: Array[Byte], path: os.SubPath) extends Rpc
15+
implicit val writeOverRw: ReadWriter[WriteOver] = macroRW
16+
17+
case class CreateFolder(path: os.SubPath) extends Rpc
18+
implicit val createFolderRw: ReadWriter[CreateFolder] = macroRW
19+
20+
case class StatInfo(p: os.SubPath, fileHash: Option[Int])
21+
implicit val statInfoRw: ReadWriter[StatInfo] = macroRW
22+
23+
case class DeletePath(path: os.SubPath) extends Rpc
24+
implicit val deletePathRw: ReadWriter[DeletePath] = macroRW
25+
26+
implicit val msgRw: ReadWriter[Rpc] = macroRW
27+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package sync
2+
3+
import upickle.default.{Reader, Writer}
4+
5+
import java.io.{DataInputStream, DataOutputStream}
6+
7+
object Shared:
8+
def send[T: Writer](out: DataOutputStream, msg: T): Unit =
9+
val bytes = upickle.default.writeBinary(msg)
10+
out.writeInt(bytes.length)
11+
out.write(bytes)
12+
out.flush()
13+
14+
def receive[T: Reader](in: DataInputStream): T =
15+
val buf = new Array[Byte](in.readInt())
16+
in.readFully(buf)
17+
upickle.default.readBinary[T](buf)
18+
19+
def hashPath(p: os.Path): Option[Int] =
20+
if (!os.isFile(p)) None
21+
else Some(java.util.Arrays.hashCode(os.read.bytes(p)))
22+
end Shared
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package sync
2+
3+
import sync.Rpc.StatInfo
4+
5+
sealed trait Msg
6+
case class ChangedPath(value: os.SubPath, deleted: Boolean) extends Msg
7+
case class AgentResponse(value: StatInfo) extends Msg
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package sync
2+
3+
import castor.Context.Simple.global
4+
5+
object Sync:
6+
def main(args: Array[String]): Unit = {
7+
if (args.length != 2) {
8+
println("Usage: sync <src> <dest>")
9+
System.exit(1)
10+
}
11+
12+
val src = os.Path(args(0))
13+
val dest = os.Path(args(1))
14+
15+
if (!os.exists(src)) {
16+
println(s"Source path does not exist: $src")
17+
System.exit(2)
18+
}
19+
20+
println(s"Syncing $src to $dest ...")
21+
22+
val agentExecutable = os.temp(os.read.bytes(os.resource / "agent.jar"))
23+
os.perms.set(agentExecutable, "rwxr-xr-x")
24+
val agent = os.proc("java", "-jar", agentExecutable).spawn(cwd = dest)
25+
26+
object SyncActor extends castor.SimpleActor[Msg]:
27+
def run(msg: Msg): Unit = {
28+
println("SyncActor handling: " + msg)
29+
msg match {
30+
case ChangedPath(value, false) =>
31+
Shared.send(agent.stdin.data, Rpc.StatPath(value))
32+
case ChangedPath(value, true) =>
33+
Shared.send(agent.stdin.data, Rpc.DeletePath(value))
34+
case AgentResponse(Rpc.StatInfo(p, remoteHash)) =>
35+
Shared.hashPath(src / p) match {
36+
case None => // It is a folder
37+
Shared.send(agent.stdin.data, Rpc.CreateFolder(p))
38+
case `remoteHash` => // Local and remote hashes are same. Do nothing
39+
case Some(_) => // It is a file and content differs
40+
Shared.send(agent.stdin.data, Rpc.WriteOver(os.read.bytes(src / p), p))
41+
}
42+
}
43+
}
44+
45+
val agentReader = new Thread(() => {
46+
while (agent.isAlive()) {
47+
SyncActor.send(AgentResponse(Shared.receive[Rpc.StatInfo](agent.stdout.data)))
48+
}
49+
})
50+
51+
agentReader.start()
52+
53+
val watcher = os.watch.watch(
54+
Seq(src),
55+
onEvent = _.foreach { p =>
56+
val path = p.subRelativeTo(src)
57+
SyncActor.send(ChangedPath(path, !os.exists(src / path)))
58+
}
59+
)
60+
61+
Thread.sleep(Long.MaxValue)
62+
}
63+
64+
end Sync
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package filesync
2+
3+
object SimpleFileSync {
4+
5+
/** There are three cases the above code does not support:
6+
* - If the source path is empty and the destination path contains a folder. This is a "delete" which we will
7+
* ignore for now; supporting this is left as an exercise for the reader
8+
* - If the source path is a folder and the destination path is a folder. In this case doing nothing is fine:
9+
* os.walk will enter the source path folder and process all the files within it recursively
10+
*/
11+
def sync(src: os.Path, dest: os.Path): Unit = {
12+
os.walk(src).foreach { srcSubPath =>
13+
val subPath = srcSubPath.subRelativeTo(src)
14+
val destSubPath = dest / subPath
15+
16+
(os.isDir(srcSubPath), os.isDir(destSubPath)) match {
17+
case (false, true) | (true, false) =>
18+
os.copy.over(srcSubPath, destSubPath, createFolders = true)
19+
case (false, false)
20+
if !os.exists(destSubPath)
21+
|| !os.read.bytes(srcSubPath).sameElements(os.read.bytes(destSubPath)) =>
22+
os.copy.over(srcSubPath, destSubPath, createFolders = true)
23+
case _ => // do nothing
24+
}
25+
}
26+
}
27+
28+
@main
29+
def main(): Unit = {
30+
sync(os.pwd / "src/main/scala", os.home / "Temp" / "blah")
31+
}
32+
}

0 commit comments

Comments
 (0)