upload works

This commit is contained in:
Michael Zhang 2023-08-10 17:55:32 -05:00
parent dcfb286f10
commit bf2b5a313c
10 changed files with 288 additions and 7 deletions

10
Cargo.lock generated
View file

@ -512,6 +512,7 @@ dependencies = [
"futures",
"mzlib",
"serde",
"serde_bytes",
"serde_json",
"tokio",
"uuid",
@ -972,6 +973,15 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde_bytes"
version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff"
dependencies = [
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.183"

View file

@ -14,6 +14,7 @@ derivative = "2.2.0"
flume = "0.10.14"
futures = "0.3.28"
serde = { version = "1.0.183", features = ["derive"] }
serde_bytes = "0.11.12"
serde_json = "1.0.104"
tokio = { version = "1.30.0", features = ["full"] }
uuid = "1.4.1"

View file

@ -9,9 +9,5 @@ export default function App() {
location.href = `/${v4()}`;
}
return (
<>
Hellosu, <Room roomId={roomId} />
</>
);
return <Room roomId={roomId} />;
}

View file

@ -3,6 +3,8 @@ import { useEffect, useState } from "react";
import useWebSocket, { ReadyState } from "react-use-websocket";
import * as Automerge from "@automerge/automerge";
import * as uuid from "uuid";
import {} from "libsodium";
import Upload from "./Upload";
const connectionStatusMap = {
[ReadyState.CONNECTING]: "Connecting",
@ -33,6 +35,7 @@ export default function Room({ roomId }) {
lastJsonMessage,
readyState: newReadyState,
} = useWebSocket("ws://localhost:3100/ws", {
share: true,
onOpen: ({}) => {
console.log("Shiet, connected.");
},
@ -130,6 +133,8 @@ export default function Room({ roomId }) {
return (
<>
<Upload roomId={roomId} />
<hr />
<p>Room Id: {roomId}</p>
<p>Connection status: {connectionStatus}</p>
Connected:

194
client/Upload.tsx Normal file
View file

@ -0,0 +1,194 @@
import { useCallback, useEffect, useState } from "react";
import * as sodium from "libsodium-wrappers";
import useWebSocket from "react-use-websocket";
import CBOR from "cbor";
export default function Upload({ roomId }) {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [encryptionKey, setEncryptionKey] = useState(null);
const [uploadProgress, setUploadProgress] = useState(null);
const [decryptionKey, setDecryptionKey] = useState("");
const [selectedSaveFile, setSelectedSaveFile] = useState(null);
const [decryptState, setDecryptState] = useState(null);
const [pendingDecryption, setPendingDecryption] = useState([]);
let decryptionKeyInternal: Uint8Array;
if (decryptionKey) {
decryptionKeyInternal = hex2Bytes(decryptionKey);
}
const [writeStream, setWriteStream] = useState(null);
const readyToDownload =
!!decryptionKeyInternal && !!selectedSaveFile && !!writeStream;
const {
sendMessage,
lastJsonMessage,
readyState: newReadyState,
} = useWebSocket("ws://localhost:3100/ws", {
share: true,
onMessage: async (event) => {
const message = CBOR.decode(await event.data.arrayBuffer());
if (message.type === "FileUploadBegin") {
const state = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
message.header,
decryptionKeyInternal
);
console.log("header", message.header);
console.log("state", state);
setDecryptState(state);
}
if (message.type === "FileUploadChunk") {
console.log("data", message);
if (readyToDownload && decryptState) {
for (const { data } of [...pendingDecryption, message]) {
const { message } =
sodium.crypto_secretstream_xchacha20poly1305_pull(
decryptState,
data
);
const result = await writeStream.write(message);
console.log("output", result, message);
}
setPendingDecryption([]);
} else {
setPendingDecryption([...pendingDecryption, message]);
}
}
if (message.type === "FileUploadComplete") {
await writeStream.close();
console.log("done!");
}
},
});
function sendCborMessage(data) {
let cbor = CBOR.encode(data);
sendMessage(cbor);
}
useEffect(() => {
(async () => {
await sodium.ready;
const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
setEncryptionKey(key);
})();
}, []);
const uploadFile = useCallback(async () => {
if (!encryptionKey) return;
if (!selectedFile) return;
console.log("file", selectedFile);
const { state, header } =
sodium.crypto_secretstream_xchacha20poly1305_init_push(encryptionKey);
sendCborMessage({
type: "FileUploadBegin",
room_id: roomId,
header,
});
const stream = selectedFile.stream();
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Encrypt this chunk
const encryptedValue =
sodium.crypto_secretstream_xchacha20poly1305_push(
state,
value,
null,
0
);
sendCborMessage({
type: "FileUploadChunk",
room_id: roomId,
data: encryptedValue,
});
console.log("sent encrypted chunk", encryptedValue);
}
sendCborMessage({
type: "FileUploadComplete",
room_id: roomId,
});
console.log("sent done");
} finally {
reader.releaseLock();
}
}, [encryptionKey, selectedFile]);
async function selectSaveFile() {
const newHandle = await window.showSaveFilePicker();
console.log("handle", newHandle);
setSelectedSaveFile(newHandle);
const writableStream = await newHandle.createWritable();
setWriteStream(writableStream);
}
return (
<div>
<h3>Send file</h3>
<p>Upload Key: {encryptionKey && bytes2Hex(encryptionKey)}</p>
<input type="file" onChange={(e) => setSelectedFile(e.target.files[0])} />
<button onClick={uploadFile}>upload</button>
<hr />
<h3>Receive file</h3>
<p>
<ol>
<li>
<input
type="text"
value={decryptionKey}
onChange={(e) => setDecryptionKey(e.target.value)}
placeholder="Download Key..."
/>
</li>
<li>
<button onClick={selectSaveFile} disabled={!!selectedSaveFile}>
{selectedSaveFile
? `Saving to "${selectedSaveFile.name}"`
: "select save file"}
</button>
</li>
</ol>
(
{readyToDownload
? "you are ready to download"
: "you are not ready to download, make sure you enter the key and select a place to save"}
)
</p>
</div>
);
}
function bytes2Hex(byteArray: Uint8Array): string {
return Array.prototype.map
.call(byteArray, function (byte: number) {
return ("0" + (byte & 0xff).toString(16)).slice(-2);
})
.join("");
}
function hex2Bytes(hexString: string): Uint8Array {
let result = [];
for (var i = 0; i < hexString.length; i += 2) {
result.push(parseInt(hexString.substr(i, 2), 16));
}
return new Uint8Array(result);
}

22
package-lock.json generated
View file

@ -7,6 +7,8 @@
"dependencies": {
"@automerge/automerge": "^2.0.3",
"cbor": "npm:@jprochazk/cbor@^0.5.0",
"libsodium": "^0.7.11",
"libsodium-wrappers": "^0.7.11",
"localforage": "^1.10.0",
"match-sorter": "^6.3.1",
"react": "^18.2.0",
@ -17,6 +19,7 @@
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/libsodium-wrappers": "^0.7.10",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@types/uuid": "^9.0.2",
@ -1011,6 +1014,12 @@
"node": ">=10"
}
},
"node_modules/@types/libsodium-wrappers": {
"version": "0.7.10",
"resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz",
"integrity": "sha512-BqI9B92u+cM3ccp8mpHf+HzJ8fBlRwdmyd6+fz3p99m3V6ifT5O3zmOMi612PGkpeFeG/G6loxUnzlDNhfjPSA==",
"dev": true
},
"node_modules/@types/prop-types": {
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
@ -1331,6 +1340,19 @@
"node": ">=6"
}
},
"node_modules/libsodium": {
"version": "0.7.11",
"resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.11.tgz",
"integrity": "sha512-WPfJ7sS53I2s4iM58QxY3Inb83/6mjlYgcmZs7DJsvDlnmVUwNinBCi5vBT43P6bHRy01O4zsMU2CoVR6xJ40A=="
},
"node_modules/libsodium-wrappers": {
"version": "0.7.11",
"resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.11.tgz",
"integrity": "sha512-SrcLtXj7BM19vUKtQuyQKiQCRJPgbpauzl3s0rSwD+60wtHqSUuqcoawlMDheCJga85nKOQwxNYQxf/CKAvs6Q==",
"dependencies": {
"libsodium": "^0.7.11"
}
},
"node_modules/lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",

View file

@ -3,6 +3,7 @@
"dev": "vite"
},
"devDependencies": {
"@types/libsodium-wrappers": "^0.7.10",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@types/uuid": "^9.0.2",
@ -15,6 +16,8 @@
"dependencies": {
"@automerge/automerge": "^2.0.3",
"cbor": "npm:@jprochazk/cbor@^0.5.0",
"libsodium": "^0.7.11",
"libsodium-wrappers": "^0.7.11",
"localforage": "^1.10.0",
"match-sorter": "^6.3.1",
"react": "^18.2.0",

View file

@ -48,6 +48,7 @@ struct Room {
document: Automerge,
sync_state: SyncState,
connected_clients: DashSet<Uuid>,
current_uploader: Option<Uuid>,
}
impl Room {
@ -193,7 +194,6 @@ async fn handle_socket(state: AppState, socket: WebSocket) -> Result<()> {
.0
.entry(room_id.clone())
.or_insert_with(|| Room::default());
rooms.insert(room_id.clone());
// Add to the list of connected clients
let connected_clients = room.connected_clients.clone();
@ -203,6 +203,37 @@ async fn handle_socket(state: AppState, socket: WebSocket) -> Result<()> {
// Broadcast to the room
broadcast(&state, connected_clients.clone(), message).await;
}
Message::FileUploadBegin { room_id, .. }
| Message::FileUploadComplete { room_id }
| Message::FileUploadChunk { room_id, .. } => {
let mut room = state
.rooms
.0
.entry(room_id.clone())
.or_insert_with(|| Room::default());
let connected_clients = room.connected_clients.clone();
connected_clients.remove(&client_id);
match &message {
Message::FileUploadBegin { header, .. } => {
match room.current_uploader {
Some(v) if v == client_id => {}
Some(v) => continue,
None => room.current_uploader = Some(client_id.clone()),
}
}
Message::FileUploadComplete { .. } => {
room.current_uploader = None;
}
Message::FileUploadChunk { data, .. } => {}
_ => {}
};
broadcast(&state, connected_clients, message).await;
}
_ => {}
};
}

View file

@ -38,6 +38,19 @@ pub enum Message {
#[derivative(Debug = "ignore")]
message: AutomergeMessage,
},
FileUploadBegin {
room_id: Uuid,
#[serde(with = "serde_bytes")]
header: Vec<u8>,
},
FileUploadChunk {
room_id: Uuid,
#[serde(with = "serde_bytes")]
data: Vec<u8>,
},
FileUploadComplete {
room_id: Uuid,
},
}
impl Message {

View file

@ -1,3 +1,9 @@
{
"compilerOptions": { "lib": ["ESNext", "DOM"], "jsx": "react-jsx" }
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "NodeNext",
"jsx": "react-jsx"
}
}