Skip to content

Commit e32775f

Browse files
authored
api/cli: add update users command (#150)
1 parent 231a239 commit e32775f

File tree

5 files changed

+216
-3
lines changed

5 files changed

+216
-3
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# Unreleased
2+
3+
## Added
4+
5+
- `re update users` for bulk user permission updates
6+
17
# v0.12.1
28

39
## Added

api/src/lib.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ use crate::resources::{
5252
user::{
5353
CreateRequest as CreateUserRequest, CreateResponse as CreateUserResponse,
5454
GetAvailableResponse as GetAvailableUsersResponse,
55-
GetCurrentResponse as GetCurrentUserResponse,
55+
GetCurrentResponse as GetCurrentUserResponse, PostUserRequest, PostUserResponse,
5656
},
5757
EmptySuccess, Response,
5858
};
@@ -98,7 +98,7 @@ pub use crate::{
9898
},
9999
user::{
100100
Email, GlobalPermission, Id as UserId, Identifier as UserIdentifier,
101-
ModifiedPermissions, NewUser, ProjectPermission, User, Username,
101+
ModifiedPermissions, NewUser, ProjectPermission, UpdateUser, User, Username,
102102
},
103103
},
104104
};
@@ -350,6 +350,14 @@ impl Client {
350350
)
351351
}
352352

353+
pub fn post_user(&self, user_id: &UserId, user: UpdateUser) -> Result<PostUserResponse> {
354+
self.post(
355+
self.endpoints.post_user(user_id)?,
356+
PostUserRequest { user: &user },
357+
Retry::Yes,
358+
)
359+
}
360+
353361
pub fn put_comment_audio(
354362
&self,
355363
source_id: &SourceId,
@@ -1260,6 +1268,15 @@ impl Endpoints {
12601268
})
12611269
}
12621270

1271+
fn post_user(&self, user_id: &UserId) -> Result<Url> {
1272+
self.base
1273+
.join(&format!("/api/_private/users/{}", user_id.0))
1274+
.map_err(|source| Error::UrlParseError {
1275+
source,
1276+
message: format!("Could not build post user URL for user `{}`.", user_id.0,),
1277+
})
1278+
}
1279+
12631280
fn dataset_by_id(&self, dataset_id: &DatasetId) -> Result<Url> {
12641281
self.base
12651282
.join(&format!("/api/v1/datasets/id:{}", dataset_id.0))

api/src/resources/user.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,25 @@ impl FromStr for GlobalPermission {
265265
}
266266
}
267267

268+
#[derive(Debug, Clone, Serialize, PartialEq, Eq, Deserialize)]
269+
pub struct UpdateUser {
270+
#[serde(skip_serializing_if = "Option::is_none")]
271+
pub organisation_permissions: Option<HashMap<ProjectName, Vec<ProjectPermission>>>,
272+
273+
#[serde(skip_serializing_if = "Option::is_none")]
274+
pub global_permissions: Option<Vec<GlobalPermission>>,
275+
}
276+
277+
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
278+
pub(crate) struct PostUserRequest<'request> {
279+
pub user: &'request UpdateUser,
280+
}
281+
282+
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
283+
pub struct PostUserResponse {
284+
pub user: User,
285+
}
286+
268287
#[cfg(test)]
269288
mod tests {
270289
use super::*;

cli/src/commands/update/mod.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
mod dataset;
22
mod project;
33
mod source;
4+
mod users;
45

5-
use self::{dataset::UpdateDatasetArgs, project::UpdateProjectArgs, source::UpdateSourceArgs};
6+
use self::{
7+
dataset::UpdateDatasetArgs, project::UpdateProjectArgs, source::UpdateSourceArgs,
8+
users::UpdateUsersArgs,
9+
};
610
use crate::printer::Printer;
711
use anyhow::Result;
812
use reinfer_client::Client;
@@ -21,12 +25,17 @@ pub enum UpdateArgs {
2125
#[structopt(name = "project")]
2226
/// Update an existing project
2327
Project(UpdateProjectArgs),
28+
29+
#[structopt(name = "users")]
30+
/// Update existing users
31+
Users(UpdateUsersArgs),
2432
}
2533

2634
pub fn run(update_args: &UpdateArgs, client: Client, printer: &Printer) -> Result<()> {
2735
match update_args {
2836
UpdateArgs::Source(source_args) => source::update(&client, source_args, printer),
2937
UpdateArgs::Dataset(dataset_args) => dataset::update(&client, dataset_args, printer),
3038
UpdateArgs::Project(project_args) => project::update(&client, project_args, printer),
39+
UpdateArgs::Users(users_args) => users::update(&client, users_args),
3140
}
3241
}

cli/src/commands/update/users.rs

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
use anyhow::{Context, Result};
2+
use colored::Colorize;
3+
use log::info;
4+
use reinfer_client::{Client, UpdateUser, UserId};
5+
use std::{
6+
fs::{self, File},
7+
io::{self, BufRead, BufReader},
8+
path::PathBuf,
9+
sync::{
10+
atomic::{AtomicUsize, Ordering},
11+
Arc,
12+
},
13+
};
14+
use structopt::StructOpt;
15+
16+
use crate::progress::{Options as ProgressOptions, Progress};
17+
18+
#[derive(Debug, StructOpt)]
19+
pub struct UpdateUsersArgs {
20+
#[structopt(short = "f", long = "file", parse(from_os_str))]
21+
/// Path to JSON file with users. If not specified, stdin will be used.
22+
input_file: Option<PathBuf>,
23+
24+
#[structopt(long)]
25+
/// Don't display a progress bar (only applicable when --file is used).
26+
no_progress: bool,
27+
}
28+
29+
pub fn update(client: &Client, args: &UpdateUsersArgs) -> Result<()> {
30+
let statistics = match &args.input_file {
31+
Some(input_file) => {
32+
info!("Processing users from file `{}`", input_file.display(),);
33+
let file_metadata = fs::metadata(&input_file).with_context(|| {
34+
format!("Could not get file metadata for `{}`", input_file.display())
35+
})?;
36+
let file = BufReader::new(
37+
File::open(input_file)
38+
.with_context(|| format!("Could not open file `{}`", input_file.display()))?,
39+
);
40+
let statistics = Arc::new(Statistics::new());
41+
let progress = if args.no_progress {
42+
None
43+
} else {
44+
Some(progress_bar(file_metadata.len(), &statistics))
45+
};
46+
update_users_from_reader(client, file, &statistics)?;
47+
if let Some(mut progress) = progress {
48+
progress.done();
49+
}
50+
Arc::try_unwrap(statistics).unwrap()
51+
}
52+
None => {
53+
info!("Processing users from stdin",);
54+
let statistics = Statistics::new();
55+
update_users_from_reader(client, BufReader::new(io::stdin()), &statistics)?;
56+
statistics
57+
}
58+
};
59+
60+
info!(
61+
concat!("Successfully updated {} users",),
62+
statistics.num_updated(),
63+
);
64+
65+
Ok(())
66+
}
67+
68+
use serde::{self, Deserialize, Serialize};
69+
#[derive(Serialize, Deserialize)]
70+
struct UserLine {
71+
id: UserId,
72+
73+
#[serde(flatten)]
74+
update: UpdateUser,
75+
}
76+
77+
fn update_users_from_reader(
78+
client: &Client,
79+
mut users: impl BufRead,
80+
statistics: &Statistics,
81+
) -> Result<()> {
82+
let mut line_number = 1;
83+
let mut line = String::new();
84+
85+
loop {
86+
line.clear();
87+
let bytes_read = users
88+
.read_line(&mut line)
89+
.with_context(|| format!("Could not read line {} from input stream", line_number))?;
90+
91+
if bytes_read == 0 {
92+
return Ok(());
93+
}
94+
95+
statistics.add_bytes_read(bytes_read);
96+
let user_line = serde_json::from_str::<UserLine>(line.trim_end()).with_context(|| {
97+
format!(
98+
"Could not parse user at line {} from input stream",
99+
line_number,
100+
)
101+
})?;
102+
103+
// Upload users
104+
client
105+
.post_user(&user_line.id, user_line.update)
106+
.context("Could not update user")?;
107+
statistics.add_user();
108+
109+
line_number += 1;
110+
}
111+
}
112+
113+
#[derive(Debug)]
114+
pub struct Statistics {
115+
bytes_read: AtomicUsize,
116+
updated: AtomicUsize,
117+
}
118+
119+
impl Statistics {
120+
fn new() -> Self {
121+
Self {
122+
bytes_read: AtomicUsize::new(0),
123+
updated: AtomicUsize::new(0),
124+
}
125+
}
126+
127+
#[inline]
128+
fn add_bytes_read(&self, bytes_read: usize) {
129+
self.bytes_read.fetch_add(bytes_read, Ordering::SeqCst);
130+
}
131+
132+
#[inline]
133+
fn add_user(&self) {
134+
self.updated.fetch_add(1, Ordering::SeqCst);
135+
}
136+
137+
#[inline]
138+
fn bytes_read(&self) -> usize {
139+
self.bytes_read.load(Ordering::SeqCst)
140+
}
141+
142+
#[inline]
143+
fn num_updated(&self) -> usize {
144+
self.updated.load(Ordering::SeqCst)
145+
}
146+
}
147+
148+
fn progress_bar(total_bytes: u64, statistics: &Arc<Statistics>) -> Progress {
149+
Progress::new(
150+
move |statistics| {
151+
let bytes_read = statistics.bytes_read();
152+
let num_updated = statistics.num_updated();
153+
(
154+
bytes_read as u64,
155+
format!("{} {}", num_updated.to_string().bold(), "users".dimmed()),
156+
)
157+
},
158+
statistics,
159+
Some(total_bytes),
160+
ProgressOptions { bytes_units: true },
161+
)
162+
}

0 commit comments

Comments
 (0)