diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e2adbcc31..2ca251069 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1016,6 +1016,7 @@ dependencies = [ "tauri-plugin-log", "tauri-plugin-window-state", "tempfile", + "thiserror", "urlencoding", "uuid 1.3.0", "walkdir", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3eea0d762..f0418a5d6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -34,6 +34,7 @@ tempfile = "3.3.0" reqwest = "0.11.14" md5 = "0.7.0" urlencoding = "2.1.2" +thiserror = "1.0.38" [features] # by default Tauri runs in production mode diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 695348c5c..0a1adbf80 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -11,7 +11,7 @@ mod watchers; use anyhow::{Context, Result}; use deltas::Delta; use log; -use serde::{Deserialize, Serialize}; +use serde::{ser::SerializeMap, Serialize}; use std::{collections::HashMap, sync::mpsc, thread}; use storage::Storage; use tauri::{generate_context, Manager}; @@ -19,10 +19,40 @@ use tauri_plugin_log::{ fern::colors::{Color, ColoredLevelConfig}, LogTarget, }; +use thiserror::Error; -#[derive(Debug, Serialize, Deserialize)] -pub struct Error { - pub message: String, +#[derive(Debug, Error)] +pub enum Error { + #[error("{0}")] + ProjectError(projects::CreateError), + #[error("Project already exists")] + ProjectAlreadyExists, + #[error("Something went wrong")] + Unknown, +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("message", &self.to_string())?; + map.end() + } +} + +impl From for Error { + fn from(e: projects::CreateError) -> Self { + Error::ProjectError(e) + } +} + +impl From for Error { + fn from(e: anyhow::Error) -> Self { + log::error!("{:#}", e); + Error::Unknown + } } const IS_DEV: bool = cfg!(debug_assertions); @@ -85,19 +115,11 @@ fn list_sessions( let users_storage = users::Storage::new(storage); let repo = repositories::Repository::open(&projects_storage, &users_storage, project_id) - .map_err(|e| { - log::error!("{:#}", e); - Error { - message: "Failed to open project".to_string(), - } - })?; + .with_context(|| format!("Failed to open repository for project {}", project_id))?; - let sessions = repo.sessions().map_err(|e| { - log::error!("{:#}", e); - Error { - message: "Failed to list sessions".to_string(), - } - })?; + let sessions = repo + .sessions() + .with_context(|| format!("Failed to list sessions for project {}", project_id))?; Ok(sessions) } @@ -108,12 +130,10 @@ fn get_user(handle: tauri::AppHandle) -> Result, Error> { let storage = storage::Storage::from_path_resolver(&path_resolver); let users_storage = users::Storage::new(storage); - match users_storage.get().map_err(|e| { - log::error!("{:#}", e); - Error { - message: "Failed to get user".to_string(), - } - })? { + match users_storage + .get() + .with_context(|| "Failed to get user".to_string())? + { Some(user) => { let local_picture = match proxy_image(handle, &user.picture) { Ok(picture) => picture, @@ -140,12 +160,9 @@ fn set_user(handle: tauri::AppHandle, user: users::User) -> Result<(), Error> { let storage = storage::Storage::from_path_resolver(&path_resolver); let users_storage = users::Storage::new(storage); - users_storage.set(&user).map_err(|e| { - log::error!("{:#}", e); - Error { - message: "Failed to save user".to_string(), - } - })?; + users_storage + .set(&user) + .with_context(|| "Failed to set user".to_string())?; sentry::configure_scope(|scope| scope.set_user(Some(user.clone().into()))); @@ -158,12 +175,9 @@ fn delete_user(handle: tauri::AppHandle) -> Result<(), Error> { let storage = storage::Storage::from_path_resolver(&path_resolver); let users_storage = users::Storage::new(storage); - users_storage.delete().map_err(|e| { - log::error!("{:#}", e); - Error { - message: "Failed to delete user".to_string(), - } - })?; + users_storage + .delete() + .with_context(|| "Failed to delete user".to_string())?; sentry::configure_scope(|scope| scope.set_user(None)); @@ -179,12 +193,11 @@ fn update_project( let storage = storage::Storage::from_path_resolver(&path_resolver); let projects_storage = projects::Storage::new(storage); - projects_storage.update_project(&project).map_err(|e| { - log::error!("{:#}", e); - Error { - message: "Failed to update project".to_string(), - } - }) + let project = projects_storage + .update_project(&project) + .with_context(|| format!("Failed to update project {}", project.id))?; + + Ok(project) } #[tauri::command] @@ -200,63 +213,32 @@ fn add_project(handle: tauri::AppHandle, path: &str) -> Result, mpsc::Receiver) = - mpsc::channel(); + let (tx, rx): (mpsc::Sender, mpsc::Receiver) = mpsc::channel(); + watchers.watch(tx, &project)?; + watch_events(handle, rx); - watchers.watch(tx, &project).map_err(|e| { - log::error!("{:#}", e); - Error { - message: "Failed to watch project".to_string(), - } - })?; - - watch_events(handle, rx); - - return Ok(project); - } else { - return Err(Error { - message: "Failed to add project".to_string(), - }); - } + Ok(project) } #[tauri::command] @@ -265,12 +247,9 @@ fn list_projects(handle: tauri::AppHandle) -> Result, Err let storage = storage::Storage::from_path_resolver(&path_resolver); let projects_storage = projects::Storage::new(storage); - projects_storage.list_projects().map_err(|e| { - log::error!("{:#}", e); - Error { - message: "Failed to list projects".to_string(), - } - }) + let projects = projects_storage.list_projects()?; + + Ok(projects) } #[tauri::command] @@ -286,37 +265,19 @@ fn delete_project(handle: tauri::AppHandle, id: &str) -> Result<(), Error> { users_storage.clone(), ); - match projects_storage.get_project(id) { - Ok(Some(project)) => { - watchers.unwatch(project).map_err(|e| { - log::error!("{:#}", e); - Error { - message: "Failed to unwatch project".to_string(), - } - })?; + match projects_storage.get_project(id)? { + Some(project) => { + watchers.unwatch(project)?; - projects_storage - .update_project(&projects::UpdateRequest { - id: id.to_string(), - deleted: Some(true), - ..Default::default() - }) - .map_err(|e| { - log::error!("{:#}", e); - Error { - message: "Failed to delete project".to_string(), - } - })?; + projects_storage.update_project(&projects::UpdateRequest { + id: id.to_string(), + deleted: Some(true), + ..Default::default() + })?; Ok(()) } - Ok(None) => Ok(()), - Err(e) => { - log::error!("{:#}", e); - Err(Error { - message: "Failed to get project".to_string(), - }) - } + None => Ok(()), } } @@ -332,20 +293,9 @@ fn list_session_files( let projects_storage = projects::Storage::new(storage.clone()); let users_storage = users::Storage::new(storage); - let repo = repositories::Repository::open(&projects_storage, &users_storage, project_id) - .map_err(|e| { - log::error!("{:#}", e); - Error { - message: "Failed to open project".to_string(), - } - })?; + let repo = repositories::Repository::open(&projects_storage, &users_storage, project_id)?; - let files = repo.files(session_id, paths).map_err(|e| { - log::error!("{:#}", e); - Error { - message: "Failed to list files".to_string(), - } - })?; + let files = repo.files(session_id, paths)?; Ok(files) } @@ -361,20 +311,9 @@ fn list_deltas( let projects_storage = projects::Storage::new(storage.clone()); let users_storage = users::Storage::new(storage); - let repo = repositories::Repository::open(&projects_storage, &users_storage, project_id) - .map_err(|e| { - log::error!("{:#}", e); - Error { - message: "Failed to open project".to_string(), - } - })?; + let repo = repositories::Repository::open(&projects_storage, &users_storage, project_id)?; - let deltas = repo.deltas(session_id).map_err(|e| { - log::error!("{:#}", e); - Error { - message: "Failed to list deltas".to_string(), - } - })?; + let deltas = repo.deltas(session_id)?; Ok(deltas) } diff --git a/src-tauri/src/projects/mod.rs b/src-tauri/src/projects/mod.rs index 7ac3a0eeb..0e4a75087 100644 --- a/src-tauri/src/projects/mod.rs +++ b/src-tauri/src/projects/mod.rs @@ -1,6 +1,5 @@ mod project; mod storage; -pub use project::Project; -pub use storage::Storage; -pub use storage::UpdateRequest; +pub use project::{CreateError, Project}; +pub use storage::{Storage, UpdateRequest}; diff --git a/src-tauri/src/projects/project.rs b/src-tauri/src/projects/project.rs index 25968f135..225a3547b 100644 --- a/src-tauri/src/projects/project.rs +++ b/src-tauri/src/projects/project.rs @@ -1,7 +1,7 @@ -use std::path::PathBuf; - -use anyhow::{anyhow, Result}; +use anyhow::Result; use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use thiserror::Error; #[derive(Debug, Deserialize, Serialize, Clone)] pub struct ApiProject { @@ -31,6 +31,16 @@ impl AsRef for Project { } } +#[derive(Error, Debug)] +pub enum CreateError { + #[error("{0} does not exist")] + PathNotFound(String), + #[error("{0} is not a directory")] + NotADirectory(String), + #[error("{0} is not a git repository")] + NotAGitRepository(String), +} + impl Project { pub fn refname(&self) -> String { format!("refs/gitbutler-{}/current", self.id) @@ -47,34 +57,40 @@ impl Project { self.session_path().join("deltas") } - pub fn from_path(path: String) -> Result { + pub fn from_path(fpath: String) -> Result { // make sure path exists - let path = std::path::Path::new(&path); + let path = std::path::Path::new(&fpath); if !path.exists() { - return Err(anyhow!("path {} does not exist", path.display())); + return Err(CreateError::PathNotFound(fpath).into()); } // make sure path is a directory if !path.is_dir() { - return Err(anyhow!("path {} is not a directory", path.display())); + return Err(CreateError::NotADirectory(fpath).into()); } // make sure it's a git repository if !path.join(".git").exists() { - return Err(anyhow!("path {} is not a git repository", path.display())); + return Err(CreateError::NotAGitRepository(fpath).into()); }; + let id = uuid::Uuid::new_v4().to_string(); + // title is the base name of the file - path.into_iter() + let title = path + .into_iter() .last() .map(|p| p.to_str().unwrap().to_string()) - .map(|title| Self { - id: uuid::Uuid::new_v4().to_string(), - deleted: false, - title, - path: path.to_str().unwrap().to_string(), - api: None, - }) - .ok_or_else(|| anyhow!("failed to get title from path")) + .unwrap_or_else(|| id.clone()); + + let project = Project { + id: uuid::Uuid::new_v4().to_string(), + deleted: false, + title, + path: path.to_str().unwrap().to_string(), + api: None, + }; + + Ok(project) } } diff --git a/src/lib/projects.ts b/src/lib/projects.ts index 944273c15..c7bf6f01a 100644 --- a/src/lib/projects.ts +++ b/src/lib/projects.ts @@ -3,20 +3,20 @@ import { derived, writable } from 'svelte/store'; import type { Project as ApiProject } from '$lib/api'; export type Project = { - id: string; - title: string; - path: string; - api: ApiProject & { sync: boolean }; + id: string; + title: string; + path: string; + api: ApiProject & { sync: boolean }; }; const list = () => invoke('list_projects'); const update = (params: { - project: { - id: string; - title?: string; - api?: ApiProject & { sync: boolean }; - }; + project: { + id: string; + title?: string; + api?: ApiProject & { sync: boolean }; + }; }) => invoke('update_project', params); const add = (params: { path: string }) => invoke('add_project', params); @@ -24,35 +24,35 @@ const add = (params: { path: string }) => invoke('add_project', params) const del = (params: { id: string }) => invoke('delete_project', params); export default async () => { - const init = await list(); - const store = writable(init); + const init = await list(); + const store = writable(init); - return { - subscribe: store.subscribe, - get: (id: string) => { - const project = derived(store, (store) => store.find((p) => p.id === id)); - return { - subscribe: project.subscribe, - update: (params: { title?: string; api?: Project['api'] }) => - update({ - project: { - id, - ...params - } - }).then((project) => { - store.update((projects) => projects.map((p) => (p.id === project.id ? project : p))); - return project; - }) - }; - }, - add: (params: { path: string }) => - add(params).then((project) => { - store.update((projects) => [...projects, project]); - return project; - }), - delete: (params: { id: string }) => - del(params).then(() => { - store.update((projects) => projects.filter((p) => p.id !== params.id)); - }) - }; + return { + subscribe: store.subscribe, + get: (id: string) => { + const project = derived(store, (store) => store.find((p) => p.id === id)); + return { + subscribe: project.subscribe, + update: (params: { title?: string; api?: Project['api'] }) => + update({ + project: { + id, + ...params + } + }).then((project) => { + store.update((projects) => projects.map((p) => (p.id === project.id ? project : p))); + return project; + }) + }; + }, + add: (params: { path: string }) => + add(params).then((project) => { + store.update((projects) => [...projects, project]); + return project; + }), + delete: (params: { id: string }) => + del(params).then(() => { + store.update((projects) => projects.filter((p) => p.id !== params.id)); + }) + }; }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index cc28ed676..ce576b93c 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,6 +1,8 @@