mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2025-04-20 15:06:15 -04:00
Rewrite FileDigester to tidy up code, be able to delete files in OPFS onPageHide and on abort of file transfer
This commit is contained in:
parent
fa86212139
commit
76c47c9623
5 changed files with 346 additions and 127 deletions
|
@ -25,6 +25,11 @@ class BrowserTabsConnector {
|
||||||
: false;
|
: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static isOnlyTab() {
|
||||||
|
let peerIdsBrowser = JSON.parse(localStorage.getItem('peer_ids_browser'));
|
||||||
|
return peerIdsBrowser.length <= 1;
|
||||||
|
}
|
||||||
|
|
||||||
static async addPeerIdToLocalStorage() {
|
static async addPeerIdToLocalStorage() {
|
||||||
const peerId = sessionStorage.getItem('peer_id');
|
const peerId = sessionStorage.getItem('peer_id');
|
||||||
if (!peerId) return false;
|
if (!peerId) return false;
|
||||||
|
|
|
@ -352,7 +352,7 @@ class Peer {
|
||||||
|
|
||||||
clearInterval(this._updateStatusTextInterval);
|
clearInterval(this._updateStatusTextInterval);
|
||||||
|
|
||||||
this._transferStatusInterval = null;
|
this._updateStatusTextInterval = null;
|
||||||
this._bytesTotal = 0;
|
this._bytesTotal = 0;
|
||||||
this._bytesReceivedFiles = 0;
|
this._bytesReceivedFiles = 0;
|
||||||
this._timeStartTransferComplete = null;
|
this._timeStartTransferComplete = null;
|
||||||
|
@ -366,9 +366,13 @@ class Peer {
|
||||||
// tidy up receiver
|
// tidy up receiver
|
||||||
this._pendingRequest = null;
|
this._pendingRequest = null;
|
||||||
this._acceptedRequest = null;
|
this._acceptedRequest = null;
|
||||||
this._digester = null;
|
|
||||||
this._filesReceived = [];
|
this._filesReceived = [];
|
||||||
|
|
||||||
|
if (this._digester) {
|
||||||
|
this._digester.cleanUp();
|
||||||
|
this._digester = null;
|
||||||
|
}
|
||||||
|
|
||||||
// disable NoSleep if idle
|
// disable NoSleep if idle
|
||||||
Events.fire('evaluate-no-sleep');
|
Events.fire('evaluate-no-sleep');
|
||||||
}
|
}
|
||||||
|
@ -625,6 +629,11 @@ class Peer {
|
||||||
|
|
||||||
_abortTransfer() {
|
_abortTransfer() {
|
||||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'error'});
|
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'error'});
|
||||||
|
|
||||||
|
if (this._digester) {
|
||||||
|
this._digester.abort();
|
||||||
|
}
|
||||||
|
|
||||||
this._reset();
|
this._reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -713,7 +722,7 @@ class Peer {
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
header.push({
|
header.push({
|
||||||
name: files[i].name,
|
displayName: files[i].name,
|
||||||
mime: files[i].type,
|
mime: files[i].type,
|
||||||
size: files[i].size
|
size: files[i].size
|
||||||
});
|
});
|
||||||
|
@ -796,7 +805,7 @@ class Peer {
|
||||||
this._sendMessage({
|
this._sendMessage({
|
||||||
type: 'transfer-header',
|
type: 'transfer-header',
|
||||||
size: file.size,
|
size: file.size,
|
||||||
name: file.name,
|
displayName: file.name,
|
||||||
mime: file.type
|
mime: file.type
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -866,8 +875,12 @@ class Peer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.fileDigesterWorkerSupported = await SWFileDigester.isSupported();
|
||||||
|
|
||||||
|
Logger.debug('Digesting files via service workers is', this.fileDigesterWorkerSupported ? 'supported' : 'NOT supported');
|
||||||
|
|
||||||
// Check if each file must be loaded into RAM completely. This might lead to a page crash (Memory limit iOS Safari: ~380 MB)
|
// 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())) {
|
if (!this.fileDigesterWorkerSupported) {
|
||||||
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.');
|
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
|
// Check if page will crash on iOS
|
||||||
|
@ -952,8 +965,23 @@ class Peer {
|
||||||
}
|
}
|
||||||
|
|
||||||
_addFileDigester(header) {
|
_addFileDigester(header) {
|
||||||
this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime},
|
this._digester = this.fileDigesterWorkerSupported
|
||||||
fileBlob => this._fileReceived(fileBlob),
|
? new FileDigesterViaWorker(
|
||||||
|
{
|
||||||
|
size: header.size,
|
||||||
|
name: header.displayName,
|
||||||
|
mime: header.mime
|
||||||
|
},
|
||||||
|
file => this._fileReceived(file),
|
||||||
|
bytesReceived => this._sendReceiveConfirmation(bytesReceived)
|
||||||
|
)
|
||||||
|
: new FileDigesterViaBuffer(
|
||||||
|
{
|
||||||
|
size: header.size,
|
||||||
|
name: header.displayName,
|
||||||
|
mime: header.mime
|
||||||
|
},
|
||||||
|
file => this._fileReceived(file),
|
||||||
bytesReceived => this._sendReceiveConfirmation(bytesReceived)
|
bytesReceived => this._sendReceiveConfirmation(bytesReceived)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1025,7 +1053,7 @@ class Peer {
|
||||||
|
|
||||||
const sameSize = header.size === acceptedHeader.size;
|
const sameSize = header.size === acceptedHeader.size;
|
||||||
const sameType = header.mime === acceptedHeader.mime;
|
const sameType = header.mime === acceptedHeader.mime;
|
||||||
const sameName = header.name === acceptedHeader.name;
|
const sameName = header.displayName === acceptedHeader.displayName;
|
||||||
|
|
||||||
return sameSize && sameType && sameName;
|
return sameSize && sameType && sameName;
|
||||||
}
|
}
|
||||||
|
@ -1045,7 +1073,7 @@ class Peer {
|
||||||
Logger.log(`File received.\n\nSize: ${size} MB\tDuration: ${duration} s\tSpeed: ${speed} MB/s`);
|
Logger.log(`File received.\n\nSize: ${size} MB\tDuration: ${duration} s\tSpeed: ${speed} MB/s`);
|
||||||
|
|
||||||
// include for compatibility with 'Snapdrop & PairDrop for Android' app
|
// include for compatibility with 'Snapdrop & PairDrop for Android' app
|
||||||
Events.fire('file-received', file);
|
Events.fire('file-received', {name: file.displayName, size: file.size});
|
||||||
|
|
||||||
this._filesReceived.push(file);
|
this._filesReceived.push(file);
|
||||||
|
|
||||||
|
@ -1605,6 +1633,9 @@ class PeersManager {
|
||||||
Events.on('ws-config', e => this._onWsConfig(e.detail));
|
Events.on('ws-config', e => this._onWsConfig(e.detail));
|
||||||
|
|
||||||
Events.on('evaluate-no-sleep', _ => this._onEvaluateNoSleep());
|
Events.on('evaluate-no-sleep', _ => this._onEvaluateNoSleep());
|
||||||
|
|
||||||
|
// clean up on page hide
|
||||||
|
Events.on('pagehide', _ => this._onPageHide());
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWsConfig(wsConfig) {
|
_onWsConfig(wsConfig) {
|
||||||
|
@ -1625,6 +1656,13 @@ class PeersManager {
|
||||||
NoSleepUI.disable();
|
NoSleepUI.disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onPageHide() {
|
||||||
|
// Clear OPFS directory ONLY if this is the last PairDrop Browser tab
|
||||||
|
if (!BrowserTabsConnector.isOnlyTab()) return;
|
||||||
|
|
||||||
|
SWFileDigester.clearDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
_refreshPeer(isCaller, peerId, roomType, roomId) {
|
_refreshPeer(isCaller, peerId, roomType, roomId) {
|
||||||
const peer = this.peers[peerId];
|
const peer = this.peers[peerId];
|
||||||
const roomTypesDiffer = Object.keys(peer._roomIds)[0] !== roomType;
|
const roomTypesDiffer = Object.keys(peer._roomIds)[0] !== roomType;
|
||||||
|
@ -1947,7 +1985,6 @@ class FileChunkerWS extends FileChunker {
|
||||||
class FileDigester {
|
class FileDigester {
|
||||||
|
|
||||||
constructor(meta, fileCompleteCallback, sendReceiveConfirmationCallback) {
|
constructor(meta, fileCompleteCallback, sendReceiveConfirmationCallback) {
|
||||||
this._buffer = [];
|
|
||||||
this._bytesReceived = 0;
|
this._bytesReceived = 0;
|
||||||
this._bytesReceivedSinceLastTime = 0;
|
this._bytesReceivedSinceLastTime = 0;
|
||||||
this._maxBytesWithoutConfirmation = 1048576; // 1 MB
|
this._maxBytesWithoutConfirmation = 1048576; // 1 MB
|
||||||
|
@ -1958,8 +1995,9 @@ class FileDigester {
|
||||||
this._sendReceiveConfimationCallback = sendReceiveConfirmationCallback;
|
this._sendReceiveConfimationCallback = sendReceiveConfirmationCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
unchunk(chunk) {
|
unchunk(chunk) {}
|
||||||
this._buffer.push(chunk);
|
|
||||||
|
evaluateChunkSize(chunk) {
|
||||||
this._bytesReceived += chunk.byteLength;
|
this._bytesReceived += chunk.byteLength;
|
||||||
this._bytesReceivedSinceLastTime += chunk.byteLength;
|
this._bytesReceivedSinceLastTime += chunk.byteLength;
|
||||||
|
|
||||||
|
@ -1972,70 +2010,152 @@ class FileDigester {
|
||||||
this._sendReceiveConfimationCallback(this._bytesReceived);
|
this._sendReceiveConfimationCallback(this._bytesReceived);
|
||||||
this._bytesReceivedSinceLastTime = 0;
|
this._bytesReceivedSinceLastTime = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// File not completely received -> Wait for next chunk.
|
|
||||||
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
|
|
||||||
FileDigesterWorker.isSupported()
|
|
||||||
.then(supported => {
|
|
||||||
if (!supported) {
|
|
||||||
this.processFileViaMemory();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
this.processFileViaWorker();
|
|
||||||
});
|
isFileReceivedCompletely() {
|
||||||
|
return this._bytesReceived >= this._size;
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanUp() {}
|
||||||
|
|
||||||
|
abort() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileDigesterViaBuffer extends FileDigester {
|
||||||
|
constructor(meta, fileCompleteCallback, sendReceiveConfirmationCallback) {
|
||||||
|
super(meta, fileCompleteCallback, sendReceiveConfirmationCallback);
|
||||||
|
this._buffer = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
unchunk(chunk) {
|
||||||
|
this._buffer.push(chunk);
|
||||||
|
this.evaluateChunkSize(chunk);
|
||||||
|
|
||||||
|
// If file is not completely received -> Wait for next chunk.
|
||||||
|
if (!this.isFileReceivedCompletely()) return;
|
||||||
|
|
||||||
|
this.processFileViaMemory();
|
||||||
}
|
}
|
||||||
|
|
||||||
processFileViaMemory() {
|
processFileViaMemory() {
|
||||||
// Loads complete file into RAM which might lead to a page crash (Memory limit iOS Safari: ~380 MB)
|
// Loads complete file into RAM which might lead to a page crash (Memory limit iOS Safari: ~380 MB)
|
||||||
const file = new File(this._buffer, this._name, {
|
const file = new File(
|
||||||
|
this._buffer,
|
||||||
|
this._name,
|
||||||
|
{
|
||||||
type: this._mime,
|
type: this._mime,
|
||||||
lastModified: new Date().getTime()
|
lastModified: new Date().getTime()
|
||||||
})
|
}
|
||||||
|
);
|
||||||
|
file.displayName = this._name
|
||||||
|
|
||||||
this._fileCompleteCallback(file);
|
this._fileCompleteCallback(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
processFileViaWorker() {
|
cleanUp() {
|
||||||
const fileDigesterWorker = new FileDigesterWorker();
|
this._buffer = [];
|
||||||
fileDigesterWorker.digestFileBuffer(this._buffer, this._name)
|
}
|
||||||
.then(file => {
|
|
||||||
this._fileCompleteCallback(file);
|
abort() {
|
||||||
})
|
this.cleanUp();
|
||||||
.catch(reason => {
|
|
||||||
Logger.warn(reason);
|
|
||||||
this.processFileViaWorker();
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FileDigesterWorker {
|
class FileDigesterViaWorker extends FileDigester {
|
||||||
|
constructor(meta, fileCompleteCallback, sendReceiveConfirmationCallback) {
|
||||||
|
super(meta, fileCompleteCallback, sendReceiveConfirmationCallback);
|
||||||
|
this._fileDigesterWorker = new SWFileDigester();
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
unchunk(chunk) {
|
||||||
|
this._fileDigesterWorker
|
||||||
|
.nextChunk(chunk, this._bytesReceived)
|
||||||
|
.then(_ => {
|
||||||
|
this.evaluateChunkSize(chunk);
|
||||||
|
|
||||||
|
// If file is not completely received -> Wait for next chunk.
|
||||||
|
if (!this.isFileReceivedCompletely()) return;
|
||||||
|
|
||||||
|
this.processFileViaWorker();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
processFileViaWorker() {
|
||||||
|
this._fileDigesterWorker
|
||||||
|
.getFile()
|
||||||
|
.then(file => {
|
||||||
|
// Save id and displayName to file to be able to truncate file later
|
||||||
|
file.id = file.name;
|
||||||
|
file.displayName = this._name;
|
||||||
|
|
||||||
|
this._fileCompleteCallback(file);
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
Logger.error("Error in SWFileDigester:", e);
|
||||||
|
this.cleanUp();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanUp() {
|
||||||
|
this._fileDigesterWorker.cleanUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
abort() {
|
||||||
|
// delete and clean up (included in deletion)
|
||||||
|
this._fileDigesterWorker.deleteFile().then((id) => {
|
||||||
|
Logger.debug("File deleted after abort:", id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SWFileDigester {
|
||||||
|
|
||||||
|
static fileWorkers = [];
|
||||||
|
|
||||||
|
constructor(id = null) {
|
||||||
// Use service worker to prevent loading the complete file into RAM
|
// Use service worker to prevent loading the complete file into RAM
|
||||||
this.fileWorker = new Worker("scripts/sw-file-digester.js");
|
// Uses origin private file system (OPFS) as storage endpoint
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
// Generate random uuid to save file on disk
|
||||||
|
// Create only one service worker per file to prevent problems with accessHandles
|
||||||
|
id = generateUUID();
|
||||||
|
SWFileDigester.fileWorkers[id] = new Worker("scripts/sw-file-digester.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.id = id;
|
||||||
|
this.fileWorker = SWFileDigester.fileWorkers[id];
|
||||||
|
|
||||||
this.fileWorker.onmessage = (e) => {
|
this.fileWorker.onmessage = (e) => {
|
||||||
switch (e.data.type) {
|
switch (e.data.type) {
|
||||||
case "support":
|
case "support":
|
||||||
this.onSupport(e.data.supported);
|
this.onSupport(e.data.supported);
|
||||||
break;
|
break;
|
||||||
case "part":
|
case "chunk-written":
|
||||||
this.onPart(e.data.part);
|
this.onChunkWritten(e.data.offset);
|
||||||
break;
|
break;
|
||||||
case "file":
|
case "file":
|
||||||
this.onFile(e.data.file);
|
this.onFile(e.data.file);
|
||||||
break;
|
break;
|
||||||
case "file-deleted":
|
case "file-deleted":
|
||||||
this.onFileDeleted();
|
this.onFileDeleted(e.data.id);
|
||||||
break;
|
break;
|
||||||
case "error":
|
case "error":
|
||||||
this.onError(e.data.error);
|
this.onError(e.data.error);
|
||||||
break;
|
break;
|
||||||
|
case "directory-cleared":
|
||||||
|
this.onDirectoryCleared();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onError(error) {
|
||||||
|
// an error occurred.
|
||||||
|
Logger.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
static isSupported() {
|
static isSupported() {
|
||||||
// Check if web worker is supported and supports specific functions
|
// Check if web worker is supported and supports specific functions
|
||||||
return new Promise(async resolve => {
|
return new Promise(async resolve => {
|
||||||
|
@ -2044,7 +2164,7 @@ class FileDigesterWorker {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileDigesterWorker = new FileDigesterWorker();
|
const fileDigesterWorker = new SWFileDigester();
|
||||||
|
|
||||||
resolve(await fileDigesterWorker.checkSupport());
|
resolve(await fileDigesterWorker.checkSupport());
|
||||||
|
|
||||||
|
@ -2068,75 +2188,88 @@ class FileDigesterWorker {
|
||||||
this.resolveSupport = null;
|
this.resolveSupport = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
digestFileBuffer(buffer, fileName) {
|
nextChunk(chunk, offset) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
this.resolveFile = resolve;
|
this.digestChunk(chunk, offset);
|
||||||
this.rejectFile = reject;
|
resolve();
|
||||||
|
});
|
||||||
this.i = 0;
|
|
||||||
this.offset = 0;
|
|
||||||
|
|
||||||
this.buffer = buffer;
|
|
||||||
this.fileName = fileName;
|
|
||||||
|
|
||||||
this.sendPart(this.buffer[0], 0);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
digestChunk(chunk, offset) {
|
||||||
sendPart(buffer, offset) {
|
|
||||||
this.fileWorker.postMessage({
|
this.fileWorker.postMessage({
|
||||||
type: "part",
|
type: "chunk",
|
||||||
name: this.fileName,
|
id: this.id,
|
||||||
buffer: buffer,
|
chunk: chunk,
|
||||||
offset: offset
|
offset: offset
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getFile() {
|
onChunkWritten(chunkOffset) {
|
||||||
this.fileWorker.postMessage({
|
Logger.debug("Chunk written at offset", chunkOffset);
|
||||||
type: "get-file",
|
|
||||||
name: this.fileName,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteFile() {
|
getFile() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.resolveFile = resolve;
|
||||||
|
|
||||||
this.fileWorker.postMessage({
|
this.fileWorker.postMessage({
|
||||||
type: "delete-file",
|
type: "get-file",
|
||||||
name: this.fileName
|
id: this.id,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onPart(part) {
|
async getFileById(id) {
|
||||||
if (this.i < this.buffer.length - 1) {
|
const swFileDigester = new SWFileDigester(id);
|
||||||
// process next chunk
|
return await swFileDigester.getFile();
|
||||||
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) {
|
onFile(file) {
|
||||||
this.buffer = [];
|
|
||||||
this.resolveFile(file);
|
this.resolveFile(file);
|
||||||
this.deleteFile();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onFileDeleted() {
|
deleteFile() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.resolveDeletion = resolve;
|
||||||
|
this.fileWorker.postMessage({
|
||||||
|
type: "delete-file",
|
||||||
|
id: this.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteFileById(id) {
|
||||||
|
const swFileDigester = new SWFileDigester(id);
|
||||||
|
return await swFileDigester.deleteFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanUp() {
|
||||||
|
// terminate service worker
|
||||||
|
this.fileWorker.terminate();
|
||||||
|
delete SWFileDigester.fileWorkers[this.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileDeleted(id) {
|
||||||
// File Digestion complete -> Tidy up
|
// File Digestion complete -> Tidy up
|
||||||
this.fileWorker.terminate();
|
Logger.debug("File deleted:", id);
|
||||||
|
this.resolveDeletion(id);
|
||||||
|
this.cleanUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
onError(error) {
|
static clearDirectory() {
|
||||||
// an error occurred.
|
for (let i = 0; i < SWFileDigester.fileWorkers.length; i++) {
|
||||||
Logger.error(error);
|
SWFileDigester.fileWorkers[i].terminate();
|
||||||
|
}
|
||||||
|
SWFileDigester.fileWorkers = [];
|
||||||
|
|
||||||
// Use memory method instead and terminate service worker.
|
const swFileDigester = new SWFileDigester();
|
||||||
this.fileWorker.terminate();
|
swFileDigester.fileWorker.postMessage({
|
||||||
this.rejectFile("Failed to process file via service-worker. Do not use Firefox private mode to prevent this.");
|
type: "clear-directory",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDirectoryCleared() {
|
||||||
|
Logger.debug("All files on OPFS truncated.");
|
||||||
|
this.cleanUp();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,70 +1,104 @@
|
||||||
|
self.accessHandle = undefined;
|
||||||
|
self.messageQueue = [];
|
||||||
|
self.busy = false;
|
||||||
|
|
||||||
|
|
||||||
self.addEventListener('message', async e => {
|
self.addEventListener('message', async e => {
|
||||||
|
// Put message into queue if busy
|
||||||
|
if (self.busy) {
|
||||||
|
self.messageQueue.push(e.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await digestMessage(e.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function digestMessage(message) {
|
||||||
|
self.busy = true;
|
||||||
try {
|
try {
|
||||||
switch (e.data.type) {
|
switch (message.type) {
|
||||||
case "check-support":
|
case "check-support":
|
||||||
await checkSupport();
|
await checkSupport();
|
||||||
break;
|
break;
|
||||||
case "part":
|
case "chunk":
|
||||||
await onPart(e.data.name, e.data.buffer, e.data.offset);
|
await onChunk(message.id, message.chunk, message.offset);
|
||||||
break;
|
break;
|
||||||
case "get-file":
|
case "get-file":
|
||||||
await onGetFile(e.data.name);
|
await onGetFile(message.id);
|
||||||
break;
|
break;
|
||||||
case "delete-file":
|
case "delete-file":
|
||||||
await onDeleteFile(e.data.name);
|
await onDeleteFile(message.id);
|
||||||
|
break;
|
||||||
|
case "clear-directory":
|
||||||
|
await onClearDirectory();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
self.postMessage({type: "error", error: e});
|
self.postMessage({type: "error", error: e});
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
// message is digested. Digest next message.
|
||||||
|
await messageDigested();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function messageDigested() {
|
||||||
|
if (!self.messageQueue.length) {
|
||||||
|
// no chunk in queue -> set flag to false and stop
|
||||||
|
this.busy = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Digest next message in queue
|
||||||
|
await this.digestMessage(self.messageQueue.pop());
|
||||||
|
}
|
||||||
|
|
||||||
async function checkSupport() {
|
async function checkSupport() {
|
||||||
try {
|
try {
|
||||||
await getAccessHandle("test.txt");
|
const accessHandle = await getAccessHandle("test");
|
||||||
self.postMessage({type: "support", supported: true});
|
self.postMessage({type: "support", supported: true});
|
||||||
|
accessHandle.close();
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
self.postMessage({type: "support", supported: false});
|
self.postMessage({type: "support", supported: false});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFileHandle(fileName) {
|
async function getFileHandle(id) {
|
||||||
const root = await navigator.storage.getDirectory();
|
const dirHandle = await navigator.storage.getDirectory();
|
||||||
return await root.getFileHandle(fileName, {create: true});
|
return await dirHandle.getFileHandle(id, {create: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAccessHandle(fileName) {
|
async function getAccessHandle(id) {
|
||||||
const fileHandle = await getFileHandle(fileName);
|
const fileHandle = await getFileHandle(id);
|
||||||
|
|
||||||
|
if (!self.accessHandle) {
|
||||||
// Create FileSystemSyncAccessHandle on the file.
|
// Create FileSystemSyncAccessHandle on the file.
|
||||||
return await fileHandle.createSyncAccessHandle();
|
self.accessHandle = await fileHandle.createSyncAccessHandle();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.accessHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onPart(fileName, buffer, offset) {
|
async function onChunk(id, chunk, offset) {
|
||||||
const accessHandle = await getAccessHandle(fileName);
|
const accessHandle = await getAccessHandle(id);
|
||||||
|
|
||||||
// Write the message to the end of the file.
|
// Write the message to the end of the file.
|
||||||
let encodedMessage = new DataView(buffer);
|
let encodedMessage = new DataView(chunk);
|
||||||
accessHandle.write(encodedMessage, { at: offset });
|
accessHandle.write(encodedMessage, { at: offset });
|
||||||
|
|
||||||
// Always close FileSystemSyncAccessHandle if done.
|
self.postMessage({type: "chunk-written", offset: offset});
|
||||||
accessHandle.close(); accessHandle.close();
|
|
||||||
|
|
||||||
self.postMessage({type: "part", part: encodedMessage});
|
|
||||||
encodedMessage = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onGetFile(fileName) {
|
async function onGetFile(id) {
|
||||||
const fileHandle = await getFileHandle(fileName);
|
const fileHandle = await getFileHandle(id);
|
||||||
let file = await fileHandle.getFile();
|
let file = await fileHandle.getFile();
|
||||||
|
|
||||||
self.postMessage({type: "file", file: file});
|
self.postMessage({type: "file", file: file});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDeleteFile(fileName) {
|
async function onDeleteFile(id) {
|
||||||
const accessHandle = await getAccessHandle(fileName);
|
const accessHandle = await getAccessHandle(id);
|
||||||
|
|
||||||
// Truncate the file to 0 bytes
|
// Truncate the file to 0 bytes
|
||||||
accessHandle.truncate(0);
|
accessHandle.truncate(0);
|
||||||
|
@ -75,5 +109,23 @@ async function onDeleteFile(fileName) {
|
||||||
// Always close FileSystemSyncAccessHandle if done.
|
// Always close FileSystemSyncAccessHandle if done.
|
||||||
accessHandle.close();
|
accessHandle.close();
|
||||||
|
|
||||||
self.postMessage({type: "file-deleted"});
|
self.postMessage({type: "file-deleted", id: id});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onClearDirectory() {
|
||||||
|
const dirHandle = await navigator.storage.getDirectory();
|
||||||
|
|
||||||
|
// Iterate through directory entries and truncate all entries to 0
|
||||||
|
for await (const [id, fileHandle] of dirHandle.entries()) {
|
||||||
|
const accessHandle = await fileHandle.createSyncAccessHandle();
|
||||||
|
|
||||||
|
// Truncate the file to 0 bytes
|
||||||
|
accessHandle.truncate(0);
|
||||||
|
|
||||||
|
// Persist changes to disk.
|
||||||
|
accessHandle.flush();
|
||||||
|
|
||||||
|
// Always close FileSystemSyncAccessHandle if done.
|
||||||
|
accessHandle.close();
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1060,7 +1060,7 @@ class ReceiveDialog extends Dialog {
|
||||||
: Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1});
|
: Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1});
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = files[0].name;
|
const fileName = files[0].displayName;
|
||||||
const fileNameSplit = fileName.split('.');
|
const fileNameSplit = fileName.split('.');
|
||||||
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
|
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
|
||||||
const fileStem = fileName.substring(0, fileName.length - fileExtension.length);
|
const fileStem = fileName.substring(0, fileName.length - fileExtension.length);
|
||||||
|
@ -1331,7 +1331,7 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||||
Events.fire('notify-user', downloadSuccessfulTranslation);
|
Events.fire('notify-user', downloadSuccessfulTranslation);
|
||||||
this.downloadSuccessful = true;
|
this.downloadSuccessful = true;
|
||||||
|
|
||||||
this.hide()
|
this.hide();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1355,7 +1355,7 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||||
_downloadFiles(files) {
|
_downloadFiles(files) {
|
||||||
let tmpBtn = document.createElement("a");
|
let tmpBtn = document.createElement("a");
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
tmpBtn.download = files[i].name;
|
tmpBtn.download = files[i].displayName;
|
||||||
tmpBtn.href = URL.createObjectURL(files[i]);
|
tmpBtn.href = URL.createObjectURL(files[i]);
|
||||||
tmpBtn.click();
|
tmpBtn.click();
|
||||||
}
|
}
|
||||||
|
@ -1435,6 +1435,7 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
super.hide();
|
super.hide();
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
this._tidyUpButtons();
|
this._tidyUpButtons();
|
||||||
this._tidyUpPreviewBox();
|
this._tidyUpPreviewBox();
|
||||||
|
@ -2651,15 +2652,20 @@ class Base64Dialog extends Dialog {
|
||||||
this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-processing");
|
this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-processing");
|
||||||
}
|
}
|
||||||
|
|
||||||
preparePasting(type) {
|
preparePasting(type, useFallback = false) {
|
||||||
const translateType = type === 'text'
|
const translateType = type === 'text'
|
||||||
? Localization.getTranslation("dialogs.base64-text")
|
? Localization.getTranslation("dialogs.base64-text")
|
||||||
: Localization.getTranslation("dialogs.base64-files");
|
: Localization.getTranslation("dialogs.base64-files");
|
||||||
|
|
||||||
if (navigator.clipboard.readText) {
|
if (navigator.clipboard.readText && !useFallback) {
|
||||||
this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-tap-to-paste", null, {type: translateType});
|
this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-tap-to-paste", null, {type: translateType});
|
||||||
this._clickCallback = _ => this.processClipboard(type);
|
this._clickCallback = _ => this.processClipboard(type);
|
||||||
this.$pasteBtn.addEventListener('click', _ => this._clickCallback());
|
this.$pasteBtn.addEventListener('click', _ => {
|
||||||
|
this._clickCallback()
|
||||||
|
.catch(_ => {
|
||||||
|
this.preparePasting(type, true);
|
||||||
|
})
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Logger.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.")
|
Logger.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.")
|
||||||
|
|
|
@ -620,3 +620,26 @@ function isUrlValid(url) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// polyfill for crypto.randomUUID()
|
||||||
|
// Credits: @Briguy37 - https://stackoverflow.com/a/8809472/14678591
|
||||||
|
function generateUUID() {
|
||||||
|
return crypto && crypto.randomUUID()
|
||||||
|
? crypto.randomUUID()
|
||||||
|
: () => {
|
||||||
|
let
|
||||||
|
d = new Date().getTime(),
|
||||||
|
d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0;
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||||
|
let r = Math.random() * 16;
|
||||||
|
if (d > 0) {
|
||||||
|
r = (d + r) % 16 | 0;
|
||||||
|
d = Math.floor(d / 16);
|
||||||
|
} else {
|
||||||
|
r = (d2 + r) % 16 | 0;
|
||||||
|
d2 = Math.floor(d2 / 16);
|
||||||
|
}
|
||||||
|
return (c == 'x' ? r : (r & 0x7 | 0x8)).toString(16);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue