readme & bugfixes

master
plazmoid 1 month ago
parent bcfdd57795
commit d434739816
  1. 41
      README.md
  2. 8
      run.py
  3. 37
      src/handlers.rs
  4. 26
      src/repo.rs

@ -0,0 +1,41 @@
# Тестовое задание для Stilsoft
Сервис для асинхронной обработки ссылок. Написан на tokio+axum+sled.
Для настройки сервиса используются переменные окружения в файле `.env`. Экспортировать их в терминал не нужно, они считаются автоматически.
Запуск осуществляется через `cargo run`.
Для отправки HTTP-запросов к сервису можно использовать дополнительную утилиту run.py:
```
./run.py list <url1> <url2>
./run.py upload <url1> <url2>
./run.py del <url1> <url2>
```
Схема:
Получение списка сохранённых ссылок:
```
Без фильтров:
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]}
```

@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys import sys
import json
import requests import requests
BASE_URL = "http://localhost:8000" BASE_URL = "http://localhost:8000"
@ -27,13 +28,16 @@ def main():
elif action == "upload": elif action == "upload":
result = requests.post(f"{BASE_URL}/urls/upload", json={"urls": urls}) result = requests.post(f"{BASE_URL}/urls/upload", json={"urls": urls})
elif action == "del": 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: else:
usage() usage()
if result.status_code == 200: if result.status_code == 200:
if len(result.text) > 0: if len(result.text) > 0:
print(result.json()) print(json.dumps(result.json()))
else: else:
print("200 OK") print("200 OK")
else: else:

@ -20,7 +20,7 @@ pub async fn get_urls<R: Repo>(
.collect::<HashSet<_>>() .collect::<HashSet<_>>()
}); });
let r_state = state.read().await; 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)) Ok(Json(result))
} }
@ -29,7 +29,31 @@ pub async fn upload_urls<R: Repo>(
State(state): State<AppStateShared<R>>, State(state): State<AppStateShared<R>>,
Json(payload): Json<UrlRequest>, Json(payload): Json<UrlRequest>,
) -> AppResult<impl IntoResponse> { ) -> AppResult<impl IntoResponse> {
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::<HashSet<_>>()
};
let tasks = { let tasks = {
urls.into_iter() urls.into_iter()
@ -68,9 +92,6 @@ pub async fn upload_urls<R: Repo>(
.collect::<Vec<_>>() .collect::<Vec<_>>()
}; };
let mut successes = vec![];
let mut failures = vec![];
for task in tasks { for task in tasks {
let result = task.await.unwrap(); let result = task.await.unwrap();
let status = result.status.unwrap_or(0); let status = result.status.unwrap_or(0);
@ -92,7 +113,7 @@ pub async fn upload_urls<R: Repo>(
pub async fn delete_urls<R: Repo>( pub async fn delete_urls<R: Repo>(
State(state): State<AppStateShared<R>>, State(state): State<AppStateShared<R>>,
req: Option<Json<UrlRequest>>, req: Option<Json<UrlRequest>>,
) -> AppResult<()> { ) -> AppResult<impl IntoResponse> {
let urls = req.map(|urls| { let urls = req.map(|urls| {
urls.0 urls.0
.urls .urls
@ -101,7 +122,7 @@ pub async fn delete_urls<R: Repo>(
.collect::<HashSet<_>>() .collect::<HashSet<_>>()
}); });
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())
} }

@ -10,9 +10,9 @@ use crate::{
pub trait Repo: Send + Sync + 'static { pub trait Repo: Send + Sync + 'static {
fn insert_urls(&self, urls: &[UrlResult]) -> AppResult<()>; fn insert_urls(&self, urls: &[UrlResult]) -> AppResult<()>;
fn delete_urls(&self, urls: Option<&HashSet<String>>) -> AppResult<()>; fn delete_urls(&self, urls: Option<&HashSet<String>>) -> AppResult<usize>;
fn list_urls(&self, urls: Option<&HashSet<String>>) -> AppResult<HashMap<String, usize>>; fn get_urls(&self, urls: Option<&HashSet<String>>) -> AppResult<HashMap<String, usize>>;
} }
impl Repo for Db { impl Repo for Db {
@ -27,20 +27,30 @@ impl Repo for Db {
self.apply_batch(batch).map_err(AppError::from) self.apply_batch(batch).map_err(AppError::from)
} }
fn delete_urls(&self, urls: Option<&HashSet<String>>) -> AppResult<()> { fn delete_urls(&self, urls: Option<&HashSet<String>>) -> AppResult<usize> {
let mut deleted = 0;
match urls { match urls {
Some(urls) => { Some(urls) => {
for url in 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<String>>) -> AppResult<HashMap<String, usize>> { fn get_urls(&self, urls: Option<&HashSet<String>>) -> AppResult<HashMap<String, usize>> {
match urls { match urls {
Some(urls) => { Some(urls) => {
let mut result = HashMap::new(); let mut result = HashMap::new();

Loading…
Cancel
Save