summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/archive.rs48
-rw-r--r--src/main.rs16
2 files changed, 63 insertions, 1 deletions
diff --git a/src/archive.rs b/src/archive.rs
index e7e6bc5..03ec643 100644
--- a/src/archive.rs
+++ b/src/archive.rs
@@ -103,6 +103,25 @@ fn snapshot(project: &Project, records: &[Value], stage: &Path) -> Result<()> {
resume
}
+fn snapshot_chat(root: &Path, stage: &Path) -> Result<()> {
+ let source = root.join("var/data/chat/chat.sqlite");
+ if !source.is_file() {
+ return Ok(());
+ }
+ let directory = stage.join("var/data/chat");
+ files::private_dir(&directory)?;
+ let database =
+ rusqlite::Connection::open_with_flags(source, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)?;
+ database.busy_timeout(std::time::Duration::from_secs(5))?;
+ let destination = directory.join("chat.sqlite");
+ database.backup("main", &destination, None::<fn(rusqlite::backup::Progress)>)?;
+ fs::set_permissions(
+ destination,
+ std::os::unix::fs::PermissionsExt::from_mode(0o600),
+ )?;
+ Ok(())
+}
+
fn snapshot_files(project: &Project, records: &[Value], stage: &Path) -> Result<()> {
files::copy_source(&project.root, stage)?;
files::copy_tree(&project.root.join(".env"), &stage.join(".env"))?;
@@ -112,13 +131,14 @@ fn snapshot_files(project: &Project, records: &[Value], stage: &Path) -> Result<
let data = project.root.join("var/data");
for item in fs::read_dir(&data)? {
let entry = item?;
- if entry.file_name() != "caddy" {
+ if entry.file_name() != "caddy" && entry.file_name() != "chat" {
files::copy_tree(
&entry.path(),
&stage.join("var/data").join(entry.file_name()),
)?;
}
}
+ snapshot_chat(&project.root, stage)?;
let caddy = records
.iter()
.find(|record| {
@@ -365,6 +385,32 @@ mod tests {
use crate::project::{Mode, Project};
#[test]
+ fn chat_snapshot_is_consistent_and_private() -> Result<()> {
+ let workspace = tempfile::tempdir()?;
+ let root = workspace.path().join("source");
+ let stage = workspace.path().join("snapshot");
+ files::private_dir(&root.join("var/data/chat"))?;
+ let connection = rusqlite::Connection::open(root.join("var/data/chat/chat.sqlite"))?;
+ connection.execute_batch(
+ "CREATE TABLE messages (body TEXT); INSERT INTO messages VALUES ('ciphertext-before');",
+ )?;
+ let transaction = connection.unchecked_transaction()?;
+ transaction.execute("INSERT INTO messages VALUES ('uncommitted')", [])?;
+ super::snapshot_chat(&root, &stage)?;
+ let snapshot = stage.join("var/data/chat/chat.sqlite");
+ let restored = rusqlite::Connection::open(&snapshot)?;
+ let count: i64 =
+ restored.query_row("SELECT COUNT(*) FROM messages", [], |row| row.get(0))?;
+ assert_eq!(count, 1);
+ assert_eq!(
+ std::os::unix::fs::PermissionsExt::mode(&fs::metadata(snapshot)?.permissions()) & 0o777,
+ 0o600
+ );
+ transaction.rollback()?;
+ Ok(())
+ }
+
+ #[test]
fn restore_is_local_and_refuses_an_existing_destination() -> Result<()> {
let workspace = tempfile::tempdir()?;
let source = workspace.path().join("source");
diff --git a/src/main.rs b/src/main.rs
index beba5b0..08c88f5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -49,6 +49,11 @@ struct Cli {
#[derive(Debug, Subcommand)]
enum Action {
+ #[command(about = "Run the experimental local chat or initialize its single .env")]
+ Chat {
+ #[arg(long)]
+ init: bool,
+ },
#[command(about = "Start the local development services")]
Dev {
#[arg(long, default_value_t = 8080)]
@@ -127,6 +132,16 @@ fn execute(arguments: Cli) -> Result<()> {
return archive::restore(&absolute(archive)?, &absolute(destination)?);
}
let project = Project::discover(arguments.root.as_deref())?;
+ if let Action::Chat { init } = arguments.command {
+ let mut command = project.command("cargo")?;
+ command
+ .args(["run", "--locked", "-p", "stack-chat-server", "--", "--root"])
+ .arg(&project.root);
+ if init {
+ command.arg("--init");
+ }
+ return process::run(&mut command);
+ }
let _operation = project.lock()?;
match arguments.command {
Action::Dev { port } => {
@@ -186,6 +201,7 @@ fn execute(arguments: Cli) -> Result<()> {
Action::Activate { archive } => release::activate(&absolute(&archive)?, &project),
Action::Check => check(&project),
Action::Licenses => licenses(&project),
+ Action::Chat { .. } => anyhow::bail!("Chat action was not handled"),
Action::Restore { .. } => anyhow::bail!("Restore action was not handled"),
}
}