remote_projects.rs

  1use anyhow::anyhow;
  2use rpc::{proto, ConnectionId};
  3use sea_orm::{
  4    ActiveModelTrait, ActiveValue, ColumnTrait, Condition, DatabaseTransaction, EntityTrait,
  5    ModelTrait, QueryFilter,
  6};
  7
  8use crate::db::ProjectId;
  9
 10use super::{
 11    dev_server, project, project_collaborator, remote_project, worktree, Database, DevServerId,
 12    RejoinedProject, RemoteProjectId, ResharedProject, ServerId, UserId,
 13};
 14
 15impl Database {
 16    pub async fn get_remote_project(
 17        &self,
 18        remote_project_id: RemoteProjectId,
 19    ) -> crate::Result<remote_project::Model> {
 20        self.transaction(|tx| async move {
 21            Ok(remote_project::Entity::find_by_id(remote_project_id)
 22                .one(&*tx)
 23                .await?
 24                .ok_or_else(|| anyhow!("no remote project with id {}", remote_project_id))?)
 25        })
 26        .await
 27    }
 28
 29    pub async fn get_remote_projects_for_dev_server(
 30        &self,
 31        dev_server_id: DevServerId,
 32    ) -> crate::Result<Vec<proto::RemoteProject>> {
 33        self.transaction(|tx| async move {
 34            let servers = remote_project::Entity::find()
 35                .filter(remote_project::Column::DevServerId.eq(dev_server_id))
 36                .find_also_related(project::Entity)
 37                .all(&*tx)
 38                .await?;
 39            Ok(servers
 40                .into_iter()
 41                .map(|(remote_project, project)| proto::RemoteProject {
 42                    id: remote_project.id.to_proto(),
 43                    project_id: project.map(|p| p.id.to_proto()),
 44                    dev_server_id: remote_project.dev_server_id.to_proto(),
 45                    path: remote_project.path,
 46                })
 47                .collect())
 48        })
 49        .await
 50    }
 51
 52    pub async fn remote_project_ids_for_user(
 53        &self,
 54        user_id: UserId,
 55        tx: &DatabaseTransaction,
 56    ) -> crate::Result<Vec<RemoteProjectId>> {
 57        let dev_servers = dev_server::Entity::find()
 58            .filter(dev_server::Column::UserId.eq(user_id))
 59            .find_with_related(remote_project::Entity)
 60            .all(tx)
 61            .await?;
 62
 63        Ok(dev_servers
 64            .into_iter()
 65            .flat_map(|(_, projects)| projects.into_iter().map(|p| p.id))
 66            .collect())
 67    }
 68
 69    pub async fn owner_for_remote_project(
 70        &self,
 71        remote_project_id: RemoteProjectId,
 72        tx: &DatabaseTransaction,
 73    ) -> crate::Result<UserId> {
 74        let dev_server = remote_project::Entity::find_by_id(remote_project_id)
 75            .find_also_related(dev_server::Entity)
 76            .one(tx)
 77            .await?
 78            .and_then(|(_, dev_server)| dev_server)
 79            .ok_or_else(|| anyhow!("no remote project"))?;
 80
 81        Ok(dev_server.user_id)
 82    }
 83
 84    pub async fn get_stale_dev_server_projects(
 85        &self,
 86        connection: ConnectionId,
 87    ) -> crate::Result<Vec<ProjectId>> {
 88        self.transaction(|tx| async move {
 89            let projects = project::Entity::find()
 90                .filter(
 91                    Condition::all()
 92                        .add(project::Column::HostConnectionId.eq(connection.id))
 93                        .add(project::Column::HostConnectionServerId.eq(connection.owner_id)),
 94                )
 95                .all(&*tx)
 96                .await?;
 97
 98            Ok(projects.into_iter().map(|p| p.id).collect())
 99        })
100        .await
101    }
102
103    pub async fn create_remote_project(
104        &self,
105        dev_server_id: DevServerId,
106        path: &str,
107        user_id: UserId,
108    ) -> crate::Result<(remote_project::Model, proto::RemoteProjectsUpdate)> {
109        self.transaction(|tx| async move {
110            let dev_server = dev_server::Entity::find_by_id(dev_server_id)
111                .one(&*tx)
112                .await?
113                .ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?;
114            if dev_server.user_id != user_id {
115                return Err(anyhow!("not your dev server"))?;
116            }
117
118            let project = remote_project::Entity::insert(remote_project::ActiveModel {
119                id: ActiveValue::NotSet,
120                dev_server_id: ActiveValue::Set(dev_server_id),
121                path: ActiveValue::Set(path.to_string()),
122            })
123            .exec_with_returning(&*tx)
124            .await?;
125
126            let status = self.remote_projects_update_internal(user_id, &tx).await?;
127
128            Ok((project, status))
129        })
130        .await
131    }
132
133    pub async fn share_remote_project(
134        &self,
135        remote_project_id: RemoteProjectId,
136        dev_server_id: DevServerId,
137        connection: ConnectionId,
138        worktrees: &[proto::WorktreeMetadata],
139    ) -> crate::Result<(proto::RemoteProject, UserId, proto::RemoteProjectsUpdate)> {
140        self.transaction(|tx| async move {
141            let dev_server = dev_server::Entity::find_by_id(dev_server_id)
142                .one(&*tx)
143                .await?
144                .ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?;
145
146            let remote_project = remote_project::Entity::find_by_id(remote_project_id)
147                .one(&*tx)
148                .await?
149                .ok_or_else(|| anyhow!("no remote project with id {}", remote_project_id))?;
150
151            if remote_project.dev_server_id != dev_server_id {
152                return Err(anyhow!("remote project shared from wrong server"))?;
153            }
154
155            let project = project::ActiveModel {
156                room_id: ActiveValue::Set(None),
157                host_user_id: ActiveValue::Set(None),
158                host_connection_id: ActiveValue::set(Some(connection.id as i32)),
159                host_connection_server_id: ActiveValue::set(Some(ServerId(
160                    connection.owner_id as i32,
161                ))),
162                id: ActiveValue::NotSet,
163                hosted_project_id: ActiveValue::Set(None),
164                remote_project_id: ActiveValue::Set(Some(remote_project_id)),
165            }
166            .insert(&*tx)
167            .await?;
168
169            if !worktrees.is_empty() {
170                worktree::Entity::insert_many(worktrees.iter().map(|worktree| {
171                    worktree::ActiveModel {
172                        id: ActiveValue::set(worktree.id as i64),
173                        project_id: ActiveValue::set(project.id),
174                        abs_path: ActiveValue::set(worktree.abs_path.clone()),
175                        root_name: ActiveValue::set(worktree.root_name.clone()),
176                        visible: ActiveValue::set(worktree.visible),
177                        scan_id: ActiveValue::set(0),
178                        completed_scan_id: ActiveValue::set(0),
179                    }
180                }))
181                .exec(&*tx)
182                .await?;
183            }
184
185            let status = self
186                .remote_projects_update_internal(dev_server.user_id, &tx)
187                .await?;
188
189            Ok((
190                remote_project.to_proto(Some(project)),
191                dev_server.user_id,
192                status,
193            ))
194        })
195        .await
196    }
197
198    pub async fn reshare_remote_projects(
199        &self,
200        reshared_projects: &Vec<proto::UpdateProject>,
201        dev_server_id: DevServerId,
202        connection: ConnectionId,
203    ) -> crate::Result<Vec<ResharedProject>> {
204        // todo!() project_transaction? (maybe we can make the lock per-dev-server instead of per-project?)
205        self.transaction(|tx| async move {
206            let mut ret = Vec::new();
207            for reshared_project in reshared_projects {
208                let project_id = ProjectId::from_proto(reshared_project.project_id);
209                let (project, remote_project) = project::Entity::find_by_id(project_id)
210                    .find_also_related(remote_project::Entity)
211                    .one(&*tx)
212                    .await?
213                    .ok_or_else(|| anyhow!("project does not exist"))?;
214
215                if remote_project.map(|rp| rp.dev_server_id) != Some(dev_server_id) {
216                    return Err(anyhow!("remote project reshared from wrong server"))?;
217                }
218
219                let Ok(old_connection_id) = project.host_connection() else {
220                    return Err(anyhow!("remote project was not shared"))?;
221                };
222
223                project::Entity::update(project::ActiveModel {
224                    id: ActiveValue::set(project_id),
225                    host_connection_id: ActiveValue::set(Some(connection.id as i32)),
226                    host_connection_server_id: ActiveValue::set(Some(ServerId(
227                        connection.owner_id as i32,
228                    ))),
229                    ..Default::default()
230                })
231                .exec(&*tx)
232                .await?;
233
234                let collaborators = project
235                    .find_related(project_collaborator::Entity)
236                    .all(&*tx)
237                    .await?;
238
239                self.update_project_worktrees(project_id, &reshared_project.worktrees, &tx)
240                    .await?;
241
242                ret.push(super::ResharedProject {
243                    id: project_id,
244                    old_connection_id,
245                    collaborators: collaborators
246                        .iter()
247                        .map(|collaborator| super::ProjectCollaborator {
248                            connection_id: collaborator.connection(),
249                            user_id: collaborator.user_id,
250                            replica_id: collaborator.replica_id,
251                            is_host: collaborator.is_host,
252                        })
253                        .collect(),
254                    worktrees: reshared_project.worktrees.clone(),
255                });
256            }
257            Ok(ret)
258        })
259        .await
260    }
261
262    pub async fn rejoin_remote_projects(
263        &self,
264        rejoined_projects: &Vec<proto::RejoinProject>,
265        user_id: UserId,
266        connection_id: ConnectionId,
267    ) -> crate::Result<Vec<RejoinedProject>> {
268        // todo!() project_transaction? (maybe we can make the lock per-dev-server instead of per-project?)
269        self.transaction(|tx| async move {
270            let mut ret = Vec::new();
271            for rejoined_project in rejoined_projects {
272                if let Some(project) = self
273                    .rejoin_project_internal(&tx, rejoined_project, user_id, connection_id)
274                    .await?
275                {
276                    ret.push(project);
277                }
278            }
279            Ok(ret)
280        })
281        .await
282    }
283}