From 20d25fd85ae5a53f7f6fbaf2d234c9c2eb340f23 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Tue, 30 Jun 2026 21:49:07 +0100 Subject: [PATCH 1/3] Remvoe extra files when restoring snapshots Signed-off-by: kerthcet --- sandd/src/snapshot/manager.rs | 102 +++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 3 deletions(-) diff --git a/sandd/src/snapshot/manager.rs b/sandd/src/snapshot/manager.rs index 01ab088..54d3908 100644 --- a/sandd/src/snapshot/manager.rs +++ b/sandd/src/snapshot/manager.rs @@ -200,7 +200,7 @@ impl SnapshotManager { Ok(()) } - /// Restore tree recursively + /// Restore tree recursively (always clean - deletes extras after successful restore) fn restore_tree<'a>( &'a self, tree_hash: &'a str, @@ -209,11 +209,19 @@ impl SnapshotManager { Box::pin(async move { fs::create_dir_all(dest).await?; - // Load tree object + // Load tree object - tells us what SHOULD exist let tree_json = self.store.get_blob(tree_hash).await?; let tree: Tree = serde_json::from_slice(&tree_json)?; - // Restore each entry + // Build set of expected names in this directory (owned strings to avoid borrow issues) + let expected_names: std::collections::HashSet = tree + .entries + .iter() + .map(|e| e.name.clone()) + .collect(); + + // Phase 1: Restore each entry from snapshot + // Do this FIRST - if restore fails, extras remain untouched (safer) for entry in tree.entries { let entry_path = dest.join(&entry.name); @@ -281,6 +289,29 @@ impl SnapshotManager { } } + // Phase 2: Clean this directory - delete extras (only after successful restore) + // Cleanup failures are warned but don't fail the operation + let mut read_dir = fs::read_dir(dest).await?; + while let Some(entry) = read_dir.next_entry().await? { + let name = entry.file_name(); + let name_str = name.to_string_lossy().to_string(); + + if !expected_names.contains(&name_str) { + let path = entry.path(); + + // Not in snapshot - delete it + if path.is_dir() { + if let Err(e) = fs::remove_dir_all(&path).await { + tracing::warn!("Failed to delete directory {}: {}", path.display(), e); + } + } else { + if let Err(e) = fs::remove_file(&path).await { + tracing::warn!("Failed to delete file {}: {}", path.display(), e); + } + } + } + } + Ok(()) }) } @@ -1189,4 +1220,69 @@ mod tests { assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("does not exist")); } + + #[tokio::test] + async fn test_restore_always_clean() { + let temp_dir = TempDir::new().unwrap(); + let store_dir = temp_dir.path().join("store"); + let workspace = temp_dir.path().join("workspace"); + let restore_dir = temp_dir.path().join("restored"); + + // Create snapshot with specific files + fs::create_dir_all(&workspace).await.unwrap(); + fs::write(workspace.join("file1.txt"), "content1") + .await + .unwrap(); + fs::create_dir_all(workspace.join("dir1")).await.unwrap(); + fs::write(workspace.join("dir1/file2.txt"), "content2") + .await + .unwrap(); + + let manager = SnapshotManager::new(store_dir).unwrap(); + let snapshot_id = manager + .create_snapshot(&workspace, Some("Clean test".to_string()), None) + .await + .unwrap(); + + // Restore to directory with extra files + fs::create_dir_all(&restore_dir).await.unwrap(); + fs::write(restore_dir.join("extra_file.txt"), "should be deleted") + .await + .unwrap(); + fs::create_dir_all(restore_dir.join("extra_dir")) + .await + .unwrap(); + fs::write(restore_dir.join("extra_dir/nested.txt"), "also deleted") + .await + .unwrap(); + fs::create_dir_all(restore_dir.join("dir1")).await.unwrap(); + fs::write(restore_dir.join("dir1/extra_in_dir.txt"), "delete me") + .await + .unwrap(); + + // Restore snapshot (should clean extras) + manager + .restore_snapshot(&snapshot_id, &restore_dir) + .await + .unwrap(); + + // Verify exact match - only snapshot files exist + assert!(restore_dir.join("file1.txt").exists()); + assert!(restore_dir.join("dir1/file2.txt").exists()); + + // Verify extras are deleted + assert!(!restore_dir.join("extra_file.txt").exists()); + assert!(!restore_dir.join("extra_dir").exists()); + assert!(!restore_dir.join("dir1/extra_in_dir.txt").exists()); + + // Verify content is correct + let content1 = fs::read_to_string(restore_dir.join("file1.txt")) + .await + .unwrap(); + assert_eq!(content1, "content1"); + let content2 = fs::read_to_string(restore_dir.join("dir1/file2.txt")) + .await + .unwrap(); + assert_eq!(content2, "content2"); + } } From 781752ddec2dcc470c572f62c97d6235e1630b41 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Wed, 1 Jul 2026 11:40:14 +0100 Subject: [PATCH 2/3] disable log event for now Signed-off-by: kerthcet --- Cargo.lock | 11 + Cargo.toml | 2 +- protocol/Cargo.toml | 9 + server/src/protocol.rs => protocol/src/lib.rs | 34 +-- sandd/Cargo.toml | 1 + sandd/src/main.rs | 6 +- sandd/src/protocol.rs | 202 ------------------ sandd/src/session.rs | 2 +- sandd/src/snapshot/manager.rs | 102 +-------- sandd/src/snapshot/types.rs | 15 +- server/Cargo.toml | 1 + server/src/lib.rs | 4 +- server/src/registry.rs | 2 +- server/src/server.rs | 2 +- 14 files changed, 55 insertions(+), 338 deletions(-) create mode 100644 protocol/Cargo.toml rename server/src/protocol.rs => protocol/src/lib.rs (97%) delete mode 100644 sandd/src/protocol.rs diff --git a/Cargo.lock b/Cargo.lock index 217ee09..6f68d5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1702,6 +1702,7 @@ dependencies = [ "parking_lot 0.12.5", "pyo3", "pythonize", + "sandd-protocol", "serde", "serde_json", "tokio", @@ -1725,6 +1726,7 @@ dependencies = [ "filetime", "futures-util", "portable-pty", + "sandd-protocol", "serde", "serde_json", "sysinfo", @@ -1739,6 +1741,15 @@ dependencies = [ "walkdir", ] +[[package]] +name = "sandd-protocol" +version = "0.0.0" +dependencies = [ + "base64", + "serde", + "serde_json", +] + [[package]] name = "schannel" version = "0.1.29" diff --git a/Cargo.toml b/Cargo.toml index 70f3efa..ea69564 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["server", "sandd"] +members = ["protocol", "server", "sandd"] resolver = "2" [workspace.dependencies] diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml new file mode 100644 index 0000000..ed3324c --- /dev/null +++ b/protocol/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "sandd-protocol" +version = "0.0.0" +edition = "2021" + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +base64 = "0.22" diff --git a/server/src/protocol.rs b/protocol/src/lib.rs similarity index 97% rename from server/src/protocol.rs rename to protocol/src/lib.rs index 3cc38ca..33fbdfe 100644 --- a/server/src/protocol.rs +++ b/protocol/src/lib.rs @@ -1,10 +1,21 @@ +// Shared protocol between daemon and server + use serde::{Deserialize, Serialize}; -/// Protocol messages exchanged between agent and daemon +/// Snapshot metadata (shared between daemon and server) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SnapshotInfo { + pub id: String, + pub created_at: u64, // Unix timestamp in seconds + pub message: String, + pub tags: Vec, + pub file_count: usize, + pub total_size: u64, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum Message { - // Connection management Register { daemon_id: String, metadata: DaemonMetadata, @@ -15,8 +26,6 @@ pub enum Message { }, Heartbeat, Pong, - - // Command execution (simple mode) ExecuteCommand { request_id: String, command: String, @@ -38,8 +47,6 @@ pub enum Message { request_id: String, error: String, }, - - // Interactive session (PTY mode) NewSession { session_id: String, rows: u16, @@ -74,14 +81,12 @@ pub enum Message { session_id: String, exit_code: i32, }, - - // File transfer FileUploadStart { request_id: String, path: String, total_size: u64, #[serde(default)] - mode: Option, // Unix file permissions + mode: Option, }, FileUploadChunk { request_id: String, @@ -109,7 +114,6 @@ pub enum Message { request_id: String, error: String, }, - // Snapshot operations CreateSnapshot { request_id: String, @@ -138,7 +142,7 @@ pub enum Message { }, SnapshotList { request_id: String, - snapshots: Vec, + snapshots: Vec, }, FindSnapshotByTag { request_id: String, @@ -150,7 +154,7 @@ pub enum Message { }, SnapshotDetails { request_id: String, - snapshot: Option, + snapshot: Option, }, DeleteSnapshot { request_id: String, @@ -163,8 +167,6 @@ pub enum Message { request_id: String, error: String, }, - - // Error handling Error { message: String, #[serde(default)] @@ -184,14 +186,13 @@ pub struct DaemonMetadata { } fn default_timeout() -> u64 { - 300 // 5 minutes + 300 } fn default_term() -> String { "xterm-256color".to_string() } -// Base64 encoding for binary data in JSON mod base64_bytes { use serde::{Deserialize, Deserializer, Serializer}; @@ -209,7 +210,6 @@ mod base64_bytes { .map_err(serde::de::Error::custom) } } - #[cfg(test)] mod tests { use super::*; diff --git a/sandd/Cargo.toml b/sandd/Cargo.toml index 8d2974b..cbac446 100644 --- a/sandd/Cargo.toml +++ b/sandd/Cargo.toml @@ -28,6 +28,7 @@ name = "snapshot_real_project" path = "../examples/snapshot_real_project.rs" [dependencies] +sandd-protocol = { path = "../protocol" } tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/sandd/src/main.rs b/sandd/src/main.rs index 377d18d..fc1f873 100644 --- a/sandd/src/main.rs +++ b/sandd/src/main.rs @@ -1,5 +1,5 @@ mod executor; -mod protocol; +// Use shared protocol crate mod session; pub mod snapshot; @@ -7,7 +7,7 @@ use anyhow::{Context, Result}; use clap::Parser; use executor::CommandExecutor; use futures_util::{SinkExt, StreamExt}; -use protocol::Message; +use sandd_protocol::Message; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; @@ -148,7 +148,7 @@ async fn connect_and_serve( let (mut ws_tx, mut ws_rx) = ws_stream.split(); // Gather system metadata - let metadata = protocol::DaemonMetadata { + let metadata = sandd_protocol::DaemonMetadata { hostname: System::host_name().unwrap_or_else(|| "unknown".to_string()), platform: std::env::consts::OS.to_string(), arch: std::env::consts::ARCH.to_string(), diff --git a/sandd/src/protocol.rs b/sandd/src/protocol.rs deleted file mode 100644 index b49c41b..0000000 --- a/sandd/src/protocol.rs +++ /dev/null @@ -1,202 +0,0 @@ -// Re-export the protocol from server crate for consistency -// In production, you'd want a shared protocol crate - -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum Message { - Register { - daemon_id: String, - metadata: DaemonMetadata, - }, - RegisterAck { - success: bool, - message: String, - }, - Heartbeat, - Pong, - ExecuteCommand { - request_id: String, - command: String, - #[serde(default = "default_timeout")] - timeout_secs: u64, - #[serde(default)] - env: std::collections::HashMap, - #[serde(default)] - cwd: Option, - }, - CommandOutput { - request_id: String, - stdout: String, - stderr: String, - exit_code: i32, - duration_ms: u64, - }, - CommandError { - request_id: String, - error: String, - }, - NewSession { - session_id: String, - rows: u16, - cols: u16, - #[serde(default = "default_term")] - term: String, - }, - SessionStarted { - session_id: String, - success: bool, - error: Option, - }, - SessionInput { - session_id: String, - #[serde(with = "base64_bytes")] - data: Vec, - }, - SessionOutput { - session_id: String, - #[serde(with = "base64_bytes")] - data: Vec, - }, - SessionResize { - session_id: String, - rows: u16, - cols: u16, - }, - SessionClose { - session_id: String, - }, - SessionExit { - session_id: String, - exit_code: i32, - }, - FileUploadStart { - request_id: String, - path: String, - total_size: u64, - #[serde(default)] - mode: Option, - }, - FileUploadChunk { - request_id: String, - #[serde(with = "base64_bytes")] - data: Vec, - offset: u64, - }, - FileUploadComplete { - request_id: String, - success: bool, - error: Option, - }, - FileDownloadStart { - request_id: String, - path: String, - }, - FileDownloadChunk { - request_id: String, - #[serde(with = "base64_bytes")] - data: Vec, - offset: u64, - is_last: bool, - }, - FileDownloadError { - request_id: String, - error: String, - }, - // Snapshot operations - CreateSnapshot { - request_id: String, - workspace: String, - message: Option, - tags: Option>, - }, - SnapshotCreated { - request_id: String, - snapshot_id: String, - file_count: usize, - total_size: u64, - }, - RestoreSnapshot { - request_id: String, - snapshot_id: String, - destination: String, - }, - SnapshotRestored { - request_id: String, - file_count: usize, - }, - ListSnapshots { - request_id: String, - tags: Option>, - }, - SnapshotList { - request_id: String, - snapshots: Vec, - }, - FindSnapshotByTag { - request_id: String, - tag: String, - }, - GetSnapshot { - request_id: String, - snapshot_id: String, - }, - SnapshotDetails { - request_id: String, - snapshot: Option, - }, - DeleteSnapshot { - request_id: String, - snapshot_id: String, - }, - SnapshotDeleted { - request_id: String, - }, - SnapshotError { - request_id: String, - error: String, - }, - Error { - message: String, - #[serde(default)] - recoverable: bool, - }, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DaemonMetadata { - pub hostname: String, - pub platform: String, - pub arch: String, - #[serde(default)] - pub version: String, - #[serde(default)] - pub labels: std::collections::HashMap, -} - -fn default_timeout() -> u64 { - 300 -} - -fn default_term() -> String { - "xterm-256color".to_string() -} - -mod base64_bytes { - use serde::{Deserialize, Deserializer, Serializer}; - - pub fn serialize(v: &Vec, s: S) -> Result { - use base64::Engine; - let base64 = base64::engine::general_purpose::STANDARD.encode(v); - s.serialize_str(&base64) - } - - pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { - use base64::Engine; - let base64 = String::deserialize(d)?; - base64::engine::general_purpose::STANDARD - .decode(base64.as_bytes()) - .map_err(serde::de::Error::custom) - } -} diff --git a/sandd/src/session.rs b/sandd/src/session.rs index ca267bf..7a24542 100644 --- a/sandd/src/session.rs +++ b/sandd/src/session.rs @@ -1,4 +1,4 @@ -use crate::protocol::Message; +use sandd_protocol::Message; use anyhow::{anyhow, Result}; use futures_util::SinkExt; use portable_pty::{native_pty_system, CommandBuilder, PtySize, PtySystem}; diff --git a/sandd/src/snapshot/manager.rs b/sandd/src/snapshot/manager.rs index 54d3908..01ab088 100644 --- a/sandd/src/snapshot/manager.rs +++ b/sandd/src/snapshot/manager.rs @@ -200,7 +200,7 @@ impl SnapshotManager { Ok(()) } - /// Restore tree recursively (always clean - deletes extras after successful restore) + /// Restore tree recursively fn restore_tree<'a>( &'a self, tree_hash: &'a str, @@ -209,19 +209,11 @@ impl SnapshotManager { Box::pin(async move { fs::create_dir_all(dest).await?; - // Load tree object - tells us what SHOULD exist + // Load tree object let tree_json = self.store.get_blob(tree_hash).await?; let tree: Tree = serde_json::from_slice(&tree_json)?; - // Build set of expected names in this directory (owned strings to avoid borrow issues) - let expected_names: std::collections::HashSet = tree - .entries - .iter() - .map(|e| e.name.clone()) - .collect(); - - // Phase 1: Restore each entry from snapshot - // Do this FIRST - if restore fails, extras remain untouched (safer) + // Restore each entry for entry in tree.entries { let entry_path = dest.join(&entry.name); @@ -289,29 +281,6 @@ impl SnapshotManager { } } - // Phase 2: Clean this directory - delete extras (only after successful restore) - // Cleanup failures are warned but don't fail the operation - let mut read_dir = fs::read_dir(dest).await?; - while let Some(entry) = read_dir.next_entry().await? { - let name = entry.file_name(); - let name_str = name.to_string_lossy().to_string(); - - if !expected_names.contains(&name_str) { - let path = entry.path(); - - // Not in snapshot - delete it - if path.is_dir() { - if let Err(e) = fs::remove_dir_all(&path).await { - tracing::warn!("Failed to delete directory {}: {}", path.display(), e); - } - } else { - if let Err(e) = fs::remove_file(&path).await { - tracing::warn!("Failed to delete file {}: {}", path.display(), e); - } - } - } - } - Ok(()) }) } @@ -1220,69 +1189,4 @@ mod tests { assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("does not exist")); } - - #[tokio::test] - async fn test_restore_always_clean() { - let temp_dir = TempDir::new().unwrap(); - let store_dir = temp_dir.path().join("store"); - let workspace = temp_dir.path().join("workspace"); - let restore_dir = temp_dir.path().join("restored"); - - // Create snapshot with specific files - fs::create_dir_all(&workspace).await.unwrap(); - fs::write(workspace.join("file1.txt"), "content1") - .await - .unwrap(); - fs::create_dir_all(workspace.join("dir1")).await.unwrap(); - fs::write(workspace.join("dir1/file2.txt"), "content2") - .await - .unwrap(); - - let manager = SnapshotManager::new(store_dir).unwrap(); - let snapshot_id = manager - .create_snapshot(&workspace, Some("Clean test".to_string()), None) - .await - .unwrap(); - - // Restore to directory with extra files - fs::create_dir_all(&restore_dir).await.unwrap(); - fs::write(restore_dir.join("extra_file.txt"), "should be deleted") - .await - .unwrap(); - fs::create_dir_all(restore_dir.join("extra_dir")) - .await - .unwrap(); - fs::write(restore_dir.join("extra_dir/nested.txt"), "also deleted") - .await - .unwrap(); - fs::create_dir_all(restore_dir.join("dir1")).await.unwrap(); - fs::write(restore_dir.join("dir1/extra_in_dir.txt"), "delete me") - .await - .unwrap(); - - // Restore snapshot (should clean extras) - manager - .restore_snapshot(&snapshot_id, &restore_dir) - .await - .unwrap(); - - // Verify exact match - only snapshot files exist - assert!(restore_dir.join("file1.txt").exists()); - assert!(restore_dir.join("dir1/file2.txt").exists()); - - // Verify extras are deleted - assert!(!restore_dir.join("extra_file.txt").exists()); - assert!(!restore_dir.join("extra_dir").exists()); - assert!(!restore_dir.join("dir1/extra_in_dir.txt").exists()); - - // Verify content is correct - let content1 = fs::read_to_string(restore_dir.join("file1.txt")) - .await - .unwrap(); - assert_eq!(content1, "content1"); - let content2 = fs::read_to_string(restore_dir.join("dir1/file2.txt")) - .await - .unwrap(); - assert_eq!(content2, "content2"); - } } diff --git a/sandd/src/snapshot/types.rs b/sandd/src/snapshot/types.rs index 27697f9..1ae727b 100644 --- a/sandd/src/snapshot/types.rs +++ b/sandd/src/snapshot/types.rs @@ -1,12 +1,15 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; +// Re-export SnapshotInfo from shared protocol +pub use sandd_protocol::SnapshotInfo; + pub type SnapshotId = String; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Snapshot { pub id: SnapshotId, - pub created_at: u64, // Unix timestamp in seconds + pub created_at: u64, // Unix timestamp in seconds pub tree: String, pub message: String, pub tags: Vec, @@ -15,16 +18,6 @@ pub struct Snapshot { pub total_size: u64, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SnapshotInfo { - pub id: SnapshotId, - pub created_at: u64, // Unix timestamp in seconds - pub message: String, - pub tags: Vec, - pub file_count: usize, - pub total_size: u64, -} - impl From for SnapshotInfo { fn from(snapshot: Snapshot) -> Self { Self { diff --git a/server/Cargo.toml b/server/Cargo.toml index bad910b..1183871 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -12,6 +12,7 @@ name = "sandbox_server" crate-type = ["cdylib", "rlib"] [dependencies] +sandd-protocol = { path = "../protocol" } tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/server/src/lib.rs b/server/src/lib.rs index 0abf94b..d788f31 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -2,7 +2,7 @@ #![allow(dead_code)] #![allow(non_local_definitions)] -mod protocol; +// Use shared protocol crate mod registry; mod server; @@ -18,7 +18,7 @@ use tokio::sync::oneshot; use tracing_subscriber; use uuid::Uuid; -use protocol::Message; +use sandd_protocol::Message; use registry::DaemonRegistry; use server::SandboxServer; diff --git a/server/src/registry.rs b/server/src/registry.rs index fd8cda2..dd28977 100644 --- a/server/src/registry.rs +++ b/server/src/registry.rs @@ -1,4 +1,4 @@ -use crate::protocol::{DaemonMetadata, Message}; +use sandd_protocol::{DaemonMetadata, Message}; use anyhow::{anyhow, Result}; use dashmap::DashMap; use std::sync::atomic::{AtomicU64, Ordering}; diff --git a/server/src/server.rs b/server/src/server.rs index 324145b..38c765f 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -1,4 +1,4 @@ -use crate::protocol::Message; +use sandd_protocol::Message; use crate::registry::{DaemonConnection, DaemonRegistry}; use anyhow::{Context, Result}; use axum::{ From ec846d19767a419f4952bbebbbb8548f910990dc Mon Sep 17 00:00:00 2001 From: kerthcet Date: Wed, 1 Jul 2026 18:05:01 +0100 Subject: [PATCH 3/3] add protocol to dockerfile Signed-off-by: kerthcet --- hack/docker/Dockerfile.alpine | 1 + hack/docker/Dockerfile.debian | 1 + hack/docker/Dockerfile.rocky | 1 + 3 files changed, 3 insertions(+) diff --git a/hack/docker/Dockerfile.alpine b/hack/docker/Dockerfile.alpine index 3fe6294..3ba2f6b 100644 --- a/hack/docker/Dockerfile.alpine +++ b/hack/docker/Dockerfile.alpine @@ -13,6 +13,7 @@ RUN apk add --no-cache \ # Copy workspace files COPY Cargo.toml Cargo.lock ./ +COPY protocol/ ./protocol/ COPY sandd/ ./sandd/ COPY server/ ./server/ diff --git a/hack/docker/Dockerfile.debian b/hack/docker/Dockerfile.debian index 2328b06..a9a98f4 100644 --- a/hack/docker/Dockerfile.debian +++ b/hack/docker/Dockerfile.debian @@ -12,6 +12,7 @@ RUN apt-get update && apt-get install -y \ # Copy workspace files COPY Cargo.toml Cargo.lock ./ +COPY protocol/ ./protocol/ COPY sandd/ ./sandd/ COPY server/ ./server/ diff --git a/hack/docker/Dockerfile.rocky b/hack/docker/Dockerfile.rocky index 8545554..1ce9003 100644 --- a/hack/docker/Dockerfile.rocky +++ b/hack/docker/Dockerfile.rocky @@ -12,6 +12,7 @@ RUN apt-get update && apt-get install -y \ # Copy workspace files COPY Cargo.toml Cargo.lock ./ +COPY protocol/ ./protocol/ COPY sandd/ ./sandd/ COPY server/ ./server/