TeaWeb/web/app/FileTransfer.ts

334 lines
11 KiB
TypeScript

import {
BrowserFileTransferSource,
BufferTransferSource,
DownloadTransferTarget,
FileDownloadTransfer,
FileTransfer,
FileTransferState,
FileUploadTransfer,
ResponseTransferTarget,
TextTransferSource,
TransferProvider,
TransferSourceType,
TransferTargetType
} from "tc-shared/file/Transfer";
import {LogCategory, logError} from "tc-shared/log";
import { tr } from "tc-shared/i18n/localize";
TransferProvider.setProvider(new class extends TransferProvider {
executeFileUpload(transfer: FileUploadTransfer) {
try {
if(!transfer.source) throw tr("transfer source is undefined");
let response: Promise<void>;
transfer.setTransferState(FileTransferState.CONNECTING);
if(transfer.source instanceof BrowserFileTransferSourceImpl) {
response = formDataUpload(transfer, transfer.source.getFile());
} else if(transfer.source instanceof BufferTransferSourceImpl) {
response = formDataUpload(transfer, transfer.source.getBuffer());
} else if(transfer.source instanceof TextTransferSourceImpl) {
response = formDataUpload(transfer, transfer.source.getArrayBuffer());
} else {
transfer.setFailed({
error: "io",
reason: "unsupported-target"
}, tr("invalid source type"));
return;
}
/* let the server notify us when the transfer has been finished */
response.catch(error => {
if(typeof error !== "string")
logError(LogCategory.FILE_TRANSFER, tr("Failed to upload object via HTTPS connection: %o"), error);
transfer.setFailed({
error: "connection",
reason: "network-error",
extraMessage: typeof error === "string" ? error : tr("Lookup the console")
}, typeof error === "string" ? error : tr("Lookup the console"));
});
} catch (error) {
if(typeof error !== "string")
logError(LogCategory.FILE_TRANSFER, tr("Failed to initialize transfer source: %o"), error);
transfer.setFailed({
error: "io",
reason: "failed-to-initialize-target",
extraMessage: typeof error === "string" ? error : tr("Lookup the console")
}, typeof error === "string" ? error : tr("Lookup the console"));
}
}
executeFileDownload(transfer: FileDownloadTransfer) {
try {
if(!transfer.target) throw tr("transfer target is undefined");
let response: Promise<void>;
transfer.setTransferState(FileTransferState.CONNECTING);
if(transfer.target instanceof ResponseTransferTargetImpl) {
response = responseFileDownload(transfer, transfer.target);
} else if(transfer.target instanceof DownloadTransferTargetImpl) {
response = downloadFileDownload(transfer, transfer.target);
} else {
transfer.setFailed({
error: "io",
reason: "unsupported-target"
}, tr("invalid transfer target type"));
return;
}
response.then(() => {
if(!transfer.isFinished()) {
/* we still need to stream the body */
transfer.setTransferState(FileTransferState.RUNNING);
}
}).catch(error => {
if(typeof error !== "string")
logError(LogCategory.FILE_TRANSFER, tr("Failed to download file to response object: %o"), error);
transfer.setFailed({
error: "connection",
reason: "network-error",
extraMessage: typeof error === "string" ? error : tr("Lookup the console")
}, typeof error === "string" ? error : tr("Lookup the console"));
});
} catch (error) {
if(typeof error !== "string")
logError(LogCategory.FILE_TRANSFER, tr("Failed to initialize transfer target: %o"), error);
transfer.setFailed({
error: "io",
reason: "failed-to-initialize-target",
extraMessage: typeof error === "string" ? error : tr("Lookup the console")
}, typeof error === "string" ? error : tr("Lookup the console"));
}
}
targetSupported(type: TransferTargetType) {
switch (type) {
case TransferTargetType.DOWNLOAD:
case TransferTargetType.RESPONSE:
return true;
default:
return false;
}
}
async createDownloadTarget(filename: string) {
return new DownloadTransferTargetImpl(filename);
}
async createResponseTarget() {
return new ResponseTransferTargetImpl();
}
sourceSupported(type: TransferSourceType) {
switch (type) {
case TransferSourceType.BROWSER_FILE:
case TransferSourceType.BUFFER:
case TransferSourceType.TEXT:
return true;
default:
return false;
}
}
async createBufferSource(buffer: ArrayBuffer): Promise<BufferTransferSource> {
return new BufferTransferSourceImpl(buffer);
}
async createBrowserFileSource(file: File): Promise<BrowserFileTransferSource> {
return new BrowserFileTransferSourceImpl(file);
}
async createTextSource(text: string): Promise<TextTransferSource> {
return new TextTransferSourceImpl(text);
}
});
function generateTransferURL(transfer: FileTransfer, fileName?: string) {
const properties = transfer.transferProperties();
const url = "https://" + properties.addresses[0].serverAddress + ":" + properties.addresses[0].serverPort + "/";
const parameters = {
"transfer-key": properties.transferKey
};
if(typeof fileName !== "undefined")
parameters["file-name"] = fileName;
const query = "?" + Object.keys(parameters).map(e => e + "=" + encodeURIComponent(parameters[e])).join("&");
return url + query;
}
async function performHTTPSTransfer(transfer: FileTransfer, body: FormData | undefined) : Promise<Response> {
try {
const response = await fetch(generateTransferURL(transfer), {
method: typeof body === "number" ? "GET" : "POST",
cache: "no-cache",
mode: "cors",
body: body,
headers: {
/* for legacy TeaSpeak servers (prior to 1.4.15) */
'transfer-key': transfer.transferProperties().transferKey,
'download-name': transfer.properties.name,
/* end legacy */
"Access-Control-Allow-Headers": "*",
"Access-Control-Expose-Headers": "*"
}
});
if(!response.ok) {
throw (response.type == 'opaque' || response.type == 'opaqueredirect' ? "invalid cross origin flag! May target isn't a TeaSpeak server?" : response.statusText || "response is not ok");
}
/* the transfer may not running anymore, because of a finished signal from the server (especially on file upload!) */
if(transfer.isRunning()) {
response.clone().blob().then(() => {
if(transfer.isRunning())
transfer.setTransferState(FileTransferState.FINISHED);
}).catch(error => {
if(typeof error !== "string")
logError(LogCategory.FILE_TRANSFER, tr("Failed to transfer data throw a HTTPS request: %o"), error);
transfer.setFailed({
error: "io",
reason: "buffer-transfer-failed",
extraMessage: typeof error === "string" ? error : tr("lookup the console")
}, typeof error === "string" ? error : tr("lookup the console"));
});
}
return response;
} catch (error) {
if(error instanceof Error && error.message === "Failed to fetch")
throw "HTTPS download failed";
throw error;
}
}
async function responseFileDownload(transfer: FileDownloadTransfer, target: ResponseTransferTargetImpl) {
target.setResponse(await performHTTPSTransfer(transfer, undefined));
}
async function downloadFileDownload(transfer: FileDownloadTransfer, target: DownloadTransferTargetImpl) {
const url = generateTransferURL(transfer);
target.startDownloadURL(url);
}
export class ResponseTransferTargetImpl extends ResponseTransferTarget {
private response: Response;
constructor() {
super();
}
hasResponse() {
return typeof this.response !== "undefined";
}
getResponse() {
return this.response;
}
setResponse(response: Response) {
this.response = response;
}
}
class DownloadTransferTargetImpl extends DownloadTransferTarget {
readonly fileName: string | undefined;
constructor(fileName: string | undefined) {
super();
this.fileName = fileName;
}
startDownloadURL(url: string) {
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.target = "_blank";
if(this.fileName)
a.download = this.fileName;
document.body.appendChild(a);
a.click();
a.remove();
}
}
class BrowserFileTransferSourceImpl extends BrowserFileTransferSource {
private readonly file: File;
constructor(file: File) {
super();
this.file = file;
}
getFile(): File {
return this.file;
}
async fileSize(): Promise<number> {
return this.file.size;
}
}
class BufferTransferSourceImpl extends BufferTransferSource {
private readonly buffer: ArrayBuffer;
constructor(buffer: ArrayBuffer) {
super();
this.buffer = buffer;
}
getBuffer(): ArrayBuffer {
return this.buffer;
}
async fileSize(): Promise<number> {
return this.buffer.byteLength;
}
}
class TextTransferSourceImpl extends TextTransferSource {
private readonly text: string;
private buffer: ArrayBuffer;
constructor(text: string) {
super();
this.text = text;
}
getText(): string {
return this.text;
}
async fileSize(): Promise<number> {
return this.getArrayBuffer().byteLength;
}
getArrayBuffer() : ArrayBuffer {
if(this.buffer) return this.buffer;
const encoder = new TextEncoder();
this.buffer = encoder.encode(this.text);
return this.buffer;
}
}
async function formDataUpload(transfer: FileUploadTransfer, data: File | ArrayBuffer | string) {
const formData = new FormData();
if(data instanceof File) {
formData.append("file", data);
} else if(typeof(data) === "string") {
formData.append("file", new Blob([data], { type: "application/octet-stream" }));
} else {
const buffer = data as BufferSource;
formData.append("file", new Blob([buffer], { type: "application/octet-stream" }));
}
await performHTTPSTransfer(transfer, formData);
}