Skip to content

Commit 2a761d7

Browse files
committed
feature: endpoint save video, with url param is estableç
1 parent efadb4f commit 2a761d7

6 files changed

Lines changed: 194 additions & 16 deletions

File tree

document_manager/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,7 @@ utoipa = { version = "4.2.3", features = ["actix_extras", "non_strict_integers",
3131
utoipa-swagger-ui = { version = "7.1.0", features = ["actix-web"] }
3232
actix-web-lab = "0.20.2"
3333

34+
reqwest = { version = "0.11", features = ["blocking", "json"] }
35+
bytes = "1.7.1"
36+
mime_guess = "2.0.5"
37+

document_manager/src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pub fn config_logger(target: Target) -> Result<()> {
3131
.format_module_path(false)
3232
.format_timestamp_millis()
3333
.write_style(env_logger::WriteStyle::Always)
34-
.filter(None, LevelFilter::Debug)
34+
.filter(None, LevelFilter::Info)
3535
.try_init()
3636
.with_context(|| "Wasn't unable to set up the logger")
3737
}

document_manager/src/endpoints/save.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
use actix_multipart::form::{tempfile::TempFile, text::Text};
22
use actix_multipart::form::MultipartForm;
33
use actix_web::{HttpResponse, post, Responder, web};
4+
use actix_web::web::Json;
45
use log::info;
6+
use serde::Deserialize;
57
use utoipa::{IntoParams, ToSchema};
68

79

810
use crate::config::DbPool;
911
use crate::EnvironmentState;
10-
use crate::operations::save_document;
12+
use crate::operations::{save_document, save_document_by_url};
1113

1214
#[derive(Debug, MultipartForm, ToSchema, IntoParams)]
1315
pub struct SaveDocumentRequest {
@@ -50,4 +52,35 @@ pub async fn upload_document(
5052
HttpResponse::InternalServerError().finish()
5153
}
5254
}
55+
}
56+
57+
58+
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
59+
pub struct SaveDocumentByUrlRequest {
60+
pub url_file: String,
61+
pub application: String,
62+
pub is_private_document: bool,
63+
pub username: String,
64+
}
65+
66+
#[utoipa::path(
67+
post,
68+
path = "/upload_document_by_url",
69+
request_body(content = SaveDocumentByUrlRequest),
70+
responses(
71+
(status = 200, description = "Successful", body = Uuid)
72+
),
73+
)]
74+
#[post("/upload_document_by_url")]
75+
pub async fn upload_document_by_url(body: Json<SaveDocumentByUrlRequest>,
76+
env_state: web::Data<EnvironmentState>,
77+
conn: web::Data<DbPool>,
78+
) -> impl Responder {
79+
match save_document_by_url(body.into_inner(), env_state, &conn).await {
80+
Ok(uuid) => HttpResponse::Ok().json(uuid),
81+
Err(error) => {
82+
info!("{error}");
83+
HttpResponse::InternalServerError().finish()
84+
}
85+
}
5386
}

document_manager/src/main.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ use utoipa_swagger_ui::SwaggerUi;
1010

1111
use crate::config::{config_logger, configure_storage_directory, EnvConfig, establish_connection};
1212
use crate::endpoints::{delete::delete_document, filter::find_documents, save::upload_document};
13-
use crate::endpoints::{delete::DeleteDocumentRequest, filter::{DocumentContentResponse, DocumentFilterRequest, FoundContentResponse, FoundDocumentResponse}, save::SaveDocumentRequest};
13+
use crate::endpoints::{delete::DeleteDocumentRequest, filter::{DocumentContentResponse, DocumentFilterRequest, FoundContentResponse, FoundDocumentResponse}, save::{SaveDocumentByUrlRequest, SaveDocumentRequest}};
14+
use crate::endpoints::save::upload_document_by_url;
1415

1516
mod config;
1617
mod models;
@@ -39,14 +40,16 @@ impl TryFrom<EnvConfig> for EnvironmentState {
3940
#[openapi(
4041
paths(
4142
endpoints::save::upload_document,
43+
endpoints::save::upload_document_by_url,
4244
endpoints::delete::delete_document,
4345
endpoints::filter::find_documents
4446
),
4547
components(schemas(
4648
SaveDocumentRequest,
49+
SaveDocumentByUrlRequest,
4750
DeleteDocumentRequest,
4851
DocumentFilterRequest,
49-
FoundDocumentResponse, FoundContentResponse, DocumentContentResponse
52+
FoundDocumentResponse, FoundContentResponse, DocumentContentResponse,
5053
))
5154
)]
5255
struct ApiDoc;
@@ -76,6 +79,7 @@ async fn main() -> Result<()> {
7679
.url("/api-docs/openapi.json", ApiDoc::openapi()),
7780
)
7881
.service(upload_document)
82+
.service(upload_document_by_url)
7983
.service(delete_document)
8084
.service(find_documents)
8185
})

document_manager/src/operations/fs.rs

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use base64::engine::{GeneralPurpose, GeneralPurposeConfig};
99
use color_eyre::{Report, Result};
1010
use color_eyre::eyre::ContextCompat;
1111
use log::debug;
12+
use tokio::fs::{File, remove_file};
13+
use tokio::io::AsyncWriteExt;
1214
use uuid::Uuid;
1315

1416
pub struct PathFile(PathBuf, String);
@@ -80,6 +82,21 @@ pub async fn read_content_file_to_base64(path: &Path) -> Result<String> {
8082
let engine = GeneralPurpose::new(&STANDARD, GeneralPurposeConfig::default());
8183
Ok(engine.encode(buffer_read_content_file))
8284
}
85+
pub async fn read_content_bytes_to_base64(b: &[u8]) -> Result<String> {
86+
let engine = GeneralPurpose::new(&STANDARD, GeneralPurposeConfig::default());
87+
Ok(engine.encode(b))
88+
}
89+
90+
fn create_parent_directories(to: &Path) -> Result<()> {
91+
if let Some(path_directory) = to.parent() {
92+
debug!(
93+
"Creating directories required to path: {:?}",
94+
path_directory
95+
);
96+
create_dir_all(path_directory)?;
97+
}
98+
Ok(())
99+
}
83100

84101
pub fn move_file(from: &Path, to: PathBuf) -> Result<()> {
85102
debug!("Moving file from: {:?}, to: {:?}", from, to);
@@ -88,22 +105,44 @@ pub fn move_file(from: &Path, to: PathBuf) -> Result<()> {
88105
return Err(Report::msg("From path is a directory"));
89106
}
90107

91-
if let Some(path_directory) = to.parent() {
92-
debug!(
93-
"Creating directories required to path: {:?}",
94-
path_directory
95-
);
96-
create_dir_all(path_directory)?
97-
}
108+
create_parent_directories(&to)?;
98109
//Error using fs::rename, because error by temp files
99110
copy(from, to)?;
100111
Ok(())
101112
}
102113

114+
115+
pub async fn save_file(to: PathBuf, content: &[u8]) -> Result<()> {
116+
if to.is_dir() {
117+
return Err(Report::msg("Path is a directory"));
118+
}
119+
create_parent_directories(&to)?;
120+
121+
debug!("Saving file in: {:?}", to);
122+
let mut f = File::create(to).await?;
123+
let _ = f.write(content).await?;
124+
125+
Ok(())
126+
}
127+
128+
pub async fn delete_file(p: &Path) -> Result<()> {
129+
remove_file(p).await?;
130+
Ok(())
131+
}
132+
103133
pub fn get_extension_and_file_name(file_name: &str) -> (&str, Option<&str>) {
104134
if let Some(index) = file_name.rfind('.') {
105135
(&file_name[..index], Some(&file_name[index..]))
106136
} else {
107137
(file_name, None)
108138
}
109139
}
140+
141+
142+
pub fn get_file_name_in_url(url: &str) -> Option<&str> {
143+
if let Some(index) = url.rfind('/') {
144+
Some(&url[index + 1..])
145+
} else {
146+
None
147+
}
148+
}

document_manager/src/operations/mod.rs

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,29 @@
1-
use std::path::PathBuf;
1+
use std::path::{Path, PathBuf};
22

33
use actix_multipart::form::MultipartForm;
44
use actix_web::web;
5+
use bytes::Bytes;
56
use chrono::Local;
67
use color_eyre::{Report, Result};
7-
use color_eyre::eyre::Context;
8+
use color_eyre::eyre::{Context, OptionExt};
89
use diesel::{ExpressionMethods, insert_into, update};
910
use diesel::prelude::*;
1011
use diesel_async::{AsyncConnection, RunQueryDsl};
1112
use diesel_async::scoped_futures::ScopedFutureExt;
1213
use itertools::Itertools;
13-
use log::debug;
14+
use log::{debug, error, info};
15+
use mime_guess::from_ext;
16+
use reqwest::Client;
1417
use serde::{Deserialize, Serialize};
1518
use uuid::Uuid;
1619

1720
use crate::config::DbPool;
1821
use crate::endpoints::delete::DeleteDocumentRequest;
1922
use crate::endpoints::filter::DocumentFilterRequest;
20-
use crate::endpoints::save::SaveDocumentRequest;
23+
use crate::endpoints::save::{SaveDocumentByUrlRequest, SaveDocumentRequest};
2124
use crate::EnvironmentState;
2225
use crate::models::{Content, DeleteContent, DeleteDocument, Document, NewContent, NewDocument};
23-
use crate::operations::fs::{generate_path_by_uuid, generate_url_by_uuid, get_extension_and_file_name, move_file, read_content_file_to_base64};
26+
use crate::operations::fs::{delete_file, generate_path_by_uuid, generate_url_by_uuid, get_extension_and_file_name, get_file_name_in_url, move_file, read_content_bytes_to_base64, read_content_file_to_base64, save_file};
2427
use crate::schema::{content, document};
2528

2629
mod fs;
@@ -86,6 +89,101 @@ pub async fn save_document(
8689
.await
8790
}
8891

92+
pub async fn save_document_by_url(params: SaveDocumentByUrlRequest,
93+
env_state: web::Data<EnvironmentState>,
94+
conn: &DbPool) -> Result<Uuid> {
95+
let conn = &mut conn.get().await?;
96+
let uuid_document = Uuid::new_v4();
97+
debug!("Generate UUID: {uuid_document}, to save document");
98+
99+
100+
let r = conn.transaction::<Uuid, Report, _>(|conn| {
101+
async move {
102+
let url_file = params.url_file.clone();
103+
let file_name_in_url = get_file_name_in_url(&url_file)
104+
.ok_or_eyre(format!("Error not match filename in url: {:?} ", url_file))?;
105+
let (filename, extension) = get_extension_and_file_name(file_name_in_url);
106+
107+
let mime_type = extension
108+
.map(|x| from_ext(x).first_or_octet_stream().to_string());
109+
let info_download = download_file(&params.url_file).await?;
110+
let new_document = NewDocument {
111+
id_document: &uuid_document,
112+
name: filename,
113+
extension: extension.map(|x| x.to_string()),
114+
content_type: mime_type,
115+
application: &params.application,
116+
create_username: &params.username,
117+
};
118+
insert_into(document::table)
119+
.values(new_document)
120+
.execute(conn)
121+
.await
122+
.with_context(|| "Error create document")?;
123+
124+
if params.is_private_document {
125+
debug!("Document is private saving in database");
126+
let content_file = read_content_bytes_to_base64(&info_download.content).await?;
127+
let content = NewContent {
128+
id_document: &uuid_document,
129+
data: &content_file,
130+
create_username: &params.username,
131+
};
132+
insert_into(content::table)
133+
.values(content)
134+
.execute(conn)
135+
.await
136+
.with_context(|| "Error save row in table content")?;
137+
} else {
138+
let new_path = PathBuf::from(generate_path_by_uuid(
139+
env_state.disk_storage_directory_path.clone(),
140+
extension.unwrap_or(""),
141+
uuid_document,
142+
)?);
143+
debug!("Document is public saving in {:?}", new_path);
144+
save_file(new_path, &info_download.content).await?;
145+
}
146+
debug!("Finish procces save document");
147+
Ok(uuid_document)
148+
}
149+
.scope_boxed()
150+
})
151+
.await;
152+
153+
154+
match r {
155+
Ok(e) => Ok(e),
156+
Err(e) => {
157+
//delete_file(&clone_new_path).await?;
158+
159+
Err(e)
160+
}
161+
}
162+
}
163+
164+
165+
struct DownloadFileInfo {
166+
pub content: Bytes,
167+
}
168+
async fn download_file(url: &str) -> Result<DownloadFileInfo> {
169+
let client = Client::new();
170+
let response = client.get(url).send().await?;
171+
172+
if response.status().is_success() {
173+
let content = response.bytes().await?;
174+
info!("File downloaded");
175+
Ok(
176+
DownloadFileInfo {
177+
content,
178+
}
179+
)
180+
} else {
181+
error!("Error download file: {}", response.status());
182+
Err(Report::msg("Error download file"))
183+
}
184+
}
185+
186+
89187
pub async fn delete_document_and_content(
90188
document_delete: web::Query<DeleteDocumentRequest>,
91189
conn: web::Data<DbPool>,

0 commit comments

Comments
 (0)