multi file work

This commit is contained in:
Hamcha 2023-11-29 17:29:19 +01:00
parent 447194252a
commit 91e4b5dee8
8 changed files with 114 additions and 46 deletions

View file

@ -1,3 +0,0 @@
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-Clink-arg=-fuse-ld=mold"]

View file

@ -91,13 +91,13 @@ impl ThreadSafeRepository {
Ok(oid) Ok(oid)
} }
pub fn commit_files(&self, paths: &[&Path], message: &str) -> Result<()> { pub fn commit_files<P: AsRef<Path>>(&self, paths: &[P], message: &str) -> Result<()> {
let repository = self.repository()?; let repository = self.repository()?;
// Commit file // Commit file
let mut index = repository.index()?; let mut index = repository.index()?;
for path in paths { for path in paths {
index.add_path(path)?; index.add_path(path.as_ref())?;
} }
self.commit(repository, index, message)?; self.commit(repository, index, message)?;

View file

@ -1,3 +1,5 @@
use std::collections::HashMap;
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
http::StatusCode, http::StatusCode,
@ -8,13 +10,13 @@ use serde::Deserialize;
use crate::{ use crate::{
http::error::{AppError, Result}, http::error::{AppError, Result},
stack::{arion, compose}, stack::{self, arion, compose, FileList, COMPOSE_FILE},
AppState, AppState,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
pub(super) struct EditStackForm { pub(super) struct EditStackForm {
source: String, files: HashMap<String, String>,
commit_message: String, commit_message: String,
} }
@ -29,21 +31,21 @@ pub(super) async fn edit_stack(
form.commit_message form.commit_message
}; };
if form.source.trim().is_empty() { if form.files.is_empty() {
return Err(AppError::Client { return Err(AppError::Client {
status: StatusCode::BAD_REQUEST, status: StatusCode::BAD_REQUEST,
code: "invalid-source", code: "invalid-files",
message: "provided stack source is empty".to_string(), message: "no files provided".to_string(),
}); });
}; }
// Cleanup source (like line endings) // Cleanup source (like line endings) in each file
let source = form.source.replace("\r\n", "\n"); let mut files = FileList::new();
for (name, source) in form.files {
files.insert(name, source.replace("\r\n", "\n"));
}
// Make sure file is ok edit_stack_int(state, &stack_name, &files, &commit_message).await?;
compose::check(&state.arion_bin, &source).await?;
edit_stack_int(state, &stack_name, &source, &commit_message).await?;
Ok(Redirect::to("./")) Ok(Redirect::to("./"))
} }
@ -51,17 +53,20 @@ pub(super) async fn edit_stack(
pub(super) async fn edit_stack_int( pub(super) async fn edit_stack_int(
state: AppState, state: AppState,
stack_name: &str, stack_name: &str,
source: &str, files: &FileList,
commit_message: &str, commit_message: &str,
) -> Result<()> { ) -> Result<()> {
// Get compose
if let Some(compose_file) = files.get(COMPOSE_FILE) {
// Make sure file is ok // Make sure file is ok
compose::check(&state.arion_bin, &source).await?; compose::check(&state.arion_bin, compose_file).await?;
}
// Write compose file // Write compose file
compose::write(&state.stack_dir, stack_name, source).await?; stack::write(&state.stack_dir, stack_name, files).await?;
// Git commit // Git commit
compose::commit(state.repository, stack_name, commit_message)?; compose::commit(state.repository, stack_name, files, commit_message)?;
// Update stack // Update stack
arion::command( arion::command(

View file

@ -1,3 +1,5 @@
use std::collections::HashMap;
use askama::Template; use askama::Template;
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
@ -63,10 +65,13 @@ pub(super) async fn restore(
let source = state.repository.get_file_at_commit(&path, &form.oid)?; let source = state.repository.get_file_at_commit(&path, &form.oid)?;
let short_id = form.oid[..6].to_string(); let short_id = form.oid[..6].to_string();
let mut files = HashMap::new();
files.insert(path.to_string_lossy().into_owned(), source);
edit_stack_int( edit_stack_int(
state, state,
&stack_name, &stack_name,
&source, &files,
format!("Revert to {}", short_id).as_str(), format!("Revert to {}", short_id).as_str(),
) )
.await?; .await?;

View file

@ -1,3 +1,4 @@
use anyhow::anyhow;
use askama::Template; use askama::Template;
use axum::extract::{Path, State}; use axum::extract::{Path, State};
use serde_json::json; use serde_json::json;
@ -6,14 +7,15 @@ use crate::{
http::response::{reply, HandlerResponse}, http::response::{reply, HandlerResponse},
node::container::ContainerInfo, node::container::ContainerInfo,
node::nix::parse_arion_compose, node::nix::parse_arion_compose,
stack, AppState, stack::{self, FileList, COMPOSE_FILE},
AppState,
}; };
#[derive(Template)] #[derive(Template)]
#[template(path = "stack/get-one.html")] #[template(path = "stack/get-one.html")]
struct GetOneTemplate { struct GetOneTemplate {
stack_name: String, stack_name: String,
file_contents: String, files: FileList,
containers: Vec<ContainerInfo>, containers: Vec<ContainerInfo>,
} }
@ -21,20 +23,24 @@ pub(super) async fn get_one(
Path(stack_name): Path<String>, Path(stack_name): Path<String>,
State(state): State<AppState>, State(state): State<AppState>,
) -> HandlerResponse { ) -> HandlerResponse {
let file_contents = stack::compose::get(&state.stack_dir, &stack_name).await?; let files = stack::get(&state.stack_dir, &stack_name).await?;
let info = parse_arion_compose(&file_contents)?; let compose_file = files
.get(COMPOSE_FILE)
.ok_or(anyhow!("compose file not found in stack"))?;
let info = parse_arion_compose(compose_file)?;
let containers = stack::list::containers(&state.docker, &info.project).await?; let containers = stack::list::containers(&state.docker, &info.project).await?;
reply( reply(
json!({ json!({
"folder": stack_name, "folder": stack_name,
"name": info.project, "name": info.project,
"file": file_contents, "files": files,
"containers": containers, "containers": containers,
}), }),
GetOneTemplate { GetOneTemplate {
stack_name: info.project, stack_name: info.project,
file_contents, files,
containers: containers.iter().map(|c| c.clone().into()).collect(), containers: containers.iter().map(|c| c.clone().into()).collect(),
}, },
) )

View file

@ -11,7 +11,7 @@ use crate::{
node::{error::StackError, nix::parse_arion_compose}, node::{error::StackError, nix::parse_arion_compose},
}; };
use super::{arion, utils, COMPOSE_FILE, PACKAGE_CONTENTS, PACKAGE_FILE}; use super::{arion, utils, FileList, COMPOSE_FILE, PACKAGE_CONTENTS, PACKAGE_FILE};
pub async fn get(base_dir: &Path, stack_name: &str) -> Result<String> { pub async fn get(base_dir: &Path, stack_name: &str) -> Result<String> {
let dir = base_dir.join(stack_name); let dir = base_dir.join(stack_name);
@ -23,18 +23,19 @@ pub async fn get(base_dir: &Path, stack_name: &str) -> Result<String> {
Ok(contents) Ok(contents)
} }
pub async fn write(base_dir: &Path, stack_name: &str, contents: &str) -> Result<()> { pub fn commit(
let dir = base_dir.join(stack_name); repository: ThreadSafeRepository,
if !utils::is_stack(&dir).await? { stack_name: &str,
return Err(StackError::NotFound.into()); files: &FileList,
} message: &str,
) -> Result<()> {
Ok(fs::write(dir.join(COMPOSE_FILE), contents).await?) repository.commit_files(
} &files
.keys()
pub fn commit(repository: ThreadSafeRepository, stack_name: &str, message: &str) -> Result<()> { .map(|key| PathBuf::from(format!("{}/{}", stack_name, key)))
let compose_path = path(stack_name); .collect::<Vec<_>>(),
repository.commit_files(&[&compose_path], message)?; message,
)?;
Ok(()) Ok(())
} }

View file

@ -1,6 +1,10 @@
use anyhow::Result;
use futures_util::future::join_all;
use serde::Serialize; use serde::Serialize;
use std::{collections::HashMap, path::Path};
use tokio::fs;
use crate::node::container::ContainerInfo; use crate::node::{container::ContainerInfo, error::StackError};
pub mod arion; pub mod arion;
pub mod compose; pub mod compose;
@ -30,7 +34,57 @@ pub struct StackInfo {
pub services: Vec<ServiceInfo>, pub services: Vec<ServiceInfo>,
} }
const COMPOSE_FILE: &str = "arion-compose.nix"; pub const COMPOSE_FILE: &str = "arion-compose.nix";
const PACKAGE_FILE: &str = "arion-pkgs.nix"; pub const PACKAGE_FILE: &str = "arion-pkgs.nix";
const PACKAGE_CONTENTS: &str = r#"import <nixpkgs> { system = "x86_64-linux"; } const PACKAGE_CONTENTS: &str = r#"import <nixpkgs> { system = "x86_64-linux"; }
"#; "#;
pub type FileList = HashMap<String, String>;
pub async fn get(base_dir: &Path, stack_name: &str) -> Result<FileList> {
let dir = base_dir.join(stack_name);
if !utils::is_stack(&dir).await? {
return Err(StackError::NotFound.into());
}
let mut files = vec![];
let mut dirs = fs::read_dir(&dir).await?;
while let Some(entry) = dirs.next_entry().await? {
let meta = entry.metadata().await?;
if meta.is_dir() {
continue;
};
let path = dir.join(entry.path());
files.push(tokio::spawn(async move {
fs::read_to_string(path)
.await
.map(|content| (entry.file_name().to_string_lossy().into_owned(), content))
}));
}
Ok(join_all(files)
.await
.into_iter()
.collect::<Result<Result<_, _>, _>>()??)
}
pub async fn write(base_dir: &Path, stack_name: &str, files: &FileList) -> Result<()> {
let dir = base_dir.join(stack_name);
if !utils::is_stack(&dir).await? {
return Err(StackError::NotFound.into());
}
join_all(
files
.iter()
.map(|(name, content)| (dir.join(name), content))
.map(|(path, content)| async move { fs::write(path, content).await }),
)
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()?;
Ok(())
}

View file

@ -62,7 +62,7 @@
</h2> </h2>
<form method="POST" action="./edit" id="editor-form"> <form method="POST" action="./edit" id="editor-form">
<div class="error"></div> <div class="error"></div>
<textarea name="source" id="editor">{{file_contents}}</textarea> <textarea name="source" id="editor">{{files["arion-compose.nix"]}}</textarea>
<div class="row"> <div class="row">
<input style="flex:1" name="commit_message" type="text" <input style="flex:1" name="commit_message" type="text"
placeholder="What did you change?" /> placeholder="What did you change?" />