Compare commits
No commits in common. 'aaef6ed851ff20e3101a1a9d037a6353bc22e8f8' and 'b587cde27b88c134d41d1bf521f2865607ea57e4' have entirely different histories.
aaef6ed851
...
b587cde27b
11 changed files with 115 additions and 549 deletions
@ -1,51 +0,0 @@ |
||||
# Тестовое задание для Stilsoft |
||||
|
||||
Сервис для асинхронной обработки ссылок. |
||||
|
||||
Для настройки сервиса используются переменные окружения в файле `.env`. Экспортировать их в терминал не нужно, они считаются автоматически. |
||||
Запуск осуществляется через `cargo run`. |
||||
|
||||
Реализованные фичи: |
||||
- сервер на axum с API для управления ссылками (просмотр/загрузка/удаление, см. схему ниже) |
||||
- in-memory хранилище на sled |
||||
- из результата запроса ссылки достаётся количество символов |
||||
- кэширование результатов (бессрочное) |
||||
- удаление дубликатов ссылок |
||||
- возможность указать размер пула обработчиков ссылок через URL_HANDLERS_POOL_SIZE |
||||
- логирование и чтение настроек логирования через RUST_LOG |
||||
- отмена загрузки ссылок на SIGHUP |
||||
|
||||
Для отправки 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,49 +0,0 @@ |
||||
#!/usr/bin/env python3 |
||||
import sys |
||||
import json |
||||
import requests |
||||
|
||||
BASE_URL = "http://localhost:8000" |
||||
|
||||
def usage(): |
||||
print("Possible actions:\n list [url1] [url2] ...\n upload [url1] [url2] ...\n del [url1] [url2] ...") |
||||
sys.exit(1) |
||||
|
||||
def main(): |
||||
try: |
||||
action = sys.argv[1] |
||||
except IndexError: |
||||
usage() |
||||
|
||||
try: |
||||
urls = sys.argv[2:] |
||||
except IndexError: |
||||
urls = [] |
||||
|
||||
if action == "list": |
||||
if len(urls) == 0: |
||||
result = requests.get(f"{BASE_URL}/urls") |
||||
else: |
||||
result = requests.post(f"{BASE_URL}/urls", json={"urls": urls}) |
||||
elif action == "upload": |
||||
result = requests.post(f"{BASE_URL}/urls/upload", json={"urls": urls}) |
||||
elif action == "del": |
||||
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(json.dumps(result.json())) |
||||
else: |
||||
print("200 OK") |
||||
else: |
||||
print(result.text) |
||||
|
||||
|
||||
|
||||
if __name__ == '__main__': |
||||
main() |
@ -1,77 +0,0 @@ |
||||
use std::collections::{HashMap, HashSet}; |
||||
|
||||
use sled::{Batch, Db}; |
||||
|
||||
use crate::{ |
||||
error::{AppError, AppResult}, |
||||
models::UrlResult, |
||||
}; |
||||
|
||||
pub trait Repo: Send + Sync + 'static { |
||||
fn insert_urls(&self, urls: &[UrlResult]) -> AppResult<()>; |
||||
|
||||
fn delete_urls(&self, urls: Option<&HashSet<String>>) -> AppResult<usize>; |
||||
|
||||
fn get_urls(&self, urls: Option<&HashSet<String>>) -> AppResult<HashMap<String, usize>>; |
||||
} |
||||
|
||||
impl Repo for Db { |
||||
fn insert_urls(&self, urls: &[UrlResult]) -> AppResult<()> { |
||||
let mut batch = Batch::default(); |
||||
for url in urls { |
||||
batch.insert( |
||||
url.url.to_string().as_bytes(), |
||||
bincode::serialize(&url.content_length)?, |
||||
); |
||||
} |
||||
self.apply_batch(batch).map_err(AppError::from) |
||||
} |
||||
|
||||
fn delete_urls(&self, urls: Option<&HashSet<String>>) -> AppResult<usize> { |
||||
let mut deleted = 0; |
||||
|
||||
match urls { |
||||
Some(urls) => { |
||||
for url in urls { |
||||
info!("deleting {url}"); |
||||
let del = self.remove(url.as_bytes())?; |
||||
if del.is_some() { |
||||
deleted += 1 |
||||
} |
||||
} |
||||
} |
||||
None => { |
||||
info!("deleting all urls"); |
||||
deleted = self.len(); |
||||
self.clear()?; |
||||
} |
||||
}; |
||||
|
||||
Ok(deleted) |
||||
} |
||||
|
||||
fn get_urls(&self, urls: Option<&HashSet<String>>) -> AppResult<HashMap<String, usize>> { |
||||
match urls { |
||||
Some(urls) => { |
||||
let mut result = HashMap::new(); |
||||
|
||||
for url in urls { |
||||
let Some(val) = self.get(url.as_bytes())? else { |
||||
continue; |
||||
}; |
||||
result.insert(url.to_owned(), bincode::deserialize(&val)?); |
||||
} |
||||
Ok(result) |
||||
} |
||||
None => self |
||||
.iter() |
||||
.map(|result| { |
||||
let (k, v) = result?; |
||||
let key = String::from_utf8(k.to_vec())?; |
||||
let val = bincode::deserialize(&v)?; |
||||
Ok((key, val)) |
||||
}) |
||||
.collect::<AppResult<_>>(), |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue