Skip to content

Commit

Permalink
refactor: extract env file handling (#32)
Browse files Browse the repository at this point in the history
### Description
Move env file handling to own module so that we can better test its
behaviour and have it contained.

### Note
We need the service name on the env file handling just so that we can
use that while building the `CliError`. It feels that maybe we can
return another type of error by the env handlers and leave the
translation into a `CliError` as a responsibility of the caller. But
maybe we can do that on another PR.
  • Loading branch information
augustoccesar authored Oct 13, 2023
1 parent bc48b4b commit b7b41e7
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 104 deletions.
282 changes: 282 additions & 0 deletions linkup-cli/src/env_files.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
use std::io::Write;
use std::{
fs::{self, OpenOptions},
path::PathBuf,
};

use crate::{CliError, Result};

const LINKUP_ENV_SEPARATOR: &str = "##### Linkup environment - DO NOT EDIT #####";

pub fn write_to_env_file(service: &str, dev_env_path: &PathBuf, env_path: &PathBuf) -> Result<()> {
if let Ok(env_content) = fs::read_to_string(env_path) {
if env_content.contains(LINKUP_ENV_SEPARATOR) {
return Ok(());
}
}

let mut dev_env_content = fs::read_to_string(dev_env_path).map_err(|e| {
CliError::SetServiceEnv(
service.to_string(),
format!("could not read dev env file: {}", e),
)
})?;

if dev_env_content.ends_with('\n') {
dev_env_content.pop();
}

let mut env_file = OpenOptions::new()
.create(true)
.append(true)
.open(env_path)
.map_err(|e| {
CliError::SetServiceEnv(
service.to_string(),
format!("Failed to open .env file: {}", e),
)
})?;

let content = vec![
format!("\n{}", LINKUP_ENV_SEPARATOR),
format!("\n{}", dev_env_content),
format!("\n{}", LINKUP_ENV_SEPARATOR),
];

writeln!(env_file, "{}", content.concat()).map_err(|e| {
CliError::SetServiceEnv(
service.to_string(),
format!("could not write to env file: {}", e),
)
})?;

Ok(())
}

pub fn clear_env_file(service: &str, env_path: &PathBuf) -> Result<()> {
let mut file_content = fs::read_to_string(env_path).map_err(|e| {
CliError::RemoveServiceEnv(
service.to_string(),
format!("could not read dev env file: {}", e),
)
})?;

let start_idx = file_content.find(LINKUP_ENV_SEPARATOR);
let end_idx = file_content.rfind(LINKUP_ENV_SEPARATOR);

if let (Some(mut start), Some(mut end)) = (start_idx, end_idx) {
if start < end {
let new_line_above_start =
start > 0 && file_content.chars().nth(start - 1) == Some('\n');
let new_line_bellow_end = file_content.chars().nth(end + 1) == Some('\n');

if new_line_above_start {
start -= 1;
}

if new_line_bellow_end {
end += 1;
}

file_content.drain(start..=end + LINKUP_ENV_SEPARATOR.len());
}

if file_content.ends_with('\n') {
file_content.pop();
}

// Write the updated content back to the file
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.open(env_path)
.map_err(|e| {
CliError::RemoveServiceEnv(
service.to_string(),
format!("Failed to open .env file for writing: {}", e),
)
})?;
file.write_all(file_content.as_bytes()).map_err(|e| {
CliError::RemoveServiceEnv(
service.to_string(),
format!("Failed to write .env file: {}", e),
)
})?;
}

Ok(())
}

#[cfg(test)]
mod test {
use rand::distributions::Alphanumeric;
use rand::Rng;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::PathBuf;

use crate::env_files::write_to_env_file;

use super::clear_env_file;

#[test]
fn write_to_env_file_empty_target() {
let source = TestFile::create("SOURCE_1=VALUE_1");
let target = TestFile::create("");

write_to_env_file("service_1", &source.path, &target.path).unwrap();

let file_content = fs::read_to_string(&target.path).unwrap();
let expected_content = format!(
"\n{}\n{}\n{}\n",
"##### Linkup environment - DO NOT EDIT #####",
"SOURCE_1=VALUE_1",
"##### Linkup environment - DO NOT EDIT #####",
);

assert_eq!(file_content, expected_content);
}

#[test]
fn write_to_env_file_filled_target() {
let source = TestFile::create("SOURCE_1=VALUE_1");
let target = TestFile::create("EXISTING_1=VALUE_1\nEXISTING_2=VALUE_2");

write_to_env_file("service_1", &source.path, &target.path).unwrap();

let file_content = fs::read_to_string(&target.path).unwrap();
let expected_content = format!(
"{}\n{}\n{}\n{}\n{}\n",
"EXISTING_1=VALUE_1",
"EXISTING_2=VALUE_2",
"##### Linkup environment - DO NOT EDIT #####",
"SOURCE_1=VALUE_1",
"##### Linkup environment - DO NOT EDIT #####",
);

assert_eq!(file_content, expected_content);
}

#[test]
fn clear_env_file_only_linkup_env() {
let content = format!(
"\n{}\n{}\n{}\n",
"##### Linkup environment - DO NOT EDIT #####",
"SOURCE_1=VALUE_1",
"##### Linkup environment - DO NOT EDIT #####",
);
let env_file = TestFile::create(&content);

clear_env_file("service_1", &env_file.path).unwrap();

let file_content = fs::read_to_string(&env_file.path).unwrap();
assert_eq!("", file_content);
}

#[test]
fn clear_env_file_existing_env_before_linkup() {
let content = format!(
"{}\n{}\n{}\n\n{}\n{}\n",
"EXISTING_1=VALUE_1",
"EXISTING_2=VALUE_2",
"##### Linkup environment - DO NOT EDIT #####",
"SOURCE_1=VALUE_1",
"##### Linkup environment - DO NOT EDIT #####",
);
let env_file = TestFile::create(&content);

clear_env_file("service_1", &env_file.path).unwrap();

let file_content = fs::read_to_string(&env_file.path).unwrap();
let expected_content = format!("{}\n{}", "EXISTING_1=VALUE_1", "EXISTING_2=VALUE_2",);
assert_eq!(expected_content, file_content);
}

#[test]
fn clear_env_file_existing_env_before_and_after_linkup() {
let content = format!(
"{}\n{}\n{}\n\n{}\n{}\n\n{}\n{}",
"EXISTING_1=VALUE_1",
"EXISTING_2=VALUE_2",
"##### Linkup environment - DO NOT EDIT #####",
"SOURCE_1=VALUE_1",
"##### Linkup environment - DO NOT EDIT #####",
"EXISTING_3=VALUE_3",
"EXISTING_4=VALUE_4",
);
let env_file = TestFile::create(&content);

clear_env_file("service_1", &env_file.path).unwrap();

let file_content = fs::read_to_string(&env_file.path).unwrap();
let expected_content = format!(
"{}\n{}\n{}\n{}",
"EXISTING_1=VALUE_1", "EXISTING_2=VALUE_2", "EXISTING_3=VALUE_3", "EXISTING_4=VALUE_4",
);
assert_eq!(expected_content, file_content);
}

#[test]
fn write_and_clear() {
let source = TestFile::create("SOURCE_1=VALUE_1\nSOURCE_2=VALUE_2");
let target = TestFile::create("EXISTING_1=VALUE_1\nEXISTING_2=VALUE_2");

write_to_env_file("service_1", &source.path, &target.path).unwrap();

// Check post write content
let file_content = fs::read_to_string(&target.path).unwrap();
let expected_content = format!(
"{}\n{}\n{}\n{}\n{}\n{}\n",
"EXISTING_1=VALUE_1",
"EXISTING_2=VALUE_2",
"##### Linkup environment - DO NOT EDIT #####",
"SOURCE_1=VALUE_1",
"SOURCE_2=VALUE_2",
"##### Linkup environment - DO NOT EDIT #####",
);
assert_eq!(file_content, expected_content);

clear_env_file("service_1", &target.path).unwrap();

// Check post clear content
let file_content = fs::read_to_string(&target.path).unwrap();
let expected_content = format!("{}\n{}", "EXISTING_1=VALUE_1", "EXISTING_2=VALUE_2",);
assert_eq!(file_content, expected_content);
}

struct TestFile {
pub path: PathBuf,
}

impl TestFile {
fn create(content: &str) -> Self {
let file_name: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(12)
.map(char::from)
.collect();

let mut test_file = std::env::temp_dir();
test_file.push(file_name);

let mut env_file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&test_file)
.unwrap();

write!(&mut env_file, "{}", content).unwrap();

Self { path: test_file }
}
}

impl Drop for TestFile {
fn drop(&mut self) {
if let Err(err) = std::fs::remove_file(&self.path) {
println!("failed to remove file {}: {}", &self.path.display(), err);
}
}
}
}
2 changes: 1 addition & 1 deletion linkup-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod background_booting;
mod background_local_server;
mod background_tunnel;
mod completion;
mod env_files;
mod local_config;
mod local_dns;
mod local_server;
Expand All @@ -32,7 +33,6 @@ const LINKUP_DIR: &str = ".linkup";
const LINKUP_STATE_FILE: &str = "state";
const LINKUP_LOCALSERVER_PID_FILE: &str = "localserver-pid";
const LINKUP_CLOUDFLARED_PID: &str = "cloudflared-pid";
const LINKUP_ENV_SEPARATOR: &str = "##### Linkup environment - DO NOT EDIT #####";
const LINKUP_LOCALDNS_INSTALL: &str = "localdns-install";
const LINKUP_CF_TLS_API_ENV_VAR: &str = "LINKUP_CF_API_TOKEN";

Expand Down
55 changes: 4 additions & 51 deletions linkup-cli/src/start.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
use std::io::Write;
use std::{
fs::{self, File, OpenOptions},
fs::{self, File},
path::{Path, PathBuf},
};

use crate::env_files::write_to_env_file;
use crate::local_config::{config_path, get_config};
use crate::{
background_booting::boot_background_services,
linkup_file_path,
local_config::{config_to_state, LocalState, YamlLocalConfig},
status::{server_status, ServerStatus},
CliError, LINKUP_ENV_SEPARATOR, LINKUP_STATE_FILE,
CliError, LINKUP_STATE_FILE,
};
use crate::{services, LINKUP_LOCALDNS_INSTALL};

Expand Down Expand Up @@ -123,54 +123,7 @@ fn set_service_env(directory: String, config_path: String) -> Result<(), CliErro
let env_path =
PathBuf::from(dev_env_path.parent().unwrap()).join(dev_env_path.file_stem().unwrap());

if let Ok(env_content) = fs::read_to_string(&env_path) {
if env_content.contains(LINKUP_ENV_SEPARATOR) {
continue;
}
}

let mut dev_env_content = fs::read_to_string(&dev_env_path).map_err(|e| {
CliError::SetServiceEnv(
directory.clone(),
format!("could not read dev env file: {}", e),
)
})?;

if dev_env_content.ends_with('\n') {
dev_env_content.pop();
}

let mut env_file = OpenOptions::new()
.create(true)
.append(true)
.open(&env_path)
.map_err(|e| {
CliError::SetServiceEnv(
directory.clone(),
format!("Failed to open .env file: {}", e),
)
})?;

writeln!(env_file, "\n{}", LINKUP_ENV_SEPARATOR).map_err(|e| {
CliError::SetServiceEnv(
directory.clone(),
format!("could not write to env file: {}", e),
)
})?;

writeln!(env_file, "{}", dev_env_content).map_err(|e| {
CliError::SetServiceEnv(
directory.clone(),
format!("could not write to env file: {}", e),
)
})?;

writeln!(env_file, "{}", LINKUP_ENV_SEPARATOR).map_err(|e| {
CliError::SetServiceEnv(
directory.clone(),
format!("could not write to env file: {}", e),
)
})?;
write_to_env_file(&directory, &dev_env_path, &env_path)?;
}

Ok(())
Expand Down
Loading

0 comments on commit b7b41e7

Please sign in to comment.