upload works
This commit is contained in:
parent
dcfb286f10
commit
bf2b5a313c
10 changed files with 288 additions and 7 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -9,9 +9,5 @@ export default function App() {
|
|||
location.href = `/${v4()}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
Hellosu, <Room roomId={roomId} />
|
||||
</>
|
||||
);
|
||||
return <Room roomId={roomId} />;
|
||||
}
|
||||
|
|
|
@ -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
194
client/Upload.tsx
Normal 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
22
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
33
src/main.rs
33
src/main.rs
|
@ -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;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
{
|
||||
"compilerOptions": { "lib": ["ESNext", "DOM"], "jsx": "react-jsx" }
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue