1
Fork 0
mirror of https://github.com/jech/galene.git synced 2024-11-23 00:55:58 +01:00

Rework file transfer code.

More explicit data structures, better error handling.
This commit is contained in:
Juliusz Chroboczek 2022-02-01 18:46:51 +01:00
parent 0ef5d10744
commit 66e5d2951d

View file

@ -2153,74 +2153,93 @@ async function gotJoined(kind, group, perms, status, data, message) {
} }
} }
/** /** @type {Object<string,TransferredFile>} */
* @typedef {Object} transferredFile
* @property {string} id
* @property {string} username
* @property {string} name
* @property {File} [file]
* @property {RTCPeerConnection} [pc]
* @property {number} size
* @property {string} type
* @property {Array<RTCIceCandidateInit>} [candidates]
* @property {Array<Blob|ArrayBuffer>} [data]
* @property {number} [datalen]
* @property {boolean} [done]
*/
/** @type {Object<string,transferredFile>} */
let transferredFiles = {}; let transferredFiles = {};
/** /**
* @param {boolean} up * A file in the process of being transferred.
* @param {string} id *
* @param {string} fileid * @constructor
*/ */
function transferredFileId(up, id, fileid) { function TransferredFile(id, userid, up, username, name, type, size) {
return id + (up ? '+' : '-') + fileid; /** @type {string} */
this.id = id;
/** @type {string} */
this.userid = userid;
/** @type {boolean} */
this.up = up;
/** @type {string} */
this.username = username;
/** @type {string} */
this.name = name;
/** @type {string} */
this.type = type;
/** @type {number} */
this.size = size;
/** @type {File} */
this.file = null;
/** @type {RTCPeerConnection} */
this.pc = null;
/** @type {RTCDataChannel} */
this.dc = null;
/** @type {Array<RTCIceCandidateInit>} */
this.candidates = [];
/** @type {Array<Blob|ArrayBuffer>} */
this.data = [];
/** @type {number} */
this.datalen = 0;
} }
TransferredFile.prototype.fullid = function() {
return this.userid + (this.up ? '+' : '-') + this.id;
};
/** /**
* @param {boolean} up * @param {boolean} up
* @param {string} id * @param {string} userid
* @param {string} fileid * @param {string} fileid
* @returns {transferredFile} * @returns {TransferredFile}
*/ */
function getTransferredFile(up, id, fileid) { TransferredFile.get = function(up, userid, fileid) {
let f = transferredFiles[transferredFileId(up, id, fileid)]; return transferredFiles[userid + (up ? '+' : '-') + fileid];
if(!f) { };
throw new Error("Couldn't find file being transferred");
TransferredFile.prototype.close = function() {
if(this.pc)
this.pc.close();
this.dc = null;
this.pc = null;
this.data = [];
this.datalen = 0;
delete(transferredFiles[this.fullid()]);
}
TransferredFile.prototype.pushData = function(data) {
if(data instanceof Blob) {
this.datalen += data.size;
} else if(data instanceof ArrayBuffer) {
this.datalen += data.byteLength;
} else {
throw new Error('unexpected type for received data');
} }
return f; this.data.push(data);
}
TransferredFile.prototype.getData = function() {
let blob = new Blob(this.data, {type: this.type});
if(blob.size != this.datalen)
throw new Error('Inconsistent data size');
this.data = [];
this.datalen = 0;
return blob;
} }
/** /**
* @param {boolean} up * @param {TransferredFile} f
* @param {string} id
* @param {string} fileid
*/ */
function deleteTransferredFile(up, id, fileid) { function fileTransferBox(f) {
let fullid = transferredFileId(up, id, fileid);
let f = transferredFiles[fullid];
if(!f)
return;
if(f.pc) {
f.pc.close();
delete(f.pc);
}
delete(transferredFiles[fullid]);
}
/**
* @param {boolean} up
* @param {string} id
* @param {string} fileid
* @param {transferredFile} f
*/
function fileTransferBox(up, id, fileid, f) {
let fullid = transferredFileId(up, id, fileid);
let p = document.createElement('p'); let p = document.createElement('p');
if(up) if(f.up)
p.textContent = p.textContent =
`We have offered to send a file called "${f.name}" ` + `We have offered to send a file called "${f.name}" ` +
`to user ${f.username}.`; `to user ${f.username}.`;
@ -2229,35 +2248,35 @@ function fileTransferBox(up, id, fileid, f) {
`User ${f.username} offered to send us a file ` + `User ${f.username} offered to send us a file ` +
`called "${f.name}" of size ${f.size}.` `called "${f.name}" of size ${f.size}.`
let bno = null, byes = null; let bno = null, byes = null;
if(up) { if(f.up) {
bno = document.createElement('button'); bno = document.createElement('button');
bno.textContent = 'Cancel'; bno.textContent = 'Cancel';
bno.onclick = function(e) { bno.onclick = function(e) {
cancelFile(id, fileid); cancelFile(f);
}; };
bno.id = "bno-" + fullid; bno.id = "bno-" + f.fullid();
} else { } else {
byes = document.createElement('button'); byes = document.createElement('button');
byes.textContent = 'Accept'; byes.textContent = 'Accept';
byes.onclick = function(e) { byes.onclick = function(e) {
getFile(id, fileid); getFile(f);
}; };
byes.id = "byes-" + fullid; byes.id = "byes-" + f.fullid();
bno = document.createElement('button'); bno = document.createElement('button');
bno.textContent = 'Decline'; bno.textContent = 'Decline';
bno.onclick = function(e) { bno.onclick = function(e) {
rejectFile(id, fileid); rejectFile(f);
}; };
bno.id = "bno-" + fullid; bno.id = "bno-" + f.fullid();
} }
let status = document.createElement('div'); let status = document.createElement('div');
status.id = 'status-' + fullid; status.id = 'status-' + f.fullid();
if(!up) { if(!f.up) {
status.textContent = status.textContent =
'(Choosing "Accept" will disclose your IP address.)'; '(Choosing "Accept" will disclose your IP address.)';
} }
let div = document.createElement('div'); let div = document.createElement('div');
div.id = 'file-' + fullid; div.id = 'file-' + f.fullid();
div.appendChild(p); div.appendChild(p);
if(byes) if(byes)
div.appendChild(byes); div.appendChild(byes);
@ -2266,36 +2285,34 @@ function fileTransferBox(up, id, fileid, f) {
div.appendChild(status); div.appendChild(status);
div.classList.add('message'); div.classList.add('message');
div.classList.add('message-private'); div.classList.add('message-private');
div.classList.add('message-row');
let box = document.getElementById('box'); let box = document.getElementById('box');
box.appendChild(div); box.appendChild(div);
return div; return div;
} }
/** /**
* @param {boolean} up * @param {TransferredFile} f
* @param {string} id
* @param {string} fileid
* @param {string} status * @param {string} status
* @param {boolean} [delyes] * @param {boolean} [delyes]
* @param {boolean} [delno] * @param {boolean} [delno]
*/ */
function setFileStatus(up, id, fileid, status, delyes, delno) { function setFileStatus(f, status, delyes, delno) {
let fullid = transferredFileId(up, id, fileid) let statusdiv = document.getElementById('status-' + f.fullid());
let statusdiv = document.getElementById('status-' + fullid);
if(!statusdiv) if(!statusdiv)
throw new Error("Couldn't find statusdiv"); throw new Error("Couldn't find statusdiv");
statusdiv.textContent = status; statusdiv.textContent = status;
if(delyes || delno) { if(delyes || delno) {
let div = document.getElementById('file-' + fullid); let div = document.getElementById('file-' + f.fullid());
if(!div) if(!div)
throw new Error("Couldn't find file div"); throw new Error("Couldn't find file div");
if(delyes) { if(delyes) {
let byes = document.getElementById('byes-' + fullid) let byes = document.getElementById('byes-' + f.fullid())
if(byes) if(byes)
div.removeChild(byes); div.removeChild(byes);
} }
if(delno) { if(delno) {
let bno = document.getElementById('bno-' + fullid) let bno = document.getElementById('bno-' + f.fullid())
if(bno) if(bno)
div.removeChild(bno); div.removeChild(bno);
} }
@ -2303,15 +2320,13 @@ function setFileStatus(up, id, fileid, status, delyes, delno) {
} }
/** /**
* @param {boolean} up * @param {TransferredFile} f
* @param {string} id
* @param {string} fileid
* @param {any} message * @param {any} message
*/ */
function failFile(up, id, fileid, message) { function failFile(f, message) {
console.error('File transfer failed:', message); console.error('File transfer failed:', message);
setFileStatus(up, id, fileid, message ? `Failed: ${message}` : 'Failed.'); setFileStatus(f, message ? `Failed: ${message}` : 'Failed.');
deleteTransferredFile(true, id, fileid); f.close();
} }
/** /**
@ -2321,50 +2336,43 @@ function failFile(up, id, fileid, message) {
*/ */
function offerFile(username, id, file) { function offerFile(username, id, file) {
let fileid = newRandomId(); let fileid = newRandomId();
let fullid = transferredFileId(true, id, fileid); let f = new TransferredFile(
if(transferredFiles[fullid]) fileid, id, true, username, file.name, file.type, file.size,
throw new Error('Id collision'); );
let f = { f.file = file;
id: fileid, transferredFiles[f.fullid()] = f;
username: username, try {
file: file, fileTransferBox(f);
name: file.name,
size: file.size,
type: file.type,
}
fileTransferBox(true, id, fileid, f);
serverConnection.userMessage('offerfile', id, { serverConnection.userMessage('offerfile', id, {
id: fileid, id: fileid,
name: f.name, name: f.name,
size: f.size, size: f.size,
type: f.type, type: f.type,
}); });
transferredFiles[fullid] = f; } catch(e) {
displayError(e);
f.close();
}
} }
/** /**
* @param {string} id * @param {TransferredFile} f
* @param {string} fileid
*/ */
function cancelFile(id, fileid) { function cancelFile(f) {
let f = getTransferredFile(true, id, fileid); serverConnection.userMessage('cancelfile', f.userid, {
serverConnection.userMessage('cancelfile', id, {
id: f.id, id: f.id,
}); });
deleteTransferredFile(true, id, fileid); f.close();
setFileStatus(true, id, fileid, 'Cancelled.', true, true); setFileStatus(f, 'Cancelled.', true, true);
} }
/** /**
* @param {string} id * @param {TransferredFile} f
* @param {string} fileid
*/ */
async function getFile(id, fileid) { async function getFile(f) {
let f = getTransferredFile(false, id, fileid);
if(f.pc) if(f.pc)
throw new Error('Download already in progress'); throw new Error("Download already in progress");
setFileStatus(f, 'Connecting...', true);
setFileStatus(false, id, fileid, 'Connecting...', true);
let pc = new RTCPeerConnection(serverConnection.rtcConfiguration); let pc = new RTCPeerConnection(serverConnection.rtcConfiguration);
if(!pc) if(!pc)
throw new Error("Couldn't create peer connection"); throw new Error("Couldn't create peer connection");
@ -2377,75 +2385,71 @@ async function getFile(id, fileid) {
} }
}; };
pc.onicecandidate = function(e) { pc.onicecandidate = function(e) {
serverConnection.userMessage('filedownice', id, { serverConnection.userMessage('filedownice', f.userid, {
id: f.id, id: f.id,
candidate: e.candidate, candidate: e.candidate,
}); });
}; };
let dc = pc.createDataChannel('file'); f.dc = pc.createDataChannel('file');
f.data = []; f.data = [];
f.datalen = 0; f.datalen = 0;
dc.onclose = function(e) { f.dc.onclose = function(e) {
try { try {
closeReceiveFileData(id, fileid, f); closeReceiveFileData(f);
} catch(e) { } catch(e) {
failFile(false, id, fileid, e); failFile(f, e);
} }
}; };
dc.onmessage = function(e) { f.dc.onmessage = function(e) {
try { try {
receiveFileData(id, fileid, f, dc, e.data); receiveFileData(f, e.data);
} catch(e) { } catch(e) {
failFile(false, id, fileid, e); failFile(f, e);
} }
}; };
dc.onerror = function(e) { f.dc.onerror = function(e) {
/** @ts-ignore */ /** @ts-ignore */
let err = e.error; let err = e.error;
failFile(false, id, fileid, err); failFile(f, err);
}; };
let offer = await pc.createOffer(); let offer = await pc.createOffer();
if(!offer) if(!offer)
throw new Error("Couldn't create offer"); throw new Error("Couldn't create offer");
await pc.setLocalDescription(offer); await pc.setLocalDescription(offer);
serverConnection.userMessage('getfile', id, { serverConnection.userMessage('getfile', f.userid, {
id: f.id, id: f.id,
offer: pc.localDescription.sdp, offer: pc.localDescription.sdp,
}); });
setFileStatus(false, id, fileid, 'Negotiating...', true); setFileStatus(f, 'Negotiating...', true);
} }
/** /**
* @param {string} id * @param {TransferredFile} f
* @param {string} fileid
*/ */
async function rejectFile(id, fileid) { async function rejectFile(f) {
let f = getTransferredFile(false, id, fileid); serverConnection.userMessage('rejectfile', f.userid, {
serverConnection.userMessage('rejectfile', id, {
id: f.id, id: f.id,
}); });
deleteTransferredFile(false, id, fileid); setFileStatus(f, 'Rejected.', true, true);
setFileStatus(false, id, fileid, 'Rejected.', true, true); f.close();
} }
/** /**
* @param {string} id * @param {TransferredFile} f
* @param {string} fileid
* @param {string} sdp * @param {string} sdp
*/ */
async function sendFile(id, fileid, sdp) { async function sendFile(f, sdp) {
let f = getTransferredFile(true, id, fileid);
if(f.pc) if(f.pc)
throw new Error('Transfer already in progress'); throw new Error('Transfer already in progress');
setFileStatus(true, id, fileid, 'Negotiating...', true); setFileStatus(f, 'Negotiating...', true);
let pc = new RTCPeerConnection(serverConnection.rtcConfiguration); let pc = new RTCPeerConnection(serverConnection.rtcConfiguration);
if(!pc) if(!pc)
throw new Error("Couldn't create peer connection"); throw new Error("Couldn't create peer connection");
f.pc = pc; f.pc = pc;
f.candidates = []; f.candidates = [];
pc.onicecandidate = function(e) { pc.onicecandidate = function(e) {
serverConnection.userMessage('fileupice', id, { serverConnection.userMessage('fileupice', f.userid, {
id: f.id, id: f.id,
candidate: e.candidate, candidate: e.candidate,
}); });
@ -2456,29 +2460,30 @@ async function sendFile(id, fileid, sdp) {
f.candidates = []; f.candidates = [];
} }
}; };
let file = f.file;
pc.ondatachannel = function(e) { pc.ondatachannel = function(e) {
let dc = /** @type{RTCDataChannel} */(e.channel); if(f.dc)
dc.onclose = function(e) { throw new Error('Duplicate datachannel');
f.dc = /** @type{RTCDataChannel} */(e.channel);
f.dc.onclose = function(e) {
try { try {
closeSendFileData(id, fileid, f); closeSendFileData(f);
} catch(e) { } catch(e) {
failFile(true, id, fileid, e); failFile(f, e);
} }
}; };
dc.onerror = function(e) { f.dc.onerror = function(e) {
/** @ts-ignore */ /** @ts-ignore */
let err = e.error; let err = e.error;
failFile(true, id, fileid, err); failFile(f, err);
} }
dc.onmessage = function(e) { f.dc.onmessage = function(e) {
try { try {
ackSendFileData(id, fileid, f, e.data); ackSendFileData(f, e.data);
} catch(e) { } catch(e) {
failFile(true, id, fileid, e); failFile(f, e);
} }
}; };
sendFileData(id, fileid, f, dc, file); sendFileData(f).catch(e => failFile(f, e));
}; };
await pc.setRemoteDescription({ await pc.setRemoteDescription({
@ -2490,62 +2495,60 @@ async function sendFile(id, fileid, sdp) {
if(!answer) if(!answer)
throw new Error("Couldn't create answer"); throw new Error("Couldn't create answer");
await pc.setLocalDescription(answer); await pc.setLocalDescription(answer);
serverConnection.userMessage('sendfile', id, { serverConnection.userMessage('sendfile', f.userid, {
id: f.id, id: f.id,
answer: pc.localDescription.sdp, answer: pc.localDescription.sdp,
}); });
setFileStatus(true, id, fileid, 'Uploading...', true); setFileStatus(f, 'Uploading...', true);
} }
/** /**
* @param {string} id * @param {TransferredFile} f
* @param {string} fileid
* @param {string} sdp * @param {string} sdp
*/ */
async function receiveFile(id, fileid, sdp) { async function receiveFile(f, sdp) {
let f = getTransferredFile(false, id, fileid);
if(!f.pc)
throw new Error('Transfer is not in progress');
await f.pc.setRemoteDescription({ await f.pc.setRemoteDescription({
type: 'answer', type: 'answer',
sdp: sdp, sdp: sdp,
}); });
setFileStatus(false, id, fileid, 'Downloading...', true); setFileStatus(f, 'Downloading...', true);
} }
/** /**
* @param {string} id * @param {TransferredFile} f
* @param {string} fileid
* @param {transferredFile} f
* @param {RTCDataChannel} dc
* @param {File} file
*/ */
async function sendFileData(id, fileid, f, dc, file) { async function sendFileData(f) {
let r = file.stream().getReader(); let r = f.file.stream().getReader();
f.datalen = 0;
dc.bufferedAmountLowThreshold = 65536; f.dc.bufferedAmountLowThreshold = 65536;
async function write(a) { async function write(a) {
while(dc.bufferedAmount > 65536) { while(f.dc.bufferedAmount > f.dc.bufferedAmountLowThreshold) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
dc.onbufferedamountlow = function(e) { if(f.dc == null) {
reject(new Error('File is closed.'));
}
f.dc.onbufferedamountlow = function(e) {
if(f.dc == null) {
reject(new Error('File is closed.'));
}
f.dc.onbufferedamountlow = null;
resolve(); resolve();
} }
}); });
} }
dc.send(a); f.dc.send(a);
f.datalen += a.length; f.datalen += a.length;
setFileStatus( setFileStatus(f, `Uploading... ${f.datalen}/${f.size}`, true);
true, id, fileid, `Uploading... ${f.datalen}/${f.size}`, true,
);
} }
while(true) { while(true) {
let v = await r.read(); let v = await r.read();
if(v.done) if(v.done)
break; break;
if(v.value.length < 16384) { if(!(v.value instanceof Uint8Array))
throw new Error('Unexpected type for chunk');
if(v.value.length <= 16384) {
await write(v.value); await write(v.value);
} else { } else {
for(let i = 0; i < v.value.length; i += 16384) { for(let i = 0; i < v.value.length; i += 16384) {
@ -2559,70 +2562,71 @@ async function sendFileData(id, fileid, f, dc, file) {
} }
/** /**
* @param {string} id * @param {TransferredFile} f
* @param {string} fileid
* @param {transferredFile} f
*/ */
function ackSendFileData(id, fileid, f, data) { function ackSendFileData(f, data) {
if(data === 'done' && f.datalen == f.size) if(data === 'done' && f.datalen == f.size)
setFileStatus(true, id, fileid, 'Done.', true, true); setFileStatus(f, 'Done.', true, true);
else else
setFileStatus(true, id, fileid, 'Failed.', true, true); setFileStatus(f, 'Failed.', true, true);
f.done = true; f.dc.onclose = null;
deleteTransferredFile(true, id, fileid); f.dc.onerror = null;
f.close();
} }
/** /**
* @param {string} id * @param {TransferredFile} f
* @param {string} fileid
* @param {transferredFile} f
*/ */
function closeSendFileData(id, fileid, f) { function closeSendFileData(f) {
if(!f.done) setFileStatus(f, 'Failed.', true, true);
setFileStatus(true, id, fileid, 'Failed.', true, true); f.close();
deleteTransferredFile(true, id, fileid);
} }
/** /**
* @param {string} id * @param {TransferredFile} f
* @param {string} fileid
* @param {transferredFile} f
* @param {RTCDataChannel} dc
* @param {Blob|ArrayBuffer} data * @param {Blob|ArrayBuffer} data
*/ */
function receiveFileData(id, fileid, f, dc, data) { function receiveFileData(f, data) {
f.data.push(data); f.pushData(data);
if(data instanceof Blob) { setFileStatus(f, `Downloading... ${f.datalen}/${f.size}`, true);
f.datalen += data.size;
} else if(data instanceof ArrayBuffer) {
f.datalen += data.byteLength;
} else {
console.error('Unexpeced type for received data', data);
throw new Error('unexpected type for received data');
}
setFileStatus(
false, id, fileid, `Downloading... ${f.datalen}/${f.size}`, true,
);
if(f.datalen < f.size) if(f.datalen < f.size)
return; return;
if(f.datalen > f.size) { if(f.datalen != f.size) {
setFileStatus(false, id, fileid, 'Failed.', true, true); setFileStatus(f, 'Failed.', true, true);
deleteTransferredFile(false, id, fileid); f.close();
return; return;
} }
dc.onmessage = null; f.dc.onmessage = null;
dc.onerror = null; doneReceiveFileData(f);
}
dc.send('done'); /**
* @param {TransferredFile} f
*/
async function doneReceiveFileData(f) {
setFileStatus(f, 'Done.', true, true);
let blob = f.getData();
setFileStatus(false, id, fileid, 'Done.', true, true); await new Promise((resolve, reject) => {
let timer = setTimeout(function(e) { resolve(); }, 2000);
f.dc.onclose = function(e) {
clearTimeout(timer);
resolve();
};
f.dc.onerror = function(e) {
clearTimeout(timer);
resolve();
};
f.dc.send('done');
});
f.dc.onclose = null;
f.dc.onerror = null;
f.close();
let blob = new Blob(f.data, {type: f.type});
f.data = null;
let url = URL.createObjectURL(blob); let url = URL.createObjectURL(blob);
let a = document.createElement('a'); let a = document.createElement('a');
a.href = url; a.href = url;
@ -2634,14 +2638,12 @@ function receiveFileData(id, fileid, f, dc, data) {
} }
/** /**
* @param {string} id * @param {TransferredFile} f
* @param {string} fileid
* @param {transferredFile} f
*/ */
function closeReceiveFileData(id, fileid, f) { function closeReceiveFileData(f) {
if(f.datalen != f.size) { if(f.datalen != f.size) {
setFileStatus(false, id, fileid, 'Failed.', true, true) setFileStatus(f, 'Failed.', true, true)
deleteTransferredFile(false, id, fileid); f.close();
} }
} }
@ -2683,38 +2685,46 @@ function gotUserMessage(id, dest, username, time, privileged, kind, message) {
} }
break; break;
case 'offerfile': { case 'offerfile': {
let fullid = transferredFileId(false, id, message.id); let f = new TransferredFile(
let f = { message.id, id, false, username,
id: message.id, message.name, message.type, message.size,
username: username, );
name: message.name, transferredFiles[f.fullid()] = f;
type: message.type, fileTransferBox(f);
size: message.size,
};
transferredFiles[fullid] = f;
fileTransferBox(false, id, message.id, f);
break; break;
} }
case 'cancelfile': { case 'cancelfile': {
setFileStatus(false, id, message.id, 'Cancelled.', true, true); let f = TransferredFile.get(false, id, message.id);
deleteTransferredFile(false, id, message.id); if(!f)
throw new Error('unexpected cancelfile');
setFileStatus(f, 'Cancelled.', true, true);
f.close();
break; break;
} }
case 'getfile': { case 'getfile': {
sendFile(id, message.id, message.offer); let f = TransferredFile.get(true, id, message.id);
if(!f)
throw new Error('unexpected getfile');
sendFile(f, message.offer);
break; break;
} }
case 'rejectfile': { case 'rejectfile': {
setFileStatus(true, id, message.id, 'Rejected.', true, true); let f = TransferredFile.get(true, id, message.id);
deleteTransferredFile(true, id, message.id); if(!f)
throw new Error('unexpected rejectfile');
setFileStatus(f, 'Rejected.', true, true);
f.close();
break; break;
} }
case 'sendfile': { case 'sendfile': {
receiveFile(id, message.id, message.answer); let f = TransferredFile.get(false, id, message.id);
if(!f)
throw new Error('unexpected sendfile');
receiveFile(f, message.answer);
break; break;
} }
case 'filedownice': { case 'filedownice': {
let f = getTransferredFile(true, id, message.id); let f = TransferredFile.get(true, id, message.id);
if(!f.pc) { if(!f.pc) {
console.warn('Unexpected filedownice'); console.warn('Unexpected filedownice');
return; return;
@ -2726,7 +2736,7 @@ function gotUserMessage(id, dest, username, time, privileged, kind, message) {
break; break;
} }
case 'fileupice': { case 'fileupice': {
let f = getTransferredFile(false, id, message.id); let f = TransferredFile.get(false, id, message.id);
if(!f.pc) { if(!f.pc) {
console.warn('Unexpected fileupice'); console.warn('Unexpected fileupice');
return; return;
@ -3279,7 +3289,7 @@ commands.sendfile = {
let files = input.files; let files = input.files;
for(let i = 0; i < files.length; i++) { for(let i = 0; i < files.length; i++) {
try { try {
offerFile(p[0], id, files[i]); offerFile(p[i], id, files[i]);
} catch(e) { } catch(e) {
console.error(e); console.error(e);
displayError(e); displayError(e);