TeaWeb/shared/js/file/FileManager.tsx
2020-05-08 12:38:23 +02:00

433 lines
No EOL
14 KiB
TypeScript

import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import {ChannelEntry} from "tc-shared/ui/channel";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {ServerCommand} from "tc-shared/connection/ConnectionBase";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler";
import {IconManager} from "tc-shared/file/Icons";
import {AvatarManager} from "tc-shared/file/Avatars";
export class FileEntry {
name: string;
datetime: number;
type: number;
size: number;
}
export class FileListRequest {
path: string;
entries: FileEntry[];
callback: (entries: FileEntry[]) => void;
}
export interface TransferKey {
client_transfer_id: number;
server_transfer_id: number;
key: string;
file_path: string;
file_name: string;
peer: {
hosts: string[],
port: number;
};
total_size: number;
}
export interface UploadOptions {
name: string;
path: string;
channel?: ChannelEntry;
channel_password?: string;
size: number;
overwrite: boolean;
}
export interface DownloadTransfer {
get_key() : DownloadKey;
request_file() : Promise<Response>;
}
export interface UploadTransfer {
get_key(): UploadKey;
put_data(data: BlobPart | File) : Promise<void>;
}
export type DownloadKey = TransferKey;
export type UploadKey = TransferKey;
export interface TransferProvider {
spawn_download_transfer(key: DownloadKey) : DownloadTransfer;
spawn_upload_transfer(key: UploadKey) : UploadTransfer;
}
let transfer_provider_: TransferProvider = new class implements TransferProvider {
spawn_download_transfer(key: TransferKey): DownloadTransfer {
return new RequestFileDownload(key);
}
spawn_upload_transfer(key: TransferKey): UploadTransfer {
return new RequestFileUpload(key);
}
};
export function transfer_provider() : TransferProvider {
return transfer_provider_;
}
export function set_transfer_provider(provider: TransferProvider) {
transfer_provider_ = provider;
}
export class RequestFileDownload implements DownloadTransfer {
readonly transfer_key: DownloadKey;
constructor(key: DownloadKey) {
this.transfer_key = key;
}
async request_file() : Promise<Response> {
return await this.try_fetch("https://" + this.transfer_key.peer.hosts[0] + ":" + this.transfer_key.peer.port);
}
private async try_fetch(url: string) : Promise<Response> {
const response = await fetch(url, {
method: 'GET',
cache: "no-cache",
mode: 'cors',
headers: {
'transfer-key': this.transfer_key.key,
'download-name': this.transfer_key.file_name,
'Access-Control-Allow-Headers': '*',
'Access-Control-Expose-Headers': '*'
}
});
if(!response.ok) {
debugger;
throw (response.type == 'opaque' || response.type == 'opaqueredirect' ? "invalid cross origin flag! May target isn't a TeaSpeak server?" : response.statusText || "response is not ok");
}
return response;
}
get_key(): DownloadKey {
return this.transfer_key;
}
}
export class RequestFileUpload implements UploadTransfer {
readonly transfer_key: UploadKey;
constructor(key: DownloadKey) {
this.transfer_key = key;
}
get_key(): UploadKey {
return this.transfer_key;
}
async put_data(data: BlobPart | File) : Promise<void> {
const form_data = new FormData();
if(data instanceof File) {
if(data.size != this.transfer_key.total_size)
throw "invalid size";
form_data.append("file", data);
} else if(typeof(data) === "string") {
if(data.length != this.transfer_key.total_size)
throw "invalid size";
form_data.append("file", new Blob([data], { type: "application/octet-stream" }));
} else {
const buffer = data as BufferSource;
if(buffer.byteLength != this.transfer_key.total_size)
throw "invalid size";
form_data.append("file", new Blob([buffer], { type: "application/octet-stream" }));
}
await this.try_put(form_data, "https://" + this.transfer_key.peer.hosts[0] + ":" + this.transfer_key.peer.port);
}
private async try_put(data: FormData, url: string) : Promise<void> {
const response = await fetch(url, {
method: 'POST',
cache: "no-cache",
mode: 'cors',
body: data,
headers: {
'transfer-key': this.transfer_key.key,
'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");
}
}
export class FileManager extends AbstractCommandHandler {
handle: ConnectionHandler;
icons: IconManager;
avatars: AvatarManager;
private listRequests: FileListRequest[] = [];
private pending_download_requests: DownloadKey[] = [];
private pending_upload_requests: UploadKey[] = [];
private transfer_counter : number = 1;
constructor(client: ConnectionHandler) {
super(client.serverConnection);
this.handle = client;
this.icons = new IconManager(this);
this.avatars = new AvatarManager(this);
this.connection.command_handler_boss().register_handler(this);
}
destroy() {
if(this.connection) {
const hboss = this.connection.command_handler_boss();
if(hboss)
hboss.unregister_handler(this);
}
this.listRequests = undefined;
this.pending_download_requests = undefined;
this.pending_upload_requests = undefined;
this.icons && this.icons.destroy();
this.icons = undefined;
this.avatars && this.avatars.destroy();
this.avatars = undefined;
}
handle_command(command: ServerCommand): boolean {
switch (command.command) {
case "notifyfilelist":
this.notifyFileList(command.arguments);
return true;
case "notifyfilelistfinished":
this.notifyFileListFinished(command.arguments);
return true;
case "notifystartdownload":
this.notifyStartDownload(command.arguments);
return true;
case "notifystartupload":
this.notifyStartUpload(command.arguments);
return true;
}
return false;
}
/******************************** File list ********************************/
//TODO multiple requests (same path)
requestFileList(path: string, channel?: ChannelEntry, password?: string) : Promise<FileEntry[]> {
const _this = this;
return new Promise((accept, reject) => {
let req = new FileListRequest();
req.path = path;
req.entries = [];
req.callback = accept;
_this.listRequests.push(req);
_this.handle.serverConnection.send_command("ftgetfilelist", {"path": path, "cid": (channel ? channel.channelId : "0"), "cpw": (password ? password : "")}).then(() => {}).catch(reason => {
_this.listRequests.remove(req);
if(reason instanceof CommandResult) {
if(reason.id == 0x0501) {
accept([]); //Empty result
return;
}
}
reject(reason);
});
});
}
private notifyFileList(json) {
let entry : FileListRequest = undefined;
for(let e of this.listRequests) {
if(e.path == json[0]["path"]){
entry = e;
break;
}
}
if(!entry) {
log.error(LogCategory.CLIENT, tr("Invalid file list entry. Path: %s"), json[0]["path"]);
return;
}
for(let e of (json as Array<FileEntry>)) {
e.datetime = parseInt(e.datetime + "");
e.size = parseInt(e.size + "");
e.type = parseInt(e.type + "");
entry.entries.push(e);
}
}
private notifyFileListFinished(json) {
let entry : FileListRequest = undefined;
for(let e of this.listRequests) {
if(e.path == json[0]["path"]){
entry = e;
this.listRequests.remove(e);
break;
}
}
if(!entry) {
log.error(LogCategory.CLIENT, tr("Invalid file list entry finish. Path: "), json[0]["path"]);
return;
}
entry.callback(entry.entries);
}
/******************************** File download/upload ********************************/
download_file(path: string, file: string, channel?: ChannelEntry, password?: string) : Promise<DownloadKey> {
const transfer_data: DownloadKey = {
file_name: file,
file_path: path,
client_transfer_id: this.transfer_counter++
} as any;
this.pending_download_requests.push(transfer_data);
return new Promise<DownloadKey>((resolve, reject) => {
transfer_data["_callback"] = resolve;
this.handle.serverConnection.send_command("ftinitdownload", {
"path": path,
"name": file,
"cid": (channel ? channel.channelId : "0"),
"cpw": (password ? password : ""),
"clientftfid": transfer_data.client_transfer_id,
"seekpos": 0,
"proto": 1
}, {process_result: false}).catch(reason => {
this.pending_download_requests.remove(transfer_data);
reject(reason);
})
});
}
upload_file(options: UploadOptions) : Promise<UploadKey> {
const transfer_data: UploadKey = {
file_path: options.path,
file_name: options.name,
client_transfer_id: this.transfer_counter++,
total_size: options.size
} as any;
this.pending_upload_requests.push(transfer_data);
return new Promise<UploadKey>((resolve, reject) => {
transfer_data["_callback"] = resolve;
this.handle.serverConnection.send_command("ftinitupload", {
"path": options.path,
"name": options.name,
"cid": (options.channel ? options.channel.channelId : "0"),
"cpw": options.channel_password || "",
"clientftfid": transfer_data.client_transfer_id,
"size": options.size,
"overwrite": options.overwrite,
"resume": false,
"proto": 1
}).catch(reason => {
this.pending_upload_requests.remove(transfer_data);
reject(reason);
})
});
}
private notifyStartDownload(json) {
json = json[0];
let clientftfid = parseInt(json["clientftfid"]);
let transfer: DownloadKey;
for(let e of this.pending_download_requests)
if(e.client_transfer_id == clientftfid) {
transfer = e;
break;
}
transfer.server_transfer_id = parseInt(json["serverftfid"]);
transfer.key = json["ftkey"];
transfer.total_size = json["size"];
transfer.peer = {
hosts: (json["ip"] || "").split(","),
port: parseInt(json["port"])
};
if(transfer.peer.hosts.length == 0)
transfer.peer.hosts.push("0.0.0.0");
if(transfer.peer.hosts[0].length == 0 || transfer.peer.hosts[0] == '0.0.0.0')
transfer.peer.hosts[0] = this.handle.serverConnection.remote_address().host;
(transfer["_callback"] as (val: DownloadKey) => void)(transfer);
this.pending_download_requests.remove(transfer);
}
private notifyStartUpload(json) {
json = json[0];
let transfer: UploadKey;
let clientftfid = parseInt(json["clientftfid"]);
for(let e of this.pending_upload_requests)
if(e.client_transfer_id == clientftfid) {
transfer = e;
break;
}
transfer.server_transfer_id = parseInt(json["serverftfid"]);
transfer.key = json["ftkey"];
transfer.peer = {
hosts: (json["ip"] || "").split(","),
port: parseInt(json["port"])
};
if(transfer.peer.hosts.length == 0)
transfer.peer.hosts.push("0.0.0.0");
if(transfer.peer.hosts[0].length == 0 || transfer.peer.hosts[0] == '0.0.0.0')
transfer.peer.hosts[0] = this.handle.serverConnection.remote_address().host;
(transfer["_callback"] as (val: UploadKey) => void)(transfer);
this.pending_upload_requests.remove(transfer);
}
/** File management **/
async delete_file(props: {
name: string,
path?: string;
cid?: number;
cpw?: string;
}) : Promise<void> {
if(!props.name)
throw "invalid name!";
try {
await this.handle.serverConnection.send_command("ftdeletefile", {
cid: props.cid || 0,
cpw: props.cpw,
path: props.path || "",
name: props.name
})
} catch(error) {
throw error;
}
}
}