Move service worker digestion into separate class and add static function to check if it is supported by the browser. Change ram-exceed-ios waring accordingly.

This commit is contained in:
schlagmichdoch 2024-02-15 18:05:48 +01:00
parent 90f10910aa
commit f4a947527d
3 changed files with 178 additions and 105 deletions

View file

@ -164,7 +164,7 @@
"selected-peer-left": "Selected peer left",
"error-sharing-size": "Files too big to be shared. They can be downloaded instead.",
"error-sharing-default": "Error while sharing. It can be downloaded instead.",
"ram-exceed-ios": "File is bigger than 250 MB and will crash the page on iOS. Use https to prevent this."
"ram-exceed-ios": "One of the files is bigger than 250 MB and will crash the page on iOS. Use https and do not use private tabs on the iOS device to prevent this."
},
"document-titles": {
"file-received": "File Received",

View file

@ -484,7 +484,7 @@ class Peer {
_sendData(data) {}
_onMessage(message) {
async _onMessage(message) {
switch (message.type) {
case 'display-name-changed':
this._onDisplayNameChanged(message);
@ -493,7 +493,7 @@ class Peer {
this._onState(message.state);
break;
case 'transfer-request':
this._onTransferRequest(message);
await this._onTransferRequest(message);
break;
case 'transfer-request-response':
this._onTransferRequestResponse(message);
@ -740,44 +740,44 @@ class Peer {
}
// File Receiver Only
_onTransferRequest(request) {
async _onTransferRequest(request) {
// Only accept one request at a time per peer
if (this._pendingRequest) {
// Only accept one request at a time per peer
this._sendTransferRequestResponse(false);
return;
}
// Check if each file must be loaded into RAM completely. This might lead to a page crash (Memory limit iOS Safari: ~380 MB)
if (!(await FileDigesterWorker.isSupported())) {
Logger.warn('Big file transfers might exceed the RAM of the receiver. Use a secure context (https) and do not use private tabs to prevent this.');
// Check if page will crash on iOS
if (window.iOS && await this._filesTooBigForSwOnIOS(request.header)) {
Events.fire('notify-user', Localization.getTranslation('notifications.ram-exceed-ios'));
// Would exceed RAM -> decline request
this._sendTransferRequestResponse(false, 'ram-exceed-ios');
return;
}
}
this._pendingRequest = request;
if (!window.Worker || !window.isSecureContext) {
// Each file must be loaded into RAM completely which might lead to a page crash (Memory limit iOS Safari: ~380 MB)
Logger.warn('Big file transfers might exceed the RAM of the receiver. Use a secure context (https) to prevent this.');
}
if (window.iOS && this._filesTooBigForIos(request.header)) {
// Page will crash. Decline request
Events.fire('notify-user', Localization.getTranslation('notifications.ram-exceed-ios'));
this._sendTransferRequestResponse(false, 'ram-exceed-ios');
return;
}
// Automatically accept request if auto-accept is set to true via the Edit Paired Devices Dialog
if (this._autoAccept) {
// auto accept if set via Edit Paired Devices Dialog
this._sendTransferRequestResponse(true);
return;
}
// default behavior: show user transfer request
// Default behavior: show transfer request to user
Events.fire('files-transfer-request', {
request: request,
peerId: this._peerId
});
}
_filesTooBigForIos(files) {
if (window.Worker && window.isSecureContext) {
return false;
}
async _filesTooBigForSwOnIOS(files) {
// Files over 250 MB crash safari if not handled via a service worker
for (let i = 0; i < files.length; i++) {
if (files[i].size > 250000000) {
return true;
@ -1313,7 +1313,7 @@ class RTCPeer extends Peer {
this._state = Peer.STATE_TRANSFER_PROCEEDING;
}
_onMessage(message) {
async _onMessage(message) {
Logger.debug('RTC Receive:', JSON.parse(message));
try {
message = JSON.parse(message);
@ -1321,7 +1321,7 @@ class RTCPeer extends Peer {
Logger.warn("RTCPeer: Received JSON is malformed");
return;
}
super._onMessage(message);
await super._onMessage(message);
}
getConnectionHash() {
@ -1412,9 +1412,9 @@ class WSPeer extends Peer {
this._sendSignal(true);
}
_onMessage(message) {
async _onMessage(message) {
Logger.debug('WS Receive:', message);
super._onMessage(message);
await super._onMessage(message);
}
_onWsRelay(message) {
@ -1847,12 +1847,14 @@ class FileDigester {
if (this._bytesReceived < this._size) return;
// We are done receiving. Preferably use a file worker to process the file to prevent exceeding of available RAM
if (!window.Worker && !window.isSecureContext) {
this.processFileViaMemory();
return;
}
this.processFileViaWorker();
FileDigesterWorker.isSupported()
.then(supported => {
if (!supported) {
this.processFileViaMemory();
return;
}
this.processFileViaWorker();
});
}
processFileViaMemory() {
@ -1865,88 +1867,146 @@ class FileDigester {
}
processFileViaWorker() {
// Use service worker to prevent loading the complete file into RAM
const fileWorker = new Worker("scripts/sw-file-digester.js");
let i = 0;
let offset = 0;
const _this = this;
function sendPart(buffer, offset) {
fileWorker.postMessage({
type: "part",
name: _this._name,
buffer: buffer,
offset: offset
});
}
function getFile() {
fileWorker.postMessage({
type: "get-file",
name: _this._name,
});
}
function deleteFile() {
fileWorker.postMessage({
type: "delete-file",
name: _this._name
const fileDigesterWorker = new FileDigesterWorker();
fileDigesterWorker.digestFileBuffer(this._buffer, this._name)
.then(file => {
this._fileCompleteCallback(file);
})
}
.catch(reason => {
Logger.warn(reason);
this.processFileViaWorker();
})
}
}
function onPart(part) {
if (i < _this._buffer.length - 1) {
// process next chunk
offset += part.byteLength;
i++;
sendPart(_this._buffer[i], offset);
return;
}
class FileDigesterWorker {
// File processing complete -> retrieve completed file
getFile();
}
constructor() {
// Use service worker to prevent loading the complete file into RAM
this.fileWorker = new Worker("scripts/sw-file-digester.js");
function onFile(file) {
_this._buffer = [];
_this._fileCompleteCallback(file);
deleteFile();
}
function onFileDeleted() {
// File Digestion complete -> Tidy up
fileWorker.terminate();
}
function onError(error) {
// an error occurred.
Logger.error(error);
Logger.warn('Failed to process file via service-worker. Do not use Firefox private mode to prevent this.')
// Use memory method instead and terminate service worker.
fileWorker.terminate();
_this.processFileViaMemory();
}
sendPart(this._buffer[i], offset);
fileWorker.onmessage = (e) => {
this.fileWorker.onmessage = (e) => {
switch (e.data.type) {
case "support":
this.onSupport(e.data.supported);
break;
case "part":
onPart(e.data.part);
this.onPart(e.data.part);
break;
case "file":
onFile(e.data.file);
this.onFile(e.data.file);
break;
case "file-deleted":
onFileDeleted();
this.onFileDeleted();
break;
case "error":
onError(e.data.error);
this.onError(e.data.error);
break;
}
}
}
}
static isSupported() {
// Check if web worker is supported and supports specific functions
return new Promise(async resolve => {
if (!window.Worker || !window.isSecureContext) {
resolve(false);
return;
}
const fileDigesterWorker = new FileDigesterWorker();
resolve(await fileDigesterWorker.checkSupport());
fileDigesterWorker.fileWorker.terminate();
})
}
checkSupport() {
return new Promise(resolve => {
this.resolveSupport = resolve;
this.fileWorker.postMessage({
type: "check-support"
});
})
}
onSupport(supported) {
if (!this.resolveSupport) return;
this.resolveSupport(supported);
this.resolveSupport = null;
}
digestFileBuffer(buffer, fileName) {
return new Promise((resolve, reject) => {
this.resolveFile = resolve;
this.rejectFile = reject;
this.i = 0;
this.offset = 0;
this.buffer = buffer;
this.fileName = fileName;
this.sendPart(this.buffer[0], 0);
})
}
sendPart(buffer, offset) {
this.fileWorker.postMessage({
type: "part",
name: this.fileName,
buffer: buffer,
offset: offset
});
}
getFile() {
this.fileWorker.postMessage({
type: "get-file",
name: this.fileName,
});
}
deleteFile() {
this.fileWorker.postMessage({
type: "delete-file",
name: this.fileName
})
}
onPart(part) {
if (this.i < this.buffer.length - 1) {
// process next chunk
this.offset += part.byteLength;
this.i++;
this.sendPart(this.buffer[this.i], this.offset);
return;
}
// File processing complete -> retrieve completed file
this.getFile();
}
onFile(file) {
this.buffer = [];
this.resolveFile(file);
this.deleteFile();
}
onFileDeleted() {
// File Digestion complete -> Tidy up
this.fileWorker.terminate();
}
onError(error) {
// an error occurred.
Logger.error(error);
// Use memory method instead and terminate service worker.
this.fileWorker.terminate();
this.rejectFile("Failed to process file via service-worker. Do not use Firefox private mode to prevent this.");
}
}

View file

@ -1,14 +1,17 @@
self.addEventListener('message', async e => {
try {
switch (e.data.type) {
case "check-support":
await checkSupport();
break;
case "part":
await this.onPart(e.data.name, e.data.buffer, e.data.offset);
await onPart(e.data.name, e.data.buffer, e.data.offset);
break;
case "get-file":
await this.onGetFile(e.data.name);
await onGetFile(e.data.name);
break;
case "delete-file":
await this.onDeleteFile(e.data.name);
await onDeleteFile(e.data.name);
break;
}
}
@ -17,6 +20,16 @@ self.addEventListener('message', async e => {
}
})
async function checkSupport() {
try {
await getAccessHandle("test.txt");
self.postMessage({type: "support", supported: true});
}
catch (e) {
self.postMessage({type: "support", supported: false});
}
}
async function getFileHandle(fileName) {
const root = await navigator.storage.getDirectory();
return await root.getFileHandle(fileName, {create: true});