From 8137fb514615051f880060dbb9a31e59d6dcebac Mon Sep 17 00:00:00 2001 From: Claas Date: Sun, 10 Nov 2024 20:31:28 +0100 Subject: [PATCH] Fix preview not preserving in Safari --- app/src/components/Camera.jsx | 52 ++++++++++++++++++++++++++++++---- app/src/components/Preview.jsx | 2 +- app/src/workers/saveFile.js | 27 ++++++++++++++++++ app/src/workers/tsconfig.json | 8 ++++++ app/tsconfig.json | 3 +- app/vite.config.js | 7 +++++ delivery-service/Cargo.toml | 2 +- delivery-service/src/main.rs | 14 ++++++++- 8 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 app/src/workers/saveFile.js create mode 100644 app/src/workers/tsconfig.json diff --git a/app/src/components/Camera.jsx b/app/src/components/Camera.jsx index 3b258f6..3f4368f 100644 --- a/app/src/components/Camera.jsx +++ b/app/src/components/Camera.jsx @@ -10,6 +10,14 @@ import { //@ts-expect-error TS6192 Can not handle new JSDoc syntax (yet?) // https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#the-jsdoc-@import-tag /** @import { Match, Signal } from "solid-js" */ +// import Worker from "../workers/saveFile.js?worker&url"; + +const fileWorker = new Worker( + new URL("../workers/saveFile.js", import.meta.url), + { + type: "module", + } +); export default function Camera() { // 1. Ask for permission in app @@ -171,12 +179,46 @@ export default function Camera() { const fileHandle = await directory.getFileHandle("preview", { create: true, }); - const writer = await fileHandle.createWritable({ keepExistingData: false }); - await writer.write(blob); - // Don't forget to close the writer to persist the file - await writer.close(); - console.debug("Wrote preview to file"); + //TODO Only persist image through restarts if origin private file system is supported and writable without a worker + if ("createWritable" in fileHandle) { + // The chromium and firefox case + const writer = await fileHandle.createWritable({ + keepExistingData: false, + }); + await writer.write(blob); + // Don't forget to close the writer to persist the file + await writer.close(); + } else { + // The safari case + let buffer; + if (crossOriginIsolated) { + // Might be faster to create an object url but that requires cleanup + // Need to test which is faster + buffer = new SharedArrayBuffer(blob.size); + //TODO is this copying the buffer? Does this even have any benefit? + const view = new Uint8Array(buffer); + const blobBuffer = await blob.arrayBuffer(); + view.set(new Uint8Array(blobBuffer)); + } else { + buffer = await blob.arrayBuffer(); + } + + // Create promise before posting message to worker to avoid (unlikely) race condition + // Wait for worker to signal it is finished + // If we don't do this the worker is disposed from navigating before it is finished + const save = /** @type {Promise} */ ( + new Promise((resolve) => + fileWorker.addEventListener("message", () => resolve(), { + once: true, + }) + ) + ); + + fileWorker.postMessage(buffer); + + await save; + } // Navigate to preview navigate("/preview"); diff --git a/app/src/components/Preview.jsx b/app/src/components/Preview.jsx index 2a5a94e..a79cd1e 100644 --- a/app/src/components/Preview.jsx +++ b/app/src/components/Preview.jsx @@ -61,7 +61,7 @@ export default function Preview() { }); return ( -
+
{/* */}
diff --git a/app/src/workers/saveFile.js b/app/src/workers/saveFile.js new file mode 100644 index 0000000..ac4a341 --- /dev/null +++ b/app/src/workers/saveFile.js @@ -0,0 +1,27 @@ +// A worker to persist files on the origin private file system in Safari +// Safari does not support the FileSystemHandle.createWritable() method +addEventListener("message", async (event) => { + if ( + !(event.data instanceof SharedArrayBuffer) && + !(event.data instanceof ArrayBuffer) + ) { + console.error("Expected SharedArrayBuffer"); + return; + } + + const directory = await navigator.storage.getDirectory(); + + const fileHandle = await directory.getFileHandle("preview", { + create: true, + }); + + const handle = await fileHandle.createSyncAccessHandle(); + // Clear file + handle.truncate(0); + // Write new data + handle.write(event.data); + handle.flush(); + handle.close(); + + postMessage(undefined); +}); diff --git a/app/src/workers/tsconfig.json b/app/src/workers/tsconfig.json new file mode 100644 index 0000000..ae24e0c --- /dev/null +++ b/app/src/workers/tsconfig.json @@ -0,0 +1,8 @@ +{ + "include": ["."], + "compilerOptions": { + "lib": ["ESNext", "WebWorker"], + "checkJs": true, + "noEmit": true + } +} diff --git a/app/tsconfig.json b/app/tsconfig.json index 63086c9..402375f 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2022", + "target": "ES2023", "useDefineForClassFields": true, "module": "ESNext", "lib": ["ES2022", "DOM", "DOM.Iterable"], @@ -24,5 +24,6 @@ // "types": ["vite/client"] }, "include": ["src"], + "exclude": ["src/workers"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/app/vite.config.js b/app/vite.config.js index 0018f6a..651ae3f 100644 --- a/app/vite.config.js +++ b/app/vite.config.js @@ -14,6 +14,13 @@ export default defineConfig({ ws: true, }, }, + + headers: { + // Support SharedArrayBuffer to send files to workers which is required for Safari to write files to the private origin file system + // Also might be nice to increase security + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp", + }, }, build: { // Fixes top level await build error and support should be fine, right?...right? diff --git a/delivery-service/Cargo.toml b/delivery-service/Cargo.toml index d36122d..70b8b2e 100644 --- a/delivery-service/Cargo.toml +++ b/delivery-service/Cargo.toml @@ -9,6 +9,6 @@ serde = { workspace = true, features = ["derive", "rc"] } thiserror = { workspace = true } tls_codec = { workspace = true } tokio = { workspace = true, features = ["full"] } -tower-http = { workspace = true, features = ["fs"] } +tower-http = { workspace = true, features = ["fs", "set-header"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } diff --git a/delivery-service/src/main.rs b/delivery-service/src/main.rs index 7012fd1..acaa418 100644 --- a/delivery-service/src/main.rs +++ b/delivery-service/src/main.rs @@ -6,7 +6,7 @@ use axum::{ ws::{self, WebSocket}, Path, State, WebSocketUpgrade, }, - http::StatusCode, + http::{header, HeaderName, HeaderValue, StatusCode}, response::IntoResponse, routing::{get, post}, Router, @@ -17,6 +17,7 @@ use tokio::{ }; use tower_http::{ services::{ServeDir, ServeFile}, + set_header::SetResponseHeaderLayer, set_status::SetStatus, }; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -83,6 +84,17 @@ async fn main() { .route("/messages/:to", post(create_message)) .route("/messages/:to", get(subscribe_messages)) .fallback_service(serve_client()) + // Support SharedArrayBuffer to send files to workers which is required for Safari to write files to the private origin file system + // Also might be nice to increase security + //TODO how to logically combine these two layers? + .layer(SetResponseHeaderLayer::if_not_present( + HeaderName::from_static("Cross-Origin-Opener-Policy"), + HeaderValue::from_static("same-origin"), + )) + .layer(SetResponseHeaderLayer::if_not_present( + HeaderName::from_static("Cross-Origin-Embedder-Policy"), + HeaderValue::from_static("require-corp"), + )) .with_state(Default::default()); // It might become annoying to get the mac pop up for running on 0.0.0.0 so change to localhost instead -- 2.51.2