diff --git a/README.md b/README.md new file mode 100644 index 0000000..1ac4690 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Тестовое задание для Stilsoft + +Сервис для асинхронной обработки ссылок. Написан на tokio+axum+sled. + +Для настройки сервиса используются переменные окружения в файле `.env`. Экспортировать их в терминал не нужно, они считаются автоматически. +Запуск осуществляется через `cargo run`. + +Для отправки HTTP-запросов к сервису можно использовать дополнительную утилиту run.py: +``` +./run.py list +./run.py upload +./run.py del +``` + +Схема: + +Получение списка сохранённых ссылок: +``` +Без фильтров: +GET localhost:8000/url + +С фильтром: +POST localhost:8000/url + body: {"urls": [url1, url2]} +``` + +Загрузка ссылок: +``` +POST localhost:8000/url/upload + body: {"urls": [url1, url2]} +``` + +Удаление сохранённых ссылок: +``` +Удаление всех ссылок: +GET localhost:8000/url/delete + +Удаление заданных ссылок: +POST localhost:8000/url + body: {"urls": [url1, url2]} +``` diff --git a/run.py b/run.py index e5c45d0..3cc0f3f 100755 --- a/run.py +++ b/run.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import sys +import json import requests BASE_URL = "http://localhost:8000" @@ -27,13 +28,16 @@ def main(): elif action == "upload": result = requests.post(f"{BASE_URL}/urls/upload", json={"urls": urls}) elif action == "del": - result = requests.get(f"{BASE_URL}/urls/delete", json={"urls": urls}) + if len(urls) == 0: + result = requests.get(f"{BASE_URL}/urls/delete") + else: + result = requests.post(f"{BASE_URL}/urls/delete", json={"urls": urls}) else: usage() if result.status_code == 200: if len(result.text) > 0: - print(result.json()) + print(json.dumps(result.json())) else: print("200 OK") else: diff --git a/src/handlers.rs b/src/handlers.rs index 978eaf4..853d7a4 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -20,7 +20,7 @@ pub async fn get_urls( .collect::>() }); let r_state = state.read().await; - let result = r_state.repo.list_urls(urls.as_ref())?; + let result = r_state.repo.get_urls(urls.as_ref())?; Ok(Json(result)) } @@ -29,7 +29,31 @@ pub async fn upload_urls( State(state): State>, Json(payload): Json, ) -> AppResult { - let urls = payload.urls; + let mut successes = vec![]; + let mut failures = vec![]; + + let urls = { + let r_state = state.read().await; + let cached_urls = r_state.repo.get_urls(None)?; + + payload + .urls + .into_iter() + .filter(|url| match cached_urls.get(&url.to_string()) { + Some(val) => { + info!("{} is cached, not fetching", url); + successes.push(UrlResult { + status: None, + url: url.clone(), + content_length: *val, + error_msg: None, + }); + false + } + None => true, + }) + .collect::>() + }; let tasks = { urls.into_iter() @@ -68,9 +92,6 @@ pub async fn upload_urls( .collect::>() }; - let mut successes = vec![]; - let mut failures = vec![]; - for task in tasks { let result = task.await.unwrap(); let status = result.status.unwrap_or(0); @@ -92,7 +113,7 @@ pub async fn upload_urls( pub async fn delete_urls( State(state): State>, req: Option>, -) -> AppResult<()> { +) -> AppResult { let urls = req.map(|urls| { urls.0 .urls @@ -101,7 +122,7 @@ pub async fn delete_urls( .collect::>() }); - state.write().await.repo.delete_urls(urls.as_ref())?; + let deleted = state.write().await.repo.delete_urls(urls.as_ref())?; - Ok(()) + Ok(deleted.to_string()) } diff --git a/src/repo.rs b/src/repo.rs index e8aa4ab..a0e93ef 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -10,9 +10,9 @@ use crate::{ pub trait Repo: Send + Sync + 'static { fn insert_urls(&self, urls: &[UrlResult]) -> AppResult<()>; - fn delete_urls(&self, urls: Option<&HashSet>) -> AppResult<()>; + fn delete_urls(&self, urls: Option<&HashSet>) -> AppResult; - fn list_urls(&self, urls: Option<&HashSet>) -> AppResult>; + fn get_urls(&self, urls: Option<&HashSet>) -> AppResult>; } impl Repo for Db { @@ -27,20 +27,30 @@ impl Repo for Db { self.apply_batch(batch).map_err(AppError::from) } - fn delete_urls(&self, urls: Option<&HashSet>) -> AppResult<()> { + fn delete_urls(&self, urls: Option<&HashSet>) -> AppResult { + let mut deleted = 0; + match urls { Some(urls) => { for url in urls { - self.remove(url.as_bytes())?; + info!("deleting {url}"); + let del = self.remove(url.as_bytes())?; + if del.is_some() { + deleted += 1 + } } - - Ok(()) } - None => self.clear().map_err(AppError::from), - } + None => { + info!("deleting all urls"); + deleted = self.len(); + self.clear()?; + } + }; + + Ok(deleted) } - fn list_urls(&self, urls: Option<&HashSet>) -> AppResult> { + fn get_urls(&self, urls: Option<&HashSet>) -> AppResult> { match urls { Some(urls) => { let mut result = HashMap::new();