Commit 1667225a0db723c110603ec3f5f822584acdff52
1 parent
149cddde
Create database entries on file upload
Showing
12 changed files
with
200 additions
and
47 deletions
| ... | ... | @@ -20,3 +20,15 @@ pub struct MarkdownDiffJson { |
| 20 | 20 | pub id: i32, |
| 21 | 21 | pub date_created: String, |
| 22 | 22 | } |
| 23 | + | |
| 24 | +#[derive(Clone, Debug, Serialize, Deserialize)] | |
| 25 | +pub struct ImageJson { | |
| 26 | + pub upload_uuid :Option<Vec<u8>>, | |
| 27 | + pub uuid :Option<Vec<u8>>, | |
| 28 | + pub size :i32, | |
| 29 | + pub dim_x :Option<i32>, | |
| 30 | + pub dim_y :Option<i32>, | |
| 31 | + pub mime_type :String, | |
| 32 | + pub date_created :String, | |
| 33 | + pub date_updated :String | |
| 34 | +} | ... | ... |
No preview for this file type
migrations/2022-01-28-163413_images/down.sql
0 → 100644
migrations/2022-01-28-163413_images/up.sql
0 → 100644
| 1 | +CREATE TABLE "images" ( | |
| 2 | + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, | |
| 3 | + -- identical uuid means identical file. | |
| 4 | + upload_uuid BLOB(16) UNIQUE, | |
| 5 | + uuid BLOB(16) UNIQUE, | |
| 6 | + size INTEGER NOT NULL, | |
| 7 | + dim_x INTEGER, | |
| 8 | + dim_y INTEGER, | |
| 9 | + mime_type VARCHAR(256) NOT NULL, | |
| 10 | + date_created TEXT NOT NULL, | |
| 11 | + date_updated TEXT NOT NULL | |
| 12 | +); | ... | ... |
| ... | ... | @@ -7,7 +7,7 @@ use r2d2; |
| 7 | 7 | type ParentError = Option<Pin<Box<dyn std::error::Error>>>; |
| 8 | 8 | |
| 9 | 9 | #[derive(Debug)] |
| 10 | -pub(crate) struct Error { | |
| 10 | +pub struct Error { | |
| 11 | 11 | source: ParentError, |
| 12 | 12 | message: String, |
| 13 | 13 | } |
| ... | ... | @@ -77,3 +77,11 @@ impl From<ParsePatchError> for Error { |
| 77 | 77 | } |
| 78 | 78 | } |
| 79 | 79 | } |
| 80 | + | |
| 81 | +impl From<uuid::Error> for Error { | |
| 82 | + fn from(source: uuid::Error) -> Self { | |
| 83 | + Self { source: Some(Box::pin(source)) | |
| 84 | + , message: String::from("UUID error") | |
| 85 | + } | |
| 86 | + } | |
| 87 | +} | ... | ... |
| ... | ... | @@ -6,11 +6,15 @@ mod models; |
| 6 | 6 | mod routes; |
| 7 | 7 | mod schema; |
| 8 | 8 | mod uuid; |
| 9 | +mod upload; | |
| 9 | 10 | |
| 11 | +use async_std::channel::Receiver; | |
| 12 | +use models::image::Image; | |
| 10 | 13 | use routes::markdown::*; |
| 11 | 14 | use routes::other::*; |
| 12 | 15 | use routes::user::*; |
| 13 | 16 | use routes::upload::*; |
| 17 | +use crate::upload::get_sample; | |
| 14 | 18 | use crate::uuid::Uuid; |
| 15 | 19 | |
| 16 | 20 | use actix_web::{guard, web, App, HttpResponse, HttpServer}; |
| ... | ... | @@ -18,10 +22,10 @@ use async_std::{ channel::{ Sender, bounded } |
| 18 | 22 | , fs::File }; |
| 19 | 23 | use diesel::r2d2::{self, ConnectionManager}; |
| 20 | 24 | use diesel::SqliteConnection; |
| 21 | -use futures::{ AsyncReadExt, AsyncSeekExt, FutureExt, StreamExt, select | |
| 25 | +use futures::{ FutureExt, StreamExt, select | |
| 22 | 26 | , stream::FuturesUnordered }; |
| 23 | 27 | use listenfd::ListenFd; |
| 24 | -use std::io::SeekFrom; | |
| 28 | +use std::convert::TryFrom; | |
| 25 | 29 | use std::sync::Arc; |
| 26 | 30 | |
| 27 | 31 | pub(crate) type Pool = r2d2::Pool<ConnectionManager<SqliteConnection>>; |
| ... | ... | @@ -29,34 +33,7 @@ pub(crate) type Pool = r2d2::Pool<ConnectionManager<SqliteConnection>>; |
| 29 | 33 | #[derive(Clone)] |
| 30 | 34 | pub struct AppData { |
| 31 | 35 | pub database_pool: Arc<Pool>, |
| 32 | - pub tx_upload_worker: Sender<String>, | |
| 33 | -} | |
| 34 | - | |
| 35 | -async fn read_at( f :&mut File | |
| 36 | - , pos :SeekFrom | |
| 37 | - , buf :&mut [u8]) -> std::io::Result<()> { | |
| 38 | - f.seek(pos).await?; | |
| 39 | - f.read_exact(buf).await | |
| 40 | -} | |
| 41 | - | |
| 42 | -async fn get_sample( f :&mut File | |
| 43 | - , buf :&mut [u8]) -> std::io::Result<()> { | |
| 44 | - let file_len = f.metadata().await?.len(); | |
| 45 | - let chunk_size = buf.len() / 3; | |
| 46 | - | |
| 47 | - read_at(f, SeekFrom::Start(0), &mut buf[0..chunk_size]).await?; | |
| 48 | - if file_len >= 2 * chunk_size as u64 { | |
| 49 | - read_at( f | |
| 50 | - , SeekFrom::End(-(chunk_size as i64)) | |
| 51 | - , &mut buf[2*chunk_size..]).await?; | |
| 52 | - } | |
| 53 | - if file_len >= 3 * chunk_size as u64 { | |
| 54 | - read_at( f | |
| 55 | - , SeekFrom::Start((file_len-chunk_size as u64) / 2) | |
| 56 | - , &mut buf[chunk_size..2*chunk_size]).await?; | |
| 57 | - } | |
| 58 | - | |
| 59 | - Ok(()) | |
| 36 | + pub tx_upload_worker: Sender<Image>, | |
| 60 | 37 | } |
| 61 | 38 | |
| 62 | 39 | #[actix_rt::main] |
| ... | ... | @@ -65,7 +42,8 @@ async fn main() -> std::io::Result<()> { |
| 65 | 42 | |
| 66 | 43 | dotenv::dotenv().ok(); |
| 67 | 44 | |
| 68 | - let (tx_upload_worker, rx_upload_worker) = bounded(32); | |
| 45 | + let (tx_upload_worker, rx_upload_worker) | |
| 46 | + : (Sender<Image>, Receiver<Image>) = bounded(32); | |
| 69 | 47 | |
| 70 | 48 | let _upload_worker = actix_rt::spawn(async move { |
| 71 | 49 | let mut workers = FuturesUnordered::new(); |
| ... | ... | @@ -75,12 +53,14 @@ async fn main() -> std::io::Result<()> { |
| 75 | 53 | filename = rx_upload_worker.recv().fuse() => { |
| 76 | 54 | match filename { |
| 77 | 55 | Err(_) => break, |
| 78 | - Ok(filename) => workers.push(async move { | |
| 79 | - let mut f = File::open(&filename).await.unwrap(); | |
| 56 | + Ok(upload) => workers.push(async move { | |
| 57 | + let upload_uuid = Uuid::try_from(upload.upload_uuid.unwrap()).unwrap(); | |
| 58 | + let upload_filename = format!("/tmp/upload_{}", upload_uuid); | |
| 59 | + let mut f = File::open(&upload_filename).await.unwrap(); | |
| 80 | 60 | let mut buf = vec!['.' as u8; 3 * 4096]; |
| 81 | 61 | get_sample(&mut f, buf.as_mut()).await.unwrap(); |
| 82 | 62 | println!( "[UPLOAD WORKER] filename: {}" |
| 83 | - , filename ); | |
| 63 | + , upload_filename ); | |
| 84 | 64 | println!( "[UPLOAD WORKER] uuid: {}" |
| 85 | 65 | , Uuid::get( "some.unique.namespace" |
| 86 | 66 | , buf.as_mut() ) ); | ... | ... |
server/src/models/image.rs
0 → 100644
| 1 | +use std::sync::Arc; | |
| 2 | + | |
| 3 | +use crate::error::*; | |
| 4 | +use crate::{schema::*, Pool}; | |
| 5 | +use diesel::{Connection, insert_into}; | |
| 6 | +use diesel::prelude::*; | |
| 7 | +use serde::{Deserialize, Serialize}; | |
| 8 | + | |
| 9 | +#[derive(Clone, Debug, Serialize, Deserialize, Queryable, Identifiable)] | |
| 10 | +pub struct Image { | |
| 11 | + pub id :i32, | |
| 12 | + pub upload_uuid :Option<Vec<u8>>, | |
| 13 | + pub uuid :Option<Vec<u8>>, | |
| 14 | + pub size :i32, | |
| 15 | + pub dim_x :Option<i32>, | |
| 16 | + pub dim_y :Option<i32>, | |
| 17 | + pub mime_type :String, | |
| 18 | + pub date_created :String, | |
| 19 | + pub date_updated :String | |
| 20 | +} | |
| 21 | + | |
| 22 | +#[derive(Debug, Insertable)] | |
| 23 | +#[table_name = "images"] | |
| 24 | +pub struct ImageNew<'a> { | |
| 25 | + pub upload_uuid :Option<&'a [u8]>, | |
| 26 | + pub size :i32, | |
| 27 | + pub mime_type :&'a str, | |
| 28 | + pub date_created :&'a str, | |
| 29 | + pub date_updated :&'a str | |
| 30 | +} | |
| 31 | + | |
| 32 | +#[derive(Clone, Debug, Serialize, Deserialize, AsChangeset)] | |
| 33 | +#[table_name = "images"] | |
| 34 | +pub struct Upload { | |
| 35 | + pub upload_uuid :Option<Vec<u8>>, | |
| 36 | + pub size :i32, | |
| 37 | + pub mime_type :String, | |
| 38 | +} | |
| 39 | + | |
| 40 | +pub(crate) fn upload( pool: Arc<Pool> | |
| 41 | + , item: Upload ) -> Result<Image> { | |
| 42 | + use crate::schema::images::dsl::*; | |
| 43 | + let db_connection = pool.get()?; | |
| 44 | + | |
| 45 | + let now = chrono::Local::now().naive_local(); | |
| 46 | + let new_image = ImageNew { | |
| 47 | + upload_uuid : item.upload_uuid.as_deref(), | |
| 48 | + size : item.size, | |
| 49 | + mime_type : &item.mime_type, | |
| 50 | + date_created : &format!("{}", now), | |
| 51 | + date_updated : &format!("{}", now) | |
| 52 | + }; | |
| 53 | + | |
| 54 | + Ok(db_connection.transaction(|| { | |
| 55 | + insert_into(images) . values(&new_image) | |
| 56 | + . execute(&db_connection)?; | |
| 57 | + images . order(id.desc()) | |
| 58 | + . first::<Image>(&db_connection) | |
| 59 | + })?) | |
| 60 | +} | ... | ... |
| ... | ... | @@ -4,25 +4,47 @@ use futures::stream::StreamExt; |
| 4 | 4 | use async_std::{fs::OpenOptions, io::WriteExt}; |
| 5 | 5 | use uuid::Uuid; |
| 6 | 6 | |
| 7 | -use crate::AppData; | |
| 7 | +use crate::{AppData, models::image::{Upload, self}}; | |
| 8 | 8 | |
| 9 | -pub async fn upload( app_data: web::Data<AppData> | |
| 10 | - , mut body: web::Payload) -> Result<HttpResponse, Error> | |
| 9 | +pub async fn upload( app_data :web::Data<AppData> | |
| 10 | + , mut body :web::Payload | |
| 11 | + , request :web::HttpRequest ) -> Result<HttpResponse, Error> | |
| 11 | 12 | { |
| 13 | + let pool = app_data.database_pool.clone(); | |
| 12 | 14 | let worker = app_data.tx_upload_worker.clone(); |
| 13 | 15 | |
| 14 | - let upload_filename = format!("/tmp/upload_{}", Uuid::new_v4()); | |
| 16 | + let random_uuid = Uuid::new_v4(); | |
| 17 | + let upload_uuid = Some(random_uuid.as_bytes().to_vec()); | |
| 18 | + let size = request.headers().get("content-length") | |
| 19 | + . and_then(|h| Some(h.to_str().unwrap().parse::<i32>())) | |
| 20 | + . unwrap().unwrap(); | |
| 21 | + let mime_type = String::from( request.headers().get("content-type") | |
| 22 | + . and_then(|h| Some(h.to_str().unwrap())) | |
| 23 | + . unwrap() ); | |
| 24 | + | |
| 25 | + let upload_filename = format!("/tmp/upload_{}", random_uuid); | |
| 15 | 26 | let mut output = OpenOptions::new(); |
| 16 | - output . create(true) | |
| 17 | - . write(true); | |
| 18 | - let mut output = output.open(&upload_filename).await?; | |
| 27 | + let mut output = output | |
| 28 | + . create(true) | |
| 29 | + . write(true) | |
| 30 | + . open(&upload_filename).await?; | |
| 19 | 31 | |
| 20 | 32 | while let Some(item) = body.next().await { |
| 21 | 33 | output.write_all(&item?).await?; |
| 22 | 34 | } |
| 23 | 35 | |
| 24 | - // TODO handle this as error response... | |
| 25 | - worker.send(upload_filename).await.unwrap(); | |
| 36 | + let upload = Upload { | |
| 37 | + upload_uuid, | |
| 38 | + size, | |
| 39 | + mime_type | |
| 40 | + }; | |
| 26 | 41 | |
| 27 | - Ok(HttpResponse::Ok().finish()) | |
| 42 | + Ok( match web::block(move || image::upload(pool, upload)).await { | |
| 43 | + Ok(image) => { | |
| 44 | + // TODO handle this as error response... | |
| 45 | + worker.send(image.clone()).await.unwrap(); | |
| 46 | + HttpResponse::Ok().json(image) | |
| 47 | + }, | |
| 48 | + Err(_) => HttpResponse::InternalServerError().finish() | |
| 49 | + } ) | |
| 28 | 50 | } | ... | ... |
| 1 | 1 | table! { |
| 2 | + images (id) { | |
| 3 | + id -> Integer, | |
| 4 | + upload_uuid -> Nullable<Binary>, | |
| 5 | + uuid -> Nullable<Binary>, | |
| 6 | + size -> Integer, | |
| 7 | + dim_x -> Nullable<Integer>, | |
| 8 | + dim_y -> Nullable<Integer>, | |
| 9 | + mime_type -> Text, | |
| 10 | + date_created -> Text, | |
| 11 | + date_updated -> Text, | |
| 12 | + } | |
| 13 | +} | |
| 14 | + | |
| 15 | +table! { | |
| 2 | 16 | markdown_diffs (markdown_id, diff_id) { |
| 3 | 17 | markdown_id -> Integer, |
| 4 | 18 | diff_id -> Integer, |
| ... | ... | @@ -28,6 +42,7 @@ table! { |
| 28 | 42 | } |
| 29 | 43 | |
| 30 | 44 | allow_tables_to_appear_in_same_query!( |
| 45 | + images, | |
| 31 | 46 | markdown_diffs, |
| 32 | 47 | markdowns, |
| 33 | 48 | users, | ... | ... |
server/src/upload.rs
0 → 100644
| 1 | +use std::io::SeekFrom; | |
| 2 | +use async_std::fs::File; | |
| 3 | +use futures::{AsyncSeekExt, AsyncReadExt}; | |
| 4 | + | |
| 5 | + | |
| 6 | +async fn read_at( f :&mut File | |
| 7 | + , pos :SeekFrom | |
| 8 | + , buf :&mut [u8]) -> std::io::Result<()> { | |
| 9 | + f.seek(pos).await?; | |
| 10 | + f.read_exact(buf).await | |
| 11 | +} | |
| 12 | + | |
| 13 | +pub async fn get_sample( f :&mut File | |
| 14 | + , buf :&mut [u8]) -> std::io::Result<()> { | |
| 15 | + let file_len = f.metadata().await?.len(); | |
| 16 | + let chunk_size = buf.len() / 3; | |
| 17 | + | |
| 18 | + read_at(f, SeekFrom::Start(0), &mut buf[0..chunk_size]).await?; | |
| 19 | + if file_len >= 2 * chunk_size as u64 { | |
| 20 | + read_at( f | |
| 21 | + , SeekFrom::End(-(chunk_size as i64)) | |
| 22 | + , &mut buf[2*chunk_size..]).await?; | |
| 23 | + } | |
| 24 | + if file_len >= 3 * chunk_size as u64 { | |
| 25 | + read_at( f | |
| 26 | + , SeekFrom::Start((file_len-chunk_size as u64) / 2) | |
| 27 | + , &mut buf[chunk_size..2*chunk_size]).await?; | |
| 28 | + } | |
| 29 | + | |
| 30 | + Ok(()) | |
| 31 | +} | ... | ... |
| 1 | -use std::fmt::Display; | |
| 1 | +use std::{fmt::Display, convert::TryFrom}; | |
| 2 | + | |
| 3 | +use crate::error::Error; | |
| 2 | 4 | |
| 3 | 5 | #[derive(Clone,Copy,Debug)] |
| 4 | 6 | pub struct Uuid(pub uuid::Uuid); |
| ... | ... | @@ -20,3 +22,11 @@ impl Uuid { |
| 20 | 22 | Self(uuid::Uuid::new_v5(&ns!(ns), buf)) |
| 21 | 23 | } |
| 22 | 24 | } |
| 25 | + | |
| 26 | +impl TryFrom<Vec<u8>> for Uuid { | |
| 27 | + type Error = Error; | |
| 28 | + | |
| 29 | + fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> { | |
| 30 | + Ok(Self(uuid::Uuid::from_slice(value.as_slice())?)) | |
| 31 | + } | |
| 32 | +} | ... | ... |
Please
register
or
login
to post a comment