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

Error handling #59

Open
wants to merge 3 commits into
base: Searching
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pnpm-debug.log*
lerna-debug.log*

node_modules

dist
dist-ssr
*.local
Expand All @@ -21,4 +22,4 @@ dist-ssr
*.ntvs*
*.njsproj
*.sln
*.sw?
*.sw?
3 changes: 2 additions & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ serde = { version = "1.0.138", features = ["derive"] }
tauri = { version = "1.0.3", features = ["api-all"] }
url = { version = "2.2.2", features = ["serde"] }
walkdir = "2.3.2"

thiserror = "1.0.48"
anyhow = "1.0.75"
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
Expand Down
4 changes: 1 addition & 3 deletions src-tauri/src/base/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ pub enum Mode {
}
pub type Scores = Vec<Score>;



#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Clone)]
pub struct Score {
pub name: String,
Expand All @@ -47,7 +45,7 @@ impl Score {
Score {
hash,
name,
value: 0
value: 0,
}
}

Expand Down
36 changes: 36 additions & 0 deletions src-tauri/src/command/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use std::string::FromUtf8Error;

use serde::Serialize;
use thiserror::Error;

pub type Result<T> = std::result::Result<T, Error>;

#[derive(Serialize, Error, Debug)]
pub enum Error {
#[error("Invalid URL")]
Url,
#[error("Arklib error")]
Arklib,
#[error("Alreay exist")]
LinkExist,
#[error("IO error")]
IO,
}

impl From<url::ParseError> for Error {
fn from(_: url::ParseError) -> Self {
Self::Url
}
}

impl From<std::io::Error> for Error {
fn from(_: std::io::Error) -> Self {
Self::IO
}
}

impl From<FromUtf8Error> for Error {
fn from(_: FromUtf8Error) -> Self {
Self::IO
}
}
79 changes: 45 additions & 34 deletions src-tauri/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ use std::{
sync::{Arc, Mutex},
};

mod errors;
use errors::{Error, Result};

use crate::{
base::{Link, OpenGraph, Score, Scores},
Cli, ARK_SHELF_WORKING_DIR, SCORES_PATH,
Expand All @@ -18,7 +21,7 @@ use walkdir::{DirEntry, WalkDir};
pub struct LinkScoreMap {
pub name: String,
pub hash: String,
pub value: i64
pub value: i64,
}

#[tauri::command]
Expand All @@ -28,26 +31,28 @@ async fn create_link(
desc: Option<String>,
url: String,
state: tauri::State<'_, Cli>,
) -> Result<String, String> {
let url = match Url::parse(url.as_str()) {
Ok(val) => val,
Err(e) => return Err(e.to_string()),
};
) -> Result<String> {
let url = Url::parse(url.as_str())?;
let resource = arklib::id::ResourceId::compute_bytes(url.as_ref().as_bytes())
.expect("Error compute resource from url");
.map_err(|_| Error::Arklib)?;
let domain = url.domain().expect("Url has no domain");
let path = format!("{}/{domain}-{}.link", &state.path, resource.crc32);
let mut link = Link::new(url, title, desc);
link.write_to_path(&state.path, &path, true)
.await
.expect("Custom error type needed");
Ok(path)
// Validate there is not already a ressource identical
if std::fs::metadata(&path).is_ok() {
Err(Error::LinkExist)
} else {
let mut link = Link::new(url, title, desc);
link.write_to_path(&state.path, &path, true)
.await
.map_err(|_| Error::Arklib)?;
Ok(path)
}
}

#[tauri::command]
/// Remove a `.link` from directory
async fn delete_link(name: String, state: tauri::State<'_, Cli>) -> Result<(), ()> {
fs::remove_file(format!("{}/{}", &state.path, name)).expect("cannot remove the link");
async fn delete_link(name: String, state: tauri::State<'_, Cli>) -> Result<()> {
fs::remove_file(format!("{}/{}", &state.path, name))?;
Ok(())
}

Expand All @@ -61,7 +66,6 @@ fn get_fs_links() -> Vec<DirEntry> {
.file_name()
.to_str()
.unwrap()
.to_string()
.ends_with(".link")
})
.map(|e| e.unwrap())
Expand All @@ -82,23 +86,26 @@ async fn read_link_list() -> Vec<String> {
}

#[tauri::command]
async fn generate_link_preview(url: String) -> Result<OpenGraph, String> {
Link::get_preview(url).await.map_err(|e| e.to_string())
async fn generate_link_preview(url: String) -> Result<OpenGraph> {
Link::get_preview(url).await.map_err(|_| Error::IO)
}

/// Get the score list
#[tauri::command]
async fn get_scores(scores: tauri::State<'_, Arc<Mutex<Scores>>>, path: tauri::State<'_, Cli>) -> Result<Scores, String> {
let scores_content = std::fs::read(SCORES_PATH.as_path()).unwrap_or("Error reading score file".into());
let scores_content = String::from_utf8(scores_content).unwrap_or("Error reading score file".into());
async fn get_scores(
scores: tauri::State<'_, Arc<Mutex<Scores>>>,
path: tauri::State<'_, Cli>,
) -> Result<Scores> {
let scores_content = std::fs::read(SCORES_PATH.as_path())?;
let scores_content = String::from_utf8(scores_content)?;
let scores_files = Score::parse_and_merge(scores_content, &path.path);
let mut guard = scores.lock().unwrap();
*guard = scores_files.clone();
Ok(scores_files)
}

#[tauri::command]
async fn add(scores: tauri::State<'_, Arc<Mutex<Scores>>>, name: String) -> Result<Option<Score>, String> {
async fn add(scores: tauri::State<'_, Arc<Mutex<Scores>>>, name: String) -> Result<Option<Score>> {
let mut guard = scores.lock().unwrap();
let mut result = None;
if let Some(score) = guard.iter_mut().find(|score| score.name == name) {
Expand All @@ -111,7 +118,10 @@ async fn add(scores: tauri::State<'_, Arc<Mutex<Scores>>>, name: String) -> Resu
}

#[tauri::command]
async fn substract(scores: tauri::State<'_, Arc<Mutex<Scores>>>, name: String) -> Result<Option<Score>, String> {
async fn substract(
scores: tauri::State<'_, Arc<Mutex<Scores>>>,
name: String,
) -> Result<Option<Score>> {
let mut guard = scores.lock().unwrap();
let mut result = None;
if let Some(score) = guard.iter_mut().find(|score| score.name == name) {
Expand All @@ -124,15 +134,19 @@ async fn substract(scores: tauri::State<'_, Arc<Mutex<Scores>>>, name: String) -
}

#[tauri::command]
async fn create_score(scores: tauri::State<'_, Arc<Mutex<Scores>>>, url: String, value: i64) -> Result<Score, String> {
async fn create_score(
scores: tauri::State<'_, Arc<Mutex<Scores>>>,
url: String,
value: i64,
) -> Result<Score> {
let mut score = Score::new(&url);
score.value = value;
let mut guard = scores.lock().unwrap();
guard.push(score.clone());
let content = Score::into_lines(&*guard);
std::fs::write(SCORES_PATH.as_path(), content).unwrap();
std::fs::write(SCORES_PATH.as_path(), content)?;
Ok(score)
}
}

/// Set scores
///
Expand All @@ -141,17 +155,14 @@ async fn create_score(scores: tauri::State<'_, Arc<Mutex<Scores>>>, url: String,
async fn set_scores(
scores: Scores,
state_scores: tauri::State<'_, Arc<Mutex<Scores>>>,
) -> Result<(), String> {
) -> Result<()> {
let guard = state_scores.lock().unwrap();
dbg!(&scores);
let mut scores_file = File::options()
.write(true)
.truncate(true)
.open(SCORES_PATH.as_path())
.unwrap();
scores_file
.write_all(Score::into_lines(&*guard).as_bytes())
.map_err(|e| e.to_string())?;
.open(SCORES_PATH.as_path())?;
scores_file.write_all(Score::into_lines(&*guard).as_bytes())?;
Ok(())
}

Expand All @@ -168,10 +179,10 @@ pub struct LinkWrapper {

#[tauri::command]
/// Read data from `.link` file
async fn read_link(name: String, state: tauri::State<'_, Cli>) -> Result<LinkWrapper, ()> {
async fn read_link(name: String, state: tauri::State<'_, Cli>) -> Result<LinkWrapper> {
let file_path = format!("{}/{name}", &state.path);
let link = Link::load(&state.path, &file_path).expect(&format!("Error loading {file_path}"));
let meta = fs::metadata(&file_path).expect("Error loading metadata");
let link = Link::load(&state.path, &file_path).map_err(|_| Error::Arklib)?;
let meta = fs::metadata(&file_path)?;
let created_time = match meta.created() {
Ok(time) => Some(time),
Err(_) => None,
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ fn main() {
// Skip if there's no content in the file.
if !scores_string.is_empty() {
scores = Score::parse_and_merge(scores_string, ARK_SHELF_WORKING_DIR.as_path());
}
}

dbg!(&scores);

Expand Down
44 changes: 33 additions & 11 deletions src/components/Form.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { toast } from '@zerodevx/svelte-toast';
import { linksInfos } from '../store';
import { createLink, getPreview } from './utils';
import { createLink, debounce, getPreview } from './utils';
import Alphabetical from '~icons/ic/outline-sort-by-alpha';
import Calendar from '~icons/ic/baseline-calendar-month';
import Scores from '~icons/ic/baseline-format-list-bulleted';
Expand All @@ -23,6 +23,16 @@
toast.push('Failed to fetch website data');
}
};

let error = false;

const debouncedCheck = debounce((url: string) => {
if ($linksInfos.some(l => l.url === url)) {
error = true;
} else {
error = false;
}
}, 200);
</script>

<div>
Expand Down Expand Up @@ -64,25 +74,37 @@
url,
desc,
};
const newLink = await createLink(data);
if (newLink) {
linksInfos.update(links => {
links.push(newLink);
return links;
});
toast.push('Link created!');
} else {
toast.push('Error creating link');
if ($linksInfos.every(l => l.url !== url)) {
const newLink = await createLink(data);
if (newLink) {
linksInfos.update(links => {
links = links.filter(l => l.url !== url);
links.push(newLink);
return links;
});
toast.push('Link created!');
} else {
toast.push('Error creating link');
}
}
}}>
<label for="url" aria-label="URL" />
{#if error}
<p class="text-red-500">There is already a link with the same URL</p>
{/if}
<input
type="text"
id="url"
name="url"
required
placeholder="URL*"
class="rounded-md bg-neutral-950 px-2 py-3 outline-none ring-1 ring-neutral-500"
on:keyup={e => {
debouncedCheck(e.currentTarget.value);
}}
on:change={e => {
debouncedCheck(e.currentTarget.value);
}}
bind:value={url} />
<label for="title" aria-label="Title" />
<input
Expand All @@ -102,7 +124,7 @@
class="rounded-md bg-neutral-950 px-2 py-3 outline-none ring-1 ring-neutral-500"
id="description" />
<div class="flex justify-between">
<button type="submit" class="pl-2 text-blue-400">CREATE</button>
<button type="submit" class="pl-2 text-blue-400" disabled={error}>CREATE</button>
<button class="pr-2 text-rose-700" {disabled} on:click={auto}>AUTO FILLED</button>
</div>
</form>
Expand Down
10 changes: 4 additions & 6 deletions src/components/LinkFooter.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,17 @@
class="rounded-md p-2 text-blue-400 hover:bg-blue-400 hover:bg-opacity-20"
on:click={() => {
clipboard.writeText(url);
}}>COPY</button
>
}}>COPY</button>
<button
class="rounded-md p-2 text-blue-400 hover:bg-blue-400 hover:bg-opacity-20"
on:click={() => {
open(url);
}}>OPEN</button
>
}}>OPEN</button>
<button
class="rounded-md p-2 text-red-500 hover:bg-red-500 hover:bg-opacity-20"
on:click={async () => {
const deleted = await deleteLink(name);
console.log({ deleted });
if (!deleted) {
toast.push('Error deleting link');
} else {
Expand All @@ -37,7 +36,6 @@
});
toast.push('Link deleted!');
}
}}>DELETE</button
>
}}>DELETE</button>
<Score {score} {url} />
</div>
12 changes: 12 additions & 0 deletions src/components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,15 @@ export const createScore = async ({ value, url }: { value: number; url: string }
return;
}
};

export const debounce = (callback: unknown, wait = 500) => {
let timeoutId: number;
return (...args: unknown[]) => {
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
if (typeof callback === 'function') {
callback(...args);
}
}, wait);
};
};
Loading