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

@ -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});