Skip to content
Clone
8f30db6
815 lines 27.6 KB Rust
use std::collections::{HashMap, HashSet};
use models::dir::dirs::{self, DirKind};
use models::dir::{dir_overwrite_group, dir_overwrite_users};
use models::space::{space_groups, space_member_groups};
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, Set,
TransactionTrait,
};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
use crate::authority;
use crate::errors::SpaceError;
use crate::services::dto::UserDto;
use crate::services::service::SpaceService;
use crate::share::authz::{
MANAGE_CHANNELS, MANAGE_OVERWRITES, effective_permissions, require_perm,
};
use crate::share::lookup;
impl From<dir::Error> for SpaceError {
fn from(error: dir::Error) -> Self {
match error {
dir::Error::InvalidInput(message) => Self::BadRequest(message),
dir::Error::NotFound => Self::NotFound,
dir::Error::Database(error) => Self::from(error),
}
}
}
pub const MAX_DIR_NODE_NAME: usize = 255;
pub const PERM_DIR_VISIBLE: i64 = 1 << 0;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct DirNodeDto {
pub id: Uuid,
pub name: String,
pub kind: DirKind,
pub parent_dir_id: Option<Uuid>,
pub color: Option<String>,
pub position: i64,
pub icon: Option<String>,
pub topic: Option<String>,
pub purpose: Option<String>,
pub is_archived: bool,
pub archived_at: Option<OffsetDateTime>,
pub created_at: OffsetDateTime,
pub updated_at: OffsetDateTime,
pub created_by_id: i64,
#[schema(no_recursion)]
pub children: Vec<DirNodeDto>,
}
impl From<dirs::Model> for DirNodeDto {
fn from(m: dirs::Model) -> Self {
Self {
id: m.id,
name: m.name,
kind: m.kind,
parent_dir_id: m.parent_dir_id,
color: m.color,
position: m.position,
icon: m.icon,
topic: m.topic,
purpose: m.purpose,
is_archived: m.is_archived,
archived_at: m.archived_at,
created_at: m.created_at,
updated_at: m.updated_at,
created_by_id: m.created_by_uid,
children: Vec::new(),
}
}
}
impl From<dir::DirDto> for DirNodeDto {
fn from(dto: dir::DirDto) -> Self {
Self {
id: dto.id,
name: dto.name,
kind: dto.kind,
parent_dir_id: dto.parent_dir_id,
color: dto.color,
position: dto.position,
icon: dto.icon,
topic: dto.topic,
purpose: dto.purpose,
is_archived: dto.is_archived,
archived_at: dto.archived_at,
created_at: dto.created_at,
updated_at: dto.updated_at,
created_by_id: dto.created_by_uid,
children: Vec::new(),
}
}
}
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct CreateDirCmd {
pub name: String,
#[serde(default)]
pub parent_dir_id: Option<Uuid>,
#[serde(default)]
pub position: Option<i64>,
}
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct UpdateDirCmd {
pub name: String,
}
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct MoveDirCmd {
#[serde(default, deserialize_with = "export::serde_patch::deserialize_opt_opt")]
pub parent_dir_id: Option<Option<Uuid>>,
#[serde(default)]
pub position: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct RepairDirResp {
pub scanned: i64,
pub fixed: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct GroupOverwriteDto {
pub dir_id: Uuid,
pub group_id: Uuid,
pub allow_mask: i64,
pub deny_mask: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UserOverwriteDto {
pub dir_id: Uuid,
pub user_id: i64,
pub user: Option<UserDto>,
pub allow_mask: i64,
pub deny_mask: i64,
}
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct SetOverwriteCmd {
pub allow_mask: i64,
pub deny_mask: i64,
}
#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
pub struct DirPath {
pub space_name: String,
pub dir_id: Uuid,
}
impl SpaceService {
async fn dir_in_space(&self, space_id: Uuid, dir_id: Uuid) -> Result<dirs::Model, SpaceError> {
dirs::Entity::find_by_id(dir_id)
.filter(dirs::Column::SpaceId.eq(space_id))
.one(self.access.database.read())
.await?
.ok_or(SpaceError::NotFound)
}
#[tracing::instrument(name = "space.create_dir_node", skip_all, fields(caller = %caller, space = %space_name))]
pub async fn create_dir_node(
&self,
caller: i64,
space_name: &str,
cmd: CreateDirCmd,
) -> Result<DirNodeDto, SpaceError> {
let space = lookup::lookup_space_by_name(self, space_name).await?;
require_perm(self, &space, caller, MANAGE_CHANNELS).await?;
let name = cmd.name.trim().to_string();
if name.is_empty() || name.chars().count() > MAX_DIR_NODE_NAME {
return Err(SpaceError::BadRequest(
"dir name is empty or too long".to_string(),
));
}
let created = self
.access
.database
.write()
.transaction::<_, dir::DirDto, SpaceError>(move |tx| {
Box::pin(async move {
Ok(dir::create_folder(
tx,
caller,
dir::NewDir {
space_id: space.id,
kind: DirKind::Dir,
name,
parent_dir_id: cmd.parent_dir_id,
position: cmd.position,
topic: None,
purpose: None,
icon: None,
color: None,
},
)
.await?)
})
})
.await?;
Ok(DirNodeDto::from(created))
}
pub async fn get_space_dir_tree(
&self,
caller: i64,
space_name: &str,
parent_id: Option<Uuid>,
) -> Result<Vec<DirNodeDto>, SpaceError> {
let space = lookup::lookup_space_by_name(self, space_name).await?;
let perms = effective_permissions(self, &space, caller).await?;
let can_manage = perms.has(MANAGE_CHANNELS);
if parent_id.is_none() {
let total_nodes = dirs::Entity::find()
.filter(dirs::Column::SpaceId.eq(space.id))
.count(self.access.database.read())
.await? as u64;
let max_nodes = self.policy.settings.space.max_dir_nodes;
if total_nodes > max_nodes {
return Err(SpaceError::BadRequest(format!(
"directory tree has {total_nodes} nodes, exceeds max_dir_nodes {max_nodes}; query with parent_id to load one level"
)));
}
}
let mut sel = dirs::Entity::find()
.filter(dirs::Column::SpaceId.eq(space.id))
.order_by_asc(dirs::Column::Position);
if let Some(pid) = parent_id {
sel = sel.filter(dirs::Column::ParentDirId.eq(Some(pid)));
}
let nodes = sel.all(self.access.database.read()).await?;
if nodes.is_empty() {
return Ok(Vec::new());
}
let node_ids: Vec<Uuid> = nodes.iter().map(|n| n.id).collect();
let group_ows = dir_overwrite_group::Entity::find()
.filter(dir_overwrite_group::Column::DirId.is_in(node_ids.clone()))
.all(self.access.database.read())
.await?;
let user_ows = dir_overwrite_users::Entity::find()
.filter(dir_overwrite_users::Column::DirId.is_in(node_ids.clone()))
.all(self.access.database.read())
.await?;
let my_member_groups = space_member_groups::Entity::find()
.filter(space_member_groups::Column::SpaceId.eq(space.id))
.filter(space_member_groups::Column::UserId.eq(caller))
.all(self.access.database.read())
.await?;
let my_group_ids: HashSet<Uuid> =
my_member_groups.into_iter().map(|mg| mg.group_id).collect();
let mut group_ow_map: HashMap<Uuid, Vec<dir_overwrite_group::Model>> = HashMap::new();
for go in group_ows {
group_ow_map.entry(go.dir_id).or_default().push(go);
}
let now = OffsetDateTime::now_utc();
let mut user_ow_map: HashMap<Uuid, Vec<dir_overwrite_users::Model>> = HashMap::new();
for uo in user_ows {
if !crate::share::dir_visibility::user_overwrite_active(&uo, now) {
continue;
}
user_ow_map.entry(uo.dir_id).or_default().push(uo);
}
let mut direct_visible: HashMap<Uuid, bool> = HashMap::new();
let mut parent_map: HashMap<Uuid, Option<Uuid>> = HashMap::new();
let mut node_map: HashMap<Uuid, dirs::Model> = HashMap::new();
for n in &nodes {
parent_map.insert(n.id, n.parent_dir_id);
node_map.insert(n.id, n.clone());
if can_manage {
direct_visible.insert(n.id, true);
continue;
}
let group_ows = group_ow_map.get(&n.id).map(Vec::as_slice).unwrap_or(&[]);
let user_ows = user_ow_map.get(&n.id).map(Vec::as_slice).unwrap_or(&[]);
direct_visible.insert(
n.id,
crate::share::dir_visibility::node_visible(
caller,
&my_group_ids,
group_ows,
user_ows,
),
);
}
fn is_truly_visible(
id: Uuid,
parent_map: &HashMap<Uuid, Option<Uuid>>,
direct_visible: &HashMap<Uuid, bool>,
memo: &mut HashMap<Uuid, bool>,
visiting: &mut HashSet<Uuid>,
) -> bool {
if let Some(&vis) = memo.get(&id) {
return vis;
}
if visiting.contains(&id) {
return false;
}
if !direct_visible.get(&id).copied().unwrap_or(false) {
memo.insert(id, false);
return false;
}
visiting.insert(id);
let parent_opt = parent_map.get(&id).copied().flatten();
let parent_vis = match parent_opt {
Some(pid) if parent_map.contains_key(&pid) => {
is_truly_visible(pid, parent_map, direct_visible, memo, visiting)
}
_ => true,
};
visiting.remove(&id);
memo.insert(id, parent_vis);
parent_vis
}
let mut memo = HashMap::new();
let mut truly_visible_set = HashSet::new();
for &nid in &node_ids {
let mut visiting = HashSet::new();
if is_truly_visible(nid, &parent_map, &direct_visible, &mut memo, &mut visiting) {
truly_visible_set.insert(nid);
}
}
let mut children_map: HashMap<Option<Uuid>, Vec<DirNodeDto>> = HashMap::new();
for n in nodes {
if !truly_visible_set.contains(&n.id) {
continue;
}
let parent_id = match n.parent_dir_id {
Some(p) if truly_visible_set.contains(&p) => Some(p),
_ => None,
};
children_map
.entry(parent_id)
.or_default()
.push(DirNodeDto::from(n));
}
fn assemble_tree(
parent_id: Option<Uuid>,
children_map: &mut HashMap<Option<Uuid>, Vec<DirNodeDto>>,
) -> Vec<DirNodeDto> {
let mut list = children_map.remove(&parent_id).unwrap_or_default();
list.sort_by(|a, b| {
a.position
.cmp(&b.position)
.then_with(|| a.created_at.cmp(&b.created_at))
});
for item in &mut list {
item.children = assemble_tree(Some(item.id), children_map);
}
list
}
let forest = assemble_tree(None, &mut children_map);
Ok(forest)
}
#[tracing::instrument(name = "space.update_dir_node", skip_all, fields(caller = %caller, space = %space_name, dir_id = %dir_id))]
pub async fn update_dir_node(
&self,
caller: i64,
space_name: &str,
dir_id: Uuid,
cmd: UpdateDirCmd,
) -> Result<DirNodeDto, SpaceError> {
let space = lookup::lookup_space_by_name(self, space_name).await?;
require_perm(self, &space, caller, MANAGE_CHANNELS).await?;
let node: dirs::Model = self.dir_in_space(space.id, dir_id).await?;
if node.kind.is_object() {
return Err(SpaceError::BadRequest(
"object dirs are managed by their owning domain".to_string(),
));
}
let name = cmd.name.trim();
if name.is_empty() || name.chars().count() > MAX_DIR_NODE_NAME {
return Err(SpaceError::BadRequest(
"dir name is empty or too long".to_string(),
));
}
let now = OffsetDateTime::now_utc();
let mut am: dirs::ActiveModel = node.into();
am.name = Set(name.to_string());
am.updated_at = Set(now);
let updated = am.update(self.access.database.write()).await?;
Ok(DirNodeDto::from(updated))
}
pub async fn move_dir_node(
&self,
caller: i64,
space_name: &str,
dir_id: Uuid,
cmd: MoveDirCmd,
) -> Result<DirNodeDto, SpaceError> {
let space = lookup::lookup_space_by_name(self, space_name).await?;
require_perm(self, &space, caller, MANAGE_CHANNELS).await?;
let node = self.dir_in_space(space.id, dir_id).await?;
if node.kind.is_object() {
return Err(SpaceError::BadRequest(
"object dirs are managed by their owning domain".to_string(),
));
}
let new_parent_id = match cmd.parent_dir_id {
None => node.parent_dir_id,
Some(None) => None,
Some(Some(p)) => {
if p == dir_id {
return Err(SpaceError::BadRequest(
"cannot set parent to self".to_string(),
));
}
let parent_node = self.dir_in_space(space.id, p).await?;
if parent_node.kind != DirKind::Dir {
return Err(SpaceError::BadRequest(
"parent node must be a dir".to_string(),
));
}
let mut curr = parent_node.parent_dir_id;
while let Some(curr_p) = curr {
if curr_p == dir_id {
return Err(SpaceError::BadRequest(
"cannot move node into its own descendant".to_string(),
));
}
let next = dirs::Entity::find_by_id(curr_p)
.filter(dirs::Column::SpaceId.eq(space.id))
.one(self.access.database.read())
.await?;
curr = next.and_then(|n| n.parent_dir_id);
}
Some(p)
}
};
let new_position = match cmd.position {
Some(pos) => {
if pos < 0 {
return Err(SpaceError::BadRequest(
"position must be non-negative".to_string(),
));
}
pos
}
None if new_parent_id != node.parent_dir_id => {
let max_node = dirs::Entity::find()
.filter(dirs::Column::SpaceId.eq(space.id))
.filter(dirs::Column::ParentDirId.eq(new_parent_id))
.order_by_desc(dirs::Column::Position)
.one(self.access.database.read())
.await?;
max_node.map(|n| n.position + 1).unwrap_or(0)
}
None => node.position,
};
let now = OffsetDateTime::now_utc();
let mut am: dirs::ActiveModel = node.into();
am.parent_dir_id = Set(new_parent_id);
am.position = Set(new_position);
am.updated_at = Set(now);
let updated = am.update(self.access.database.write()).await?;
Ok(DirNodeDto::from(updated))
}
pub async fn repair_dir_parents(
&self,
caller: i64,
space_name: &str,
) -> Result<RepairDirResp, SpaceError> {
let space = lookup::lookup_space_by_name(self, space_name).await?;
if space.owner_user_id != caller {
return Err(SpaceError::Forbidden);
}
let nodes = dirs::Entity::find()
.filter(dirs::Column::SpaceId.eq(space.id))
.all(self.access.database.read())
.await?;
let scanned = nodes.len() as i64;
let mut parent_map: HashMap<Uuid, Option<Uuid>> = HashMap::new();
for n in &nodes {
parent_map.insert(n.id, n.parent_dir_id);
}
let mut fixed_nodes: HashSet<Uuid> = HashSet::new();
for n in &nodes {
if let Some(p) = n.parent_dir_id
&& (p == n.id || !parent_map.contains_key(&p))
{
fixed_nodes.insert(n.id);
parent_map.insert(n.id, None);
}
}
let mut state: HashMap<Uuid, u8> = HashMap::new();
let keys: Vec<Uuid> = parent_map.keys().copied().collect();
for nid in keys {
if state.get(&nid).copied().unwrap_or(0) != 0 {
continue;
}
let mut curr = Some(nid);
let mut path = Vec::new();
while let Some(c) = curr {
let st = state.get(&c).copied().unwrap_or(0);
if st == 1 {
fixed_nodes.insert(c);
parent_map.insert(c, None);
break;
} else if st == 2 {
break;
}
state.insert(c, 1);
path.push(c);
curr = parent_map.get(&c).copied().flatten();
}
for p in path {
state.insert(p, 2);
}
}
let now = OffsetDateTime::now_utc();
let fixed = fixed_nodes.len() as i64;
if !fixed_nodes.is_empty() {
self.access
.database
.write()
.transaction::<_, (), SpaceError>(|txn| {
Box::pin(async move {
for fid in fixed_nodes {
let node = dirs::Entity::find_by_id(fid).one(txn).await?;
if let Some(n) = node {
let mut am: dirs::ActiveModel = n.into();
am.parent_dir_id = Set(None);
am.updated_at = Set(now);
am.update(txn).await?;
}
}
Ok(())
})
})
.await?;
}
Ok(RepairDirResp { scanned, fixed })
}
pub async fn list_dir_group_overwrites(
&self,
caller: i64,
space_name: &str,
dir_id: Uuid,
) -> Result<Vec<GroupOverwriteDto>, SpaceError> {
let space = lookup::lookup_space_by_name(self, space_name).await?;
require_perm(self, &space, caller, MANAGE_OVERWRITES).await?;
self.dir_in_space(space.id, dir_id).await?;
let rows = dir_overwrite_group::Entity::find()
.filter(dir_overwrite_group::Column::DirId.eq(dir_id))
.all(self.access.database.read())
.await?;
Ok(rows
.into_iter()
.map(|r| GroupOverwriteDto {
dir_id: r.dir_id,
group_id: r.group_id,
allow_mask: r.allow_mask,
deny_mask: r.deny_mask,
})
.collect())
}
#[tracing::instrument(name = "space.set_dir_group_overwrite", skip_all, fields(caller = %caller, space = %space_name, dir_id = %dir_id, group_id = %group_id))]
pub async fn set_dir_group_overwrite(
&self,
caller: i64,
space_name: &str,
dir_id: Uuid,
group_id: Uuid,
cmd: SetOverwriteCmd,
) -> Result<GroupOverwriteDto, SpaceError> {
let space = lookup::lookup_space_by_name(self, space_name).await?;
require_perm(self, &space, caller, MANAGE_OVERWRITES).await?;
self.dir_in_space(space.id, dir_id).await?;
space_groups::Entity::find_by_id(group_id)
.filter(space_groups::Column::SpaceId.eq(space.id))
.one(self.access.database.read())
.await?
.ok_or(SpaceError::NotFound)?;
if cmd.allow_mask < 0 || cmd.deny_mask < 0 {
return Err(SpaceError::BadRequest(
"masks must be non-negative".to_string(),
));
}
let existing = dir_overwrite_group::Entity::find_by_id((dir_id, group_id))
.one(self.access.database.read())
.await?;
if let Some(row) = existing {
let mut am: dir_overwrite_group::ActiveModel = row.into();
am.allow_mask = Set(cmd.allow_mask);
am.deny_mask = Set(cmd.deny_mask);
let updated = am.update(self.access.database.write()).await?;
Ok(GroupOverwriteDto {
dir_id: updated.dir_id,
group_id: updated.group_id,
allow_mask: updated.allow_mask,
deny_mask: updated.deny_mask,
})
} else {
let am = dir_overwrite_group::ActiveModel {
dir_id: Set(dir_id),
group_id: Set(group_id),
allow_mask: Set(cmd.allow_mask),
deny_mask: Set(cmd.deny_mask),
};
let inserted = am.insert(self.access.database.write()).await?;
Ok(GroupOverwriteDto {
dir_id: inserted.dir_id,
group_id: inserted.group_id,
allow_mask: inserted.allow_mask,
deny_mask: inserted.deny_mask,
})
}
}
#[tracing::instrument(name = "space.remove_dir_group_overwrite", skip_all, fields(caller = %caller, space = %space_name, dir_id = %dir_id, group_id = %group_id))]
pub async fn remove_dir_group_overwrite(
&self,
caller: i64,
space_name: &str,
dir_id: Uuid,
group_id: Uuid,
) -> Result<(), SpaceError> {
let space = lookup::lookup_space_by_name(self, space_name).await?;
require_perm(self, &space, caller, MANAGE_OVERWRITES).await?;
self.dir_in_space(space.id, dir_id).await?;
let res = dir_overwrite_group::Entity::delete_many()
.filter(dir_overwrite_group::Column::DirId.eq(dir_id))
.filter(dir_overwrite_group::Column::GroupId.eq(group_id))
.exec(self.access.database.write())
.await?;
if res.rows_affected == 0 {
return Err(SpaceError::NotFound);
}
Ok(())
}
pub async fn list_dir_user_overwrites(
&self,
caller: i64,
space_name: &str,
dir_id: Uuid,
) -> Result<Vec<UserOverwriteDto>, SpaceError> {
let space = lookup::lookup_space_by_name(self, space_name).await?;
require_perm(self, &space, caller, MANAGE_OVERWRITES).await?;
self.dir_in_space(space.id, dir_id).await?;
let rows = dir_overwrite_users::Entity::find()
.filter(dir_overwrite_users::Column::DirId.eq(dir_id))
.all(self.access.database.read())
.await?;
let user_ids: Vec<i64> = rows.iter().map(|r| r.user_id).collect();
let user_map: HashMap<i64, UserDto> = self.load_user_briefs(&user_ids).await?;
Ok(rows
.into_iter()
.map(|r| {
let user_brief = user_map.get(&r.user_id).cloned();
UserOverwriteDto {
dir_id: r.dir_id,
user_id: r.user_id,
user: user_brief,
allow_mask: r.allow_mask,
deny_mask: r.deny_mask,
}
})
.collect())
}
#[tracing::instrument(name = "space.set_dir_user_overwrite", skip_all, fields(caller = %caller, space = %space_name, dir_id = %dir_id, user_id = %user_id))]
pub async fn set_dir_user_overwrite(
&self,
caller: i64,
space_name: &str,
dir_id: Uuid,
user_id: i64,
cmd: SetOverwriteCmd,
) -> Result<UserOverwriteDto, SpaceError> {
let space = lookup::lookup_space_by_name(self, space_name).await?;
require_perm(self, &space, caller, MANAGE_OVERWRITES).await?;
self.dir_in_space(space.id, dir_id).await?;
if space.owner_user_id != user_id {
authority::require_member(self, space.id, user_id)
.await
.map_err(|_| {
SpaceError::BadRequest(
"target user is not an active member of this space".to_string(),
)
})?;
}
if cmd.allow_mask < 0 || cmd.deny_mask < 0 {
return Err(SpaceError::BadRequest(
"masks must be non-negative".to_string(),
));
}
let existing = dir_overwrite_users::Entity::find_by_id((dir_id, user_id))
.one(self.access.database.read())
.await?;
let (final_allow, final_deny) = if let Some(row) = existing {
let mut am: dir_overwrite_users::ActiveModel = row.into();
am.allow_mask = Set(cmd.allow_mask);
am.deny_mask = Set(cmd.deny_mask);
let updated = am.update(self.access.database.write()).await?;
(updated.allow_mask, updated.deny_mask)
} else {
let am = dir_overwrite_users::ActiveModel {
dir_id: Set(dir_id),
user_id: Set(user_id),
allow_mask: Set(cmd.allow_mask),
deny_mask: Set(cmd.deny_mask),
expires_at: Set(None),
};
let inserted = am.insert(self.access.database.write()).await?;
(inserted.allow_mask, inserted.deny_mask)
};
let user_map: HashMap<i64, UserDto> = self.load_user_briefs(&[user_id]).await?;
let user_brief = user_map.get(&user_id).cloned();
Ok(UserOverwriteDto {
dir_id,
user_id,
user: user_brief,
allow_mask: final_allow,
deny_mask: final_deny,
})
}
#[tracing::instrument(name = "space.remove_dir_user_overwrite", skip_all, fields(caller = %caller, space = %space_name, dir_id = %dir_id, user_id = %user_id))]
pub async fn remove_dir_user_overwrite(
&self,
caller: i64,
space_name: &str,
dir_id: Uuid,
user_id: i64,
) -> Result<(), SpaceError> {
let space = lookup::lookup_space_by_name(self, space_name).await?;
require_perm(self, &space, caller, MANAGE_OVERWRITES).await?;
self.dir_in_space(space.id, dir_id).await?;
let res = dir_overwrite_users::Entity::delete_many()
.filter(dir_overwrite_users::Column::DirId.eq(dir_id))
.filter(dir_overwrite_users::Column::UserId.eq(user_id))
.exec(self.access.database.write())
.await?;
if res.rows_affected == 0 {
return Err(SpaceError::NotFound);
}
Ok(())
}
}