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,3 +20,15 @@ pub struct MarkdownDiffJson { | ||
| 20 | pub id: i32, | 20 | pub id: i32, |
| 21 | pub date_created: String, | 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 +7,7 @@ use r2d2; | ||
| 7 | type ParentError = Option<Pin<Box<dyn std::error::Error>>>; | 7 | type ParentError = Option<Pin<Box<dyn std::error::Error>>>; |
| 8 | 8 | ||
| 9 | #[derive(Debug)] | 9 | #[derive(Debug)] |
| 10 | -pub(crate) struct Error { | 10 | +pub struct Error { |
| 11 | source: ParentError, | 11 | source: ParentError, |
| 12 | message: String, | 12 | message: String, |
| 13 | } | 13 | } |
| @@ -77,3 +77,11 @@ impl From<ParsePatchError> for Error { | @@ -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,11 +6,15 @@ mod models; | ||
| 6 | mod routes; | 6 | mod routes; |
| 7 | mod schema; | 7 | mod schema; |
| 8 | mod uuid; | 8 | mod uuid; |
| 9 | +mod upload; | ||
| 9 | 10 | ||
| 11 | +use async_std::channel::Receiver; | ||
| 12 | +use models::image::Image; | ||
| 10 | use routes::markdown::*; | 13 | use routes::markdown::*; |
| 11 | use routes::other::*; | 14 | use routes::other::*; |
| 12 | use routes::user::*; | 15 | use routes::user::*; |
| 13 | use routes::upload::*; | 16 | use routes::upload::*; |
| 17 | +use crate::upload::get_sample; | ||
| 14 | use crate::uuid::Uuid; | 18 | use crate::uuid::Uuid; |
| 15 | 19 | ||
| 16 | use actix_web::{guard, web, App, HttpResponse, HttpServer}; | 20 | use actix_web::{guard, web, App, HttpResponse, HttpServer}; |
| @@ -18,10 +22,10 @@ use async_std::{ channel::{ Sender, bounded } | @@ -18,10 +22,10 @@ use async_std::{ channel::{ Sender, bounded } | ||
| 18 | , fs::File }; | 22 | , fs::File }; |
| 19 | use diesel::r2d2::{self, ConnectionManager}; | 23 | use diesel::r2d2::{self, ConnectionManager}; |
| 20 | use diesel::SqliteConnection; | 24 | use diesel::SqliteConnection; |
| 21 | -use futures::{ AsyncReadExt, AsyncSeekExt, FutureExt, StreamExt, select | 25 | +use futures::{ FutureExt, StreamExt, select |
| 22 | , stream::FuturesUnordered }; | 26 | , stream::FuturesUnordered }; |
| 23 | use listenfd::ListenFd; | 27 | use listenfd::ListenFd; |
| 24 | -use std::io::SeekFrom; | 28 | +use std::convert::TryFrom; |
| 25 | use std::sync::Arc; | 29 | use std::sync::Arc; |
| 26 | 30 | ||
| 27 | pub(crate) type Pool = r2d2::Pool<ConnectionManager<SqliteConnection>>; | 31 | pub(crate) type Pool = r2d2::Pool<ConnectionManager<SqliteConnection>>; |
| @@ -29,34 +33,7 @@ pub(crate) type Pool = r2d2::Pool<ConnectionManager<SqliteConnection>>; | @@ -29,34 +33,7 @@ pub(crate) type Pool = r2d2::Pool<ConnectionManager<SqliteConnection>>; | ||
| 29 | #[derive(Clone)] | 33 | #[derive(Clone)] |
| 30 | pub struct AppData { | 34 | pub struct AppData { |
| 31 | pub database_pool: Arc<Pool>, | 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 | #[actix_rt::main] | 39 | #[actix_rt::main] |
| @@ -65,7 +42,8 @@ async fn main() -> std::io::Result<()> { | @@ -65,7 +42,8 @@ async fn main() -> std::io::Result<()> { | ||
| 65 | 42 | ||
| 66 | dotenv::dotenv().ok(); | 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 | let _upload_worker = actix_rt::spawn(async move { | 48 | let _upload_worker = actix_rt::spawn(async move { |
| 71 | let mut workers = FuturesUnordered::new(); | 49 | let mut workers = FuturesUnordered::new(); |
| @@ -75,12 +53,14 @@ async fn main() -> std::io::Result<()> { | @@ -75,12 +53,14 @@ async fn main() -> std::io::Result<()> { | ||
| 75 | filename = rx_upload_worker.recv().fuse() => { | 53 | filename = rx_upload_worker.recv().fuse() => { |
| 76 | match filename { | 54 | match filename { |
| 77 | Err(_) => break, | 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 | let mut buf = vec!['.' as u8; 3 * 4096]; | 60 | let mut buf = vec!['.' as u8; 3 * 4096]; |
| 81 | get_sample(&mut f, buf.as_mut()).await.unwrap(); | 61 | get_sample(&mut f, buf.as_mut()).await.unwrap(); |
| 82 | println!( "[UPLOAD WORKER] filename: {}" | 62 | println!( "[UPLOAD WORKER] filename: {}" |
| 83 | - , filename ); | 63 | + , upload_filename ); |
| 84 | println!( "[UPLOAD WORKER] uuid: {}" | 64 | println!( "[UPLOAD WORKER] uuid: {}" |
| 85 | , Uuid::get( "some.unique.namespace" | 65 | , Uuid::get( "some.unique.namespace" |
| 86 | , buf.as_mut() ) ); | 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,25 +4,47 @@ use futures::stream::StreamExt; | ||
| 4 | use async_std::{fs::OpenOptions, io::WriteExt}; | 4 | use async_std::{fs::OpenOptions, io::WriteExt}; |
| 5 | use uuid::Uuid; | 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 | let worker = app_data.tx_upload_worker.clone(); | 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 | let mut output = OpenOptions::new(); | 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 | while let Some(item) = body.next().await { | 32 | while let Some(item) = body.next().await { |
| 21 | output.write_all(&item?).await?; | 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 | table! { | 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 | markdown_diffs (markdown_id, diff_id) { | 16 | markdown_diffs (markdown_id, diff_id) { |
| 3 | markdown_id -> Integer, | 17 | markdown_id -> Integer, |
| 4 | diff_id -> Integer, | 18 | diff_id -> Integer, |
| @@ -28,6 +42,7 @@ table! { | @@ -28,6 +42,7 @@ table! { | ||
| 28 | } | 42 | } |
| 29 | 43 | ||
| 30 | allow_tables_to_appear_in_same_query!( | 44 | allow_tables_to_appear_in_same_query!( |
| 45 | + images, | ||
| 31 | markdown_diffs, | 46 | markdown_diffs, |
| 32 | markdowns, | 47 | markdowns, |
| 33 | users, | 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 | #[derive(Clone,Copy,Debug)] | 5 | #[derive(Clone,Copy,Debug)] |
| 4 | pub struct Uuid(pub uuid::Uuid); | 6 | pub struct Uuid(pub uuid::Uuid); |
| @@ -20,3 +22,11 @@ impl Uuid { | @@ -20,3 +22,11 @@ impl Uuid { | ||
| 20 | Self(uuid::Uuid::new_v5(&ns!(ns), buf)) | 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