From a2365042a1079ec4c58bae80a7cb288d01a4582d Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Sat, 23 Apr 2022 06:50:17 -0700 Subject: [PATCH] server transport via websocket Co-authored-by: Oscar Beaumont Co-authored-by: Brendan Allan --- .vscode/settings.json | 1 + Cargo.lock | Bin 215866 -> 222943 bytes apps/server/Cargo.toml | 7 +- apps/server/Dockerfile | 7 + apps/server/src/main.rs | 172 +++++++++++++++++- apps/web/src/App.tsx | 62 ++++++- core/prisma/schema.prisma | 6 + core/src/file/cas/identifier.rs | 2 +- .../src/components/file/FileList.tsx | 15 +- .../src/components/file/FileThumb.tsx | 8 +- .../interface/src/components/file/Sidebar.tsx | 7 +- 11 files changed, 252 insertions(+), 35 deletions(-) create mode 100644 apps/server/Dockerfile diff --git a/.vscode/settings.json b/.vscode/settings.json index 1dbe20c96..6241d352a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "cSpell.words": [ + "actix", "bpfrpt", "creationdate", "ipfs", diff --git a/Cargo.lock b/Cargo.lock index d71f4e991b7598189493248d281f434782ee9b75..bffe1c80122bbfc172bd65e74b5cf6cc027b98f9 100644 GIT binary patch delta 3363 zcma)Fot9_0;Rp#5eDXt9v3Ha2a<5DXF^F){i9(O{aGXc{lU3*vLmw0l8K{M2(g zv+w2q{GZ?RpC3Na|JKj?Pc*W1uG73fJ=oi08&ML41T)THN3r6H+sLd%EJhcN(?RmU zkusJAWwG_z8t!92ED&ddFZRX3-KJb_qTLzaQyUwvUhejx-8>Si|FF$YoG}L%#`mPL zC$quy-YeNavG;ddp185+isGH0UX{N1)oeq0a611``rD&1@8eue+0L`QrGdt?BG#J-1!YrIDO#hM19&a!E0f##l}rQZ7(wgcF8h z?xm_D8Xzn*jnL; zQJ677gk{k?LAYm*<6sTeGDxXqv3URBb<6h)Zbi&B%1$hV_K~LRO56SVy`AO^s9PLv zjHbuGmTjJFw>mS;uJ#Xf0(=BzvE@^z-JCmEy>{z!_iU>j(rqjs?42>4K#FrO52iQz zvVruczU;$j#>^eO*BlHJovM7W%1q0dnF)8u%*}NCZP_t_$Iv83lc@UgrHWQBpI_*t#k^ZeAt%R(rO(SY?n>bI>M@9?k0M8}+O{c<~^eHQpiV#2xwmDG4qCk7$my3Yx1B zDb^Gj0T0rnXax(}8EOT_A_%LgQP!KFMF3k%;^H{be-F_5|)%#FZAL&YQ$;qpGiKGjE$oDs>Rhnsrqr%F; zX{2+GSset5Mg~qTM~oN}6c>^}M{CTa2MS=NDV4PBrDZ{S=gmSV6er%?QOu7I4OLg@ zw#@2{X)?Fu2c{U1!5ZU8kkLt{e2h_RP@*b{41|K+q5!X%2$(TY7_bn76A)u`DbirA z+a&jy!_W}paznM@eZdx^kK5~6;K9b|Irbd+$N^0zARkZwgegfuY{R&Shj47SAY$2bU1?{HYqz?MzuNzmJ(qpBOzWzMp}p$UOQl))%0aeoY3B7STdl**F1bFyA34$Nw9FWu1Xxsm z`QOm93N**JO{S}lX1mhOJijvi^;6kstpfl8*3d~^w1#$VzohyET({L;SkrVYz5RpI zWH`hil@^hLYg|CodnPfmfeS=9V1Q5xvyc)DO%Z`UQ4os_=hADBQ)aWln=3WD0{lOP z0o7KNS$V27s_6U8SF>vN)Y8|_y&Sf$o!@q9Lrv0~q?`x}xI4#PKcCV^enV$XyQ z;8qwVK@%c;lvGM3wK77xV0cMSXd42^z_$3@Ouer@zkiVJ@pOvgs^%}2*R zcCwKjzM%(9k?z^pGm!55O@5%EVGN6M_604$o3y2 z8Yd0rsTTlE3)6%72yu!M0BSIV3oA6|PQ$o&gn0-mgh#3%v6zTRw4g95PMjJp*1tG> z@~#(NeQ@Kdfm^&kY#X3C*N`GC1`sO-Il;nVBa;+rN)3GMAw6IQ`Vgdy< zRjN3*xxVxfl5M}XwlcSix1Kc(K^O!hfGgGz?<7IoAdU$z)IlIAj~}aPWFra$)D(gs zO`LZY(HL}Ed;y}|#hEi))OK#|TgG0o^DEmnF299#x!+!m2+Um#^g|`41>=l58*LOY zX$Z%VL5UG?3b>u)V2c=p3zi$0f-vT7d6Tqj4u;b3DCiHF(nZPUTl^8XGTJGEp(KFfWbtR5Y@w$auMU`4-mf%qH<9TiN))R0JhJ4Q(Pe%tM@k@M|$JfmsZb6R2Uirrv1+rVH9@ z_|f6vM};ERnozubeso>AQ^GH2J9c*Wd|)lO@uZlfD_ibwM4*9zCU?pU3diTNECAF4 xKm?Y?fWrVOM^S4O;2cBXMjn15co?)Km}2pc!Q$92Hy7{yyuP&Yrk;=I{{~<@97O;C delta 145 zcmV;C0B--^%?-Mi4X{4}vuG1*L$k71o*c7Sb^TJ4*~u7}pJf74vw+EZeYd|y0UJA) znuP%rliwjyw;qK7t@xM8l>!Tw2BHBUw>y3UF;bVHF#!h*AR=ycZC`X~a%Ev;ml2== z8@GJX0-yquwgMctcGLpZ9FyFE3zsI80uYxcP5~;nkMROlaF^a?0|&RbWCJGxhyXe? diff --git a/apps/server/Cargo.toml b/apps/server/Cargo.toml index 2128535f7..bd1cccad6 100644 --- a/apps/server/Cargo.toml +++ b/apps/server/Cargo.toml @@ -6,5 +6,10 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +actix = "0.13.0" +actix-web = "4.0.1" +actix-web-actors = "4.1.0" sdcore = { path = "../../core" } -tokio = "1.17.0" +serde = "1.0.136" +serde_json = "1.0.79" +tokio = { version = "1.17.0", features = ["sync", "rt"] } diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile new file mode 100644 index 000000000..027305bb3 --- /dev/null +++ b/apps/server/Dockerfile @@ -0,0 +1,7 @@ +FROM gcr.io/distroless/cc + +COPY ./server /sdserver + +EXPOSE 8080 + +ENTRYPOINT [ "/sdserver" ] diff --git a/apps/server/src/main.rs b/apps/server/src/main.rs index d189cc588..835b10391 100644 --- a/apps/server/src/main.rs +++ b/apps/server/src/main.rs @@ -1,8 +1,166 @@ -use sdcore::Core; +use sdcore::{ClientCommand, ClientQuery, Core, CoreController, CoreEvent, CoreResponse}; use std::{env, path::Path}; -#[tokio::main] -async fn main() { +use actix::{ + Actor, AsyncContext, ContextFutureSpawner, Handler, Message, StreamHandler, + WrapFuture, +}; +use actix_web::{web, App, Error, HttpRequest, HttpResponse, HttpServer}; +use actix_web_actors::ws; +use serde::{Deserialize, Serialize}; + +use tokio::sync::mpsc; + +/// Define HTTP actor +struct Socket { + event_receiver: web::Data>, + core: web::Data, +} + +impl Actor for Socket { + type Context = ws::WebsocketContext; +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase", tag = "type", content = "data")] +enum SocketMessagePayload { + Command(ClientCommand), + Query(ClientQuery), +} + +#[derive(Serialize, Deserialize, Message)] +#[rtype(result = "()")] +#[serde(rename_all = "camelCase")] +struct SocketMessage { + id: String, + payload: SocketMessagePayload, +} + +impl StreamHandler> for Socket { + fn handle( + &mut self, + msg: Result, + ctx: &mut Self::Context, + ) { + // TODO: Add heartbeat and reconnect logic in the future. We can refer to https://github.com/actix/examples/blob/master/websockets/chat/src/session.rs for the heartbeat stuff. + + match msg { + Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), + Ok(ws::Message::Text(text)) => { + let msg: SocketMessage = serde_json::from_str(&text).unwrap(); + + ctx.notify(msg); + }, + _ => (), + } + } +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase", tag = "type", content = "data")] +pub enum SocketResponsePayload { + Query(CoreResponse), +} + +#[derive(Message, Serialize)] +#[rtype(result = "()")] +struct SocketResponse { + id: String, + payload: SocketResponsePayload, +} + +impl Handler for Socket { + type Result = (); + + fn handle(&mut self, msg: SocketResponse, ctx: &mut Self::Context) { + let string = serde_json::to_string(&msg).unwrap(); + println!("sending response: {string}"); + ctx.text(string); + } +} + +impl Handler for Socket { + type Result = (); + + fn handle(&mut self, msg: SocketMessage, ctx: &mut Self::Context) -> Self::Result { + let core = self.core.clone(); + + let recipient = ctx.address().recipient(); + + let fut = async move { + match msg.payload { + SocketMessagePayload::Query(query) => { + match core.query(query).await { + Ok(response) => recipient.do_send(SocketResponse { + id: msg.id.clone(), + payload: SocketResponsePayload::Query(response), + }), + Err(err) => { + // println!("query error: {:?}", err); + // Err(err.to_string()) + }, + }; + }, + SocketMessagePayload::Command(command) => { + match core.command(command).await { + Ok(response) => recipient.do_send(SocketResponse { + id: msg.id.clone(), + payload: SocketResponsePayload::Query(response), + }), + Err(err) => { + // println!("command error: {:?}", err); + // Err(err.to_string()) + }, + }; + }, + _ => {}, + } + }; + + fut.into_actor(self).spawn(ctx); + + () + } +} + +async fn index( + req: HttpRequest, + stream: web::Payload, + event_receiver: web::Data>, + controller: web::Data, +) -> Result { + let resp = ws::start( + Socket { + event_receiver, + core: controller, + }, + &req, + stream, + ); + println!("{:?}", resp); + resp +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let (event_receiver, controller) = setup().await; + + println!("Listening http://localhost:8080"); + HttpServer::new(move || { + App::new() + .app_data(event_receiver.clone()) + .app_data(controller.clone()) + .route("/ws", web::get().to(index)) + }) + .bind(("0.0.0.0", 8080))? + .run() + .await +} + +async fn setup() -> ( + web::Data>, + web::Data, +) { let data_dir_var = "DATA_DIR"; let data_dir = match env::var(data_dir_var) { Ok(path) => path, @@ -11,7 +169,7 @@ async fn main() { let data_dir_path = Path::new(&data_dir); - let (mut core, mut event_receiver) = Core::new(data_dir_path.to_path_buf()).await; + let (mut core, event_receiver) = Core::new(data_dir_path.to_path_buf()).await; core.initializer().await; @@ -19,7 +177,7 @@ async fn main() { tokio::spawn(async move { core.start().await; - }) - .await - .unwrap(); + }); + + (web::Data::new(event_receiver), web::Data::new(controller)) } diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index d610821cf..b3a3b0b6b 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,16 +1,65 @@ import React from 'react'; import SpacedriveInterface from '@sd/interface'; -import { ClientCommand, ClientQuery } from '@sd/core'; +import { ClientCommand, ClientQuery, CoreEvent } from '@sd/core'; import { BaseTransport } from '@sd/client'; +const websocket = new WebSocket('ws://localhost:8080/ws'); + +const randomId = () => Math.random().toString(36).slice(2); + // bind state to core via Tauri class Transport extends BaseTransport { - async query(query: ClientQuery) { - // return await invoke('client_query_transport', { data: query }); + requestMap = new Map void>(); + + constructor() { + super(); + + websocket.addEventListener('message', (event) => { + if (!event.data) return; + + const { id, payload } = JSON.parse(event.data); + + const { type, data } = payload; + if (type === 'event') { + this.emit('core_event', data); + } else if (type === 'query' || type === 'command') { + if (this.requestMap.has(id)) { + this.requestMap.get(id)?.(data); + this.requestMap.delete(id); + } + } + }); } - async command(query: ClientCommand) { - // return await invoke('client_command_transport', { data: query }); + async query(query: ClientQuery) { + const id = randomId(); + let resolve: (data: any) => void; + + const promise = new Promise((res) => { + resolve = res; + }); + + // @ts-ignore + this.requestMap.set(id, resolve); + + websocket.send(JSON.stringify({ id, payload: { type: 'query', data: query } })); + + return await promise; + } + async command(command: ClientCommand) { + const id = randomId(); + let resolve: (data: any) => void; + + const promise = new Promise((res) => { + resolve = res; + }); + + // @ts-ignore + this.requestMap.set(id, resolve); + + websocket.send(JSON.stringify({ id, payload: { type: 'command', data: command } })); + + return await promise; } } @@ -20,9 +69,6 @@ function App() { {/*
*/}
- {row.is_dir ? ( - - ) : ( - hasThumbnail && - location?.data_path && ( - - ) - )} +
{/* {colKey == 'name' && (() => { diff --git a/packages/interface/src/components/file/FileThumb.tsx b/packages/interface/src/components/file/FileThumb.tsx index 1c6485942..bbbf9db22 100644 --- a/packages/interface/src/components/file/FileThumb.tsx +++ b/packages/interface/src/components/file/FileThumb.tsx @@ -3,7 +3,7 @@ import { FilePath } from '@sd/core'; import clsx from 'clsx'; import React, { useContext } from 'react'; import { AppPropsContext } from '../../App'; - +import icons from '../../assets/icons'; import { ReactComponent as Folder } from '../../assets/svg/folder.svg'; export default function FileThumb(props: { @@ -15,7 +15,7 @@ export default function FileThumb(props: { const { data: client } = useBridgeQuery('ClientGetState'); if (props.file.is_dir) { - return ; + return ; } if (props.file.has_local_thumbnail && client?.data_path) { @@ -29,5 +29,9 @@ export default function FileThumb(props: { ); } + if (icons[props.file.extension as keyof typeof icons]) { + let Icon = icons[props.file.extension as keyof typeof icons]; + return ; + } return
; } diff --git a/packages/interface/src/components/file/Sidebar.tsx b/packages/interface/src/components/file/Sidebar.tsx index f08d3de51..a6274fb34 100644 --- a/packages/interface/src/components/file/Sidebar.tsx +++ b/packages/interface/src/components/file/Sidebar.tsx @@ -58,8 +58,9 @@ export function MacOSTrafficLights() { export const Sidebar: React.FC = (props) => { const appPropsContext = useContext(AppPropsContext); - const { data: locations } = useBridgeQuery('SysGetLocations'); + const { data: locations } = useBridgeQuery('SysGetLocations', undefined, {}); const { mutate: createLocation } = useBridgeCommand('LocCreate'); + const { data: clientState } = useBridgeQuery('ClientGetState', undefined, {}); const tags = [ { id: 1, name: 'Keepsafe', color: '#FF6788' }, @@ -92,9 +93,9 @@ export const Sidebar: React.FC = (props) => { variant: 'gray' }} // buttonIcon={} - buttonText="Jeff's Library" + buttonText={clientState?.client_name || 'Loading...'} items={[ - [{ name: `Jeff's Library`, selected: true }, { name: 'Private Library' }], + [{ name: clientState?.client_name || '', selected: true }, { name: 'Private Library' }], [ { name: 'Library Settings', icon: CogIcon }, { name: 'Add Library', icon: PlusIcon },