Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

features/0.6.0 #18

Merged
merged 7 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ dccmd-rs upload /your/path your.dracoon.domain/public/upload-shares/someLongAcce

**Note**: This essentially means you need to copy the created share link

#### Upload options

When uploading, the default resolution strategy is *autorename* - this means that if a file `foo.pdf` uploaded and already present, it is automatically renamed by DRACOON (e.g. to `foo (1).pdf`).

In order to change this behavior, you can the pass the following flags / options:
- *--overwrite* - a file with the same name will be overwritten (essentially creating versions of the same file)
- *--keep-share-links* - if *--overwrite* is used, you can additionally keep existing (download) share links for file(s)

### Listing nodes
To list nodes, use the `ls` command:

Expand Down Expand Up @@ -219,6 +227,33 @@ dccmd-rs users info your.dracoon.domain/ --user-id 2
dccmd-rs users info your.dracoon.domain/ --user-name foo # short: -u
```


### Managing groups

To list groups, you can use the `groups ls some.dracoon.domain.com` command:

```bash
# optional flags: --all (lists all groups, default: 500, paging) --csv (csv format)
# optional flags: --search (by username)
dccmd-rs groups ls your.dracoon.domain/
dccmd-rs groups ls your.dracoon.domain/ --csv --all > grouplist.csv
dccmd-rs groups ls your.dracoon.domain/ --search foo
```

To create groups, you can use the `groups create some.dracoon.domain.com` command:

```bash
# params: --name
dccmd-rs groups create your.dracoon.domain/ --name foo
```

To delete groups, you can use the `groups some.dracoon.domain.com rm` command:

```bash
# supported: group id, group name
dccmd-rs groups rm your.dracoon.domain/ --group-id 2
dccmd-rs groups rm your.dracoon.domain/ --group-name foo

### Config

#### Stored authorization
Expand Down
167 changes: 167 additions & 0 deletions src/cmd/groups/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
use std::sync::Arc;

use console::Term;
use dco3::{
auth::Connected,
groups::{CreateGroupRequest, Group, GroupsFilter},
Dracoon, Groups, ListAllParams,
};

use futures_util::{stream, StreamExt};
use tokio::sync::Mutex;
use tracing::error;

mod models;
mod print;

use super::{
init_dracoon,
models::{DcCmdError, GroupCommand},
users::UserCommandHandler,
utils::strings::format_success_message,
};

pub struct GroupCommandHandler {
client: Dracoon<Connected>,
term: Term,
}

impl GroupCommandHandler {
pub async fn try_new(target_domain: String, term: Term) -> Result<Self, DcCmdError> {
let client = init_dracoon(&target_domain, None, false).await?;

Ok(Self { client, term })
}

async fn create_group(&self, name: String) -> Result<(), DcCmdError> {
let req = CreateGroupRequest::new(name, None);
let group = self.client.groups.create_group(req).await?;

let msg = format!("Group {} ({}) created", group.name, group.id);

self.term
.write_line(format_success_message(&msg).as_str())
.map_err(|_| DcCmdError::IoError)?;

Ok(())
}

async fn delete_group(&self, name: Option<String>, id: Option<u64>) -> Result<(), DcCmdError> {
let group_id = match (name, id) {
(_, Some(id)) => id,
(Some(name), _) => self.find_group_by_name(name).await?.id,
_ => {
return Err(DcCmdError::InvalidArgument(
"Either group name or id must be provided".to_string(),
))
}
};

self.client.groups.delete_group(group_id).await?;

let msg = format!("Group {} deleted", group_id);

self.term
.write_line(format_success_message(&msg).as_str())
.map_err(|_| DcCmdError::IoError)?;

Ok(())
}

async fn find_group_by_name(&self, name: String) -> Result<Group, DcCmdError> {
let params = ListAllParams::builder()
.with_filter(GroupsFilter::name_contains(&name))
.build();
let groups = self.client.groups.get_groups(Some(params)).await?;

let Some(group) = groups.items.iter().find(|g| g.name == name) else {
error!("No group found with name: {name}");
let msg = format!("No group found with name: {name}");
return Err(DcCmdError::InvalidArgument(msg));
};

Ok(group.clone())
}

async fn list_groups(
&self,
search: Option<String>,
offset: Option<u32>,
limit: Option<u32>,
all: bool,
csv: bool,
) -> Result<(), DcCmdError> {
let params = UserCommandHandler::build_params(
&search,
offset.unwrap_or(0).into(),
limit.unwrap_or(500).into(),
);

let groups = self.client.groups.get_groups(Some(params)).await?;

if all {
let total = groups.range.total;
let shared_results = Arc::new(Mutex::new(groups.clone()));

let reqs = (500..=total)
.step_by(500)
.map(|offset| {
let params = UserCommandHandler::build_params(&search, offset, 500);

self.client.groups.get_groups(Some(params))
})
.collect::<Vec<_>>();

stream::iter(reqs)
.for_each_concurrent(5, |f| {
let shared_results_clone = Arc::clone(&shared_results);
async move {
match f.await {
Ok(mut users) => {
let mut shared_results = shared_results_clone.lock().await;
shared_results.items.append(&mut users.items);
}
Err(e) => {
error!("Failed to fetch users: {}", e);
}
}
}
})
.await;

let results = shared_results.lock().await.clone();

self.print_groups(results, csv)?;
} else {
self.print_groups(groups, csv)?;
}

Ok(())
}
}

pub async fn handle_groups_cmd(cmd: GroupCommand, term: Term) -> Result<(), DcCmdError> {
let target = match &cmd {
GroupCommand::Create { target, .. }
| GroupCommand::Ls { target, .. }
| GroupCommand::Rm { target, .. } => target,
};

let handler = GroupCommandHandler::try_new(target.to_string(), term).await?;
match cmd {
GroupCommand::Create { target: _, name } => handler.create_group(name).await,
GroupCommand::Ls {
target: _,
search,
offset,
limit,
all,
csv,
} => handler.list_groups(search, offset, limit, all, csv).await,
GroupCommand::Rm {
group_name,
target: _,
group_id,
} => handler.delete_group(group_name, group_id).await,
}
}
24 changes: 24 additions & 0 deletions src/cmd/groups/models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use dco3::groups::Group;
use tabled::Tabled;

#[derive(Tabled)]
pub struct GroupInfo {
pub id: String,
pub name: String,
pub created_at: String,
pub cnt_users: u64,
#[tabled(display_with = "crate::cmd::users::display_option")]
pub updated_at: Option<String>,
}

impl From<Group> for GroupInfo {
fn from(group: Group) -> Self {
Self {
id: group.id.to_string(),
name: group.name,
cnt_users: group.cnt_users.unwrap_or(0),
created_at: group.created_at.to_string(),
updated_at: group.updated_at.map(|dt| dt.to_rfc3339()),
}
}
}
66 changes: 66 additions & 0 deletions src/cmd/groups/print.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use dco3::{groups::Group, RangedItems};
use tabled::{
settings::{object::Segment, Modify, Panel, Style, Width},
Table,
};

use crate::cmd::{
groups::models::GroupInfo,
models::{DcCmdError, PrintFormat},
};

use super::GroupCommandHandler;

impl GroupCommandHandler {
pub fn print_groups(&self, groups: RangedItems<Group>, csv: bool) -> Result<(), DcCmdError> {
let print_mode = if csv {
PrintFormat::Csv
} else {
PrintFormat::Pretty
};

match print_mode {
PrintFormat::Csv => {
let header = "id,name,cnt_users,created_at,updated_at";
self.term
.write_line(header)
.map_err(|_| DcCmdError::IoError)?;

for group in groups.items {
let updated_at = match group.updated_at {
Some(updated_at) => updated_at.to_rfc3339(),
None => "N/A".to_string(),
};
self.term
.write_line(&format!(
"{},{},{},{},{}",
group.id,
group.name,
group.cnt_users.unwrap_or(0),
group.created_at,
updated_at
))
.map_err(|_| DcCmdError::IoError)?;
}

Ok(())
}
PrintFormat::Pretty => {
let total = groups.range.total;
let groups: Vec<_> = groups.items.into_iter().map(GroupInfo::from).collect();
let displayed = groups.len();
let mut user_table = Table::new(groups);
user_table
.with(Panel::footer(
format!("{displayed} groups ({total} total)",),
))
.with(Style::modern())
.with(Modify::new(Segment::all()).with(Width::wrap(16)));

println!("{user_table}");

Ok(())
}
}
}
}
1 change: 1 addition & 0 deletions src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use dco3::{
};

pub mod config;
pub mod groups;
pub mod models;
pub mod nodes;
pub mod users;
Expand Down
59 changes: 59 additions & 0 deletions src/cmd/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ pub enum DcCmdCommand {
cmd: UserCommand,
},

/// Manage groups in DRACOON
Groups {
#[clap(subcommand)]
cmd: GroupCommand,
},

/// Configure DRACOON Commander
Config {
#[clap(subcommand)]
Expand Down Expand Up @@ -336,6 +342,59 @@ pub enum UserCommand {
},
}

#[derive(Parser)]
pub enum GroupCommand {
/// List groups in DRACOON
Ls {
/// DRACOON url
target: String,

/// search filter (group name)
#[clap(long)]
search: Option<String>,

/// skip n groups (default offset: 0)
#[clap(short, long)]
offset: Option<u32>,

/// limit n groups (default limit: 500)
#[clap(long)]
limit: Option<u32>,

/// fetch all groups (default: 500)
#[clap(long)]
all: bool,

/// print user information in CSV format
#[clap(long)]
csv: bool,
},

/// Create a group in DRACOON
Create {
/// DRACOON url
target: String,

/// Group name
#[clap(long, short)]
name: String,
},

/// delete a group in DRACOON
Rm {
/// DRACOON url
target: String,

/// Group name
#[clap(long, short)]
group_name: Option<String>,

/// Group id
#[clap(long)]
group_id: Option<u64>,
},
}

#[derive(Parser)]
pub enum ConfigCommand {
/// Manage DRACOON Commander auth credentials (refresh token)
Expand Down
Loading
Loading