TeaWeb/shared/js/FileManager.ts

570 lines
19 KiB
TypeScript
Raw Normal View History

2018-02-27 16:20:49 +00:00
/// <reference path="client.ts" />
class FileEntry {
name: string;
datetime: number;
type: number;
size: number;
}
class FileListRequest {
path: string;
entries: FileEntry[];
callback: (entries: FileEntry[]) => void;
}
class DownloadFileTransfer {
transferId: number;
serverTransferId: number;
transferKey: string;
totalSize: number;
currentSize: number = 0;
remotePort: number;
remoteHost: string;
on_start: () => void = () => {};
on_complete: () => void = () => {};
on_fail: (reason: string) => void = (_) => {};
on_data: (data: Uint8Array) => void = (_) => {};
private _handle: FileManager;
private _promiseCallback: (value: DownloadFileTransfer) => void;
private _socket: WebSocket;
private _active: boolean;
private _succeed: boolean;
private _parseActive: boolean;
constructor(handle: FileManager, id: number) {
this.transferId = id;
this._handle = handle;
}
startTransfer() {
if(!this.remoteHost || !this.remotePort || !this.transferKey || !this.totalSize) {
this.on_fail("Missing data!");
return;
}
console.debug(tr("Create new file download to %s:%s (Key: %s, Expect %d bytes)"), this.remoteHost, this.remotePort, this.transferId, this.totalSize);
2018-02-27 16:20:49 +00:00
this._active = true;
2018-03-24 22:38:01 +00:00
this._socket = new WebSocket("wss://" + this.remoteHost + ":" + this.remotePort);
2018-02-27 16:20:49 +00:00
this._socket.onopen = this.onOpen.bind(this);
this._socket.onclose = this.onClose.bind(this);
this._socket.onmessage = this.onMessage.bind(this);
this._socket.onerror = this.onError.bind(this);
}
private onOpen() {
if(!this._active) return;
this._socket.send(this.transferKey);
this.on_start();
}
private onMessage(data: MessageEvent) {
if(!this._active) {
console.error(tr("Got data, but socket closed?"));
2018-02-27 16:20:49 +00:00
return;
}
this._parseActive = true;
2018-03-24 22:38:01 +00:00
let fileReader = new FileReader();
fileReader.onload = (event: any) => {
this.onBinaryData(new Uint8Array(event.target.result));
2018-04-16 18:38:35 +00:00
//if(this._socket.readyState != WebSocket.OPEN && !this._succeed) this.on_fail("unexpected close");
2018-03-24 22:38:01 +00:00
this._parseActive = false;
2018-02-27 16:20:49 +00:00
};
fileReader.readAsArrayBuffer(data.data);
}
private onBinaryData(data: Uint8Array) {
this.currentSize += data.length;
this.on_data(data);
if(this.currentSize == this.totalSize) {
this._succeed = true;
this.on_complete();
this.disconnect();
}
}
private onError() {
if(!this._active) return;
this.on_fail(tr("an error occurent"));
2018-02-27 16:20:49 +00:00
this.disconnect();
}
private onClose() {
if(!this._active) return;
if(!this._parseActive) this.on_fail(tr("unexpected close (remote closed)"));
2018-02-27 16:20:49 +00:00
this.disconnect();
}
private disconnect(){
this._active = false;
//this._socket.close();
}
}
class FileManager {
handle: TSClient;
icons: IconManager;
2018-04-16 18:38:35 +00:00
avatars: AvatarManager;
2018-02-27 16:20:49 +00:00
private listRequests: FileListRequest[] = [];
private pendingDownloadTransfers: DownloadFileTransfer[] = [];
private downloadCounter : number = 0;
constructor(client: TSClient) {
this.handle = client;
this.icons = new IconManager(this);
2018-04-16 18:38:35 +00:00
this.avatars = new AvatarManager(this);
2018-02-27 16:20:49 +00:00
this.handle.serverConnection.commandHandler["notifyfilelist"] = this.notifyFileList.bind(this);
this.handle.serverConnection.commandHandler["notifyfilelistfinished"] = this.notifyFileListFinished.bind(this);
this.handle.serverConnection.commandHandler["notifystartdownload"] = this.notifyStartDownload.bind(this);
}
/******************************** 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.sendCommand("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) {
console.error(tr("Invalid file list entry. Path: %s"), json[0]["path"]);
2018-02-27 16:20:49 +00:00
return;
}
for(let e of (json as Array<FileEntry>))
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) {
console.error(tr("Invalid file list entry finish. Path: "), json[0]["path"]);
2018-02-27 16:20:49 +00:00
return;
}
entry.callback(entry.entries);
}
/******************************** File download ********************************/
requestFileDownload(path: string, file: string, channel?: ChannelEntry, password?: string) : Promise<DownloadFileTransfer> {
const _this = this;
let transfer = new DownloadFileTransfer(this, this.downloadCounter++);
this.pendingDownloadTransfers.push(transfer);
return new Promise<DownloadFileTransfer>((resolve, reject) => {
transfer["_promiseCallback"] = resolve;
_this.handle.serverConnection.sendCommand("ftinitdownload", {
"path": path,
"name": file,
"cid": (channel ? channel.channelId : "0"),
"cpw": (password ? password : ""),
"clientftfid": transfer.transferId
}).catch(reason => {
_this.pendingDownloadTransfers.remove(transfer);
reject(reason);
})
});
}
private notifyStartDownload(json) {
json = json[0];
let transfer: DownloadFileTransfer;
for(let e of this.pendingDownloadTransfers)
if(e.transferId == json["clientftfid"]) {
transfer = e;
break;
}
transfer.serverTransferId = json["serverftfid"];
transfer.transferKey = json["ftkey"];
transfer.totalSize = json["size"];
transfer.remotePort = json["port"];
2018-05-07 09:51:50 +00:00
transfer.remoteHost = (json["ip"] ? json["ip"] : "").replace(/,/g, "");
if(!transfer.remoteHost || transfer.remoteHost == '0.0.0.0' || transfer.remoteHost == '127.168.0.0')
transfer.remoteHost = this.handle.serverConnection._remote_address.host;
2018-02-27 16:20:49 +00:00
(transfer["_promiseCallback"] as (val: DownloadFileTransfer) => void)(transfer);
this.pendingDownloadTransfers.remove(transfer);
}
}
class Icon {
id: number;
name: string;
base64: string;
}
2018-11-25 12:45:45 +00:00
enum ImageType {
UNKNOWN,
BITMAP,
PNG,
GIF,
SVG,
JPEG
}
function media_image_type(type: ImageType) {
switch (type) {
case ImageType.BITMAP:
return "bmp";
case ImageType.GIF:
return "gif";
case ImageType.SVG:
return "svg+xml";
case ImageType.JPEG:
return "jpeg";
case ImageType.UNKNOWN:
case ImageType.PNG:
default:
return "png";
}
}
function image_type(base64: string) {
const bin = atob(base64);
if(bin.length < 10) return ImageType.UNKNOWN;
if(bin[0] == String.fromCharCode(66) && bin[1] == String.fromCharCode(77)) {
return ImageType.BITMAP;
} else if(bin.substr(0, 8) == "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a") {
return ImageType.PNG;
} else if(bin.substr(0, 4) == "\x47\x49\x46\x38" && (bin[4] == '\x37' || bin[4] == '\x39') && bin[5] == '\x61') {
return ImageType.GIF;
} else if(bin[0] == '\x3c') {
return ImageType.SVG;
} else if(bin[0] == '\xFF' && bin[1] == '\xd8') {
return ImageType.JPEG;
}
return ImageType.UNKNOWN;
}
2018-02-27 16:20:49 +00:00
class IconManager {
handle: FileManager;
2018-08-12 17:01:47 +00:00
private loading_icons: {promise: Promise<Icon>, id: number}[] = [];
2018-02-27 16:20:49 +00:00
constructor(handle: FileManager) {
this.handle = handle;
}
iconList() : Promise<FileEntry[]> {
return this.handle.requestFileList("/icons");
}
downloadIcon(id: number) : Promise<DownloadFileTransfer> {
return this.handle.requestFileDownload("", "/icon_" + id);
}
resolveCached?(id: number) : Icon {
let icon = localStorage.getItem("icon_" + id);
if(icon) {
let i = JSON.parse(icon) as Icon;
if(i.base64.length > 0) { //TODO timestamp?
return i;
}
}
return undefined;
}
2018-08-12 17:01:47 +00:00
private load_finished(id: number) {
for(let entry of this.loading_icons)
if(entry.id == id)
this.loading_icons.remove(entry);
}
2018-02-27 16:20:49 +00:00
loadIcon(id: number) : Promise<Icon> {
2018-08-12 17:01:47 +00:00
for(let entry of this.loading_icons)
if(entry.id == id) return entry.promise;
let promise = new Promise<Icon>((resolve, reject) => {
2018-02-27 16:20:49 +00:00
let icon = this.resolveCached(id);
if(icon){
2018-08-12 17:01:47 +00:00
this.load_finished(id);
2018-02-27 16:20:49 +00:00
resolve(icon);
return;
}
2018-08-12 17:01:47 +00:00
this.downloadIcon(id).then(ft => {
2018-02-27 16:20:49 +00:00
let array = new Uint8Array(0);
ft.on_fail = reason => {
2018-08-12 17:01:47 +00:00
this.load_finished(id);
console.error(tr("Could not download icon %s -> %s"), id, tr(reason));
chat.serverChat().appendError(tr("Fail to download icon {0}. ({1})"), id, JSON.stringify(reason));
2018-02-27 16:20:49 +00:00
reject(reason);
};
ft.on_start = () => {};
ft.on_data = (data: Uint8Array) => {
array = concatenate(Uint8Array, array, data);
};
ft.on_complete = () => {
let base64 = btoa(String.fromCharCode.apply(null, array));
let icon = new Icon();
icon.base64 = base64;
icon.id = id;
icon.name = "icon_" + id;
localStorage.setItem("icon_" + id, JSON.stringify(icon));
2018-08-12 17:01:47 +00:00
this.load_finished(id);
2018-02-27 16:20:49 +00:00
resolve(icon);
};
ft.startTransfer();
}).catch(reason => {
console.error(tr("Error while downloading icon! (%s)"), tr(JSON.stringify(reason)));
chat.serverChat().appendError(tr("Failed to request download for icon {0}. ({1})"), id, tr(JSON.stringify(reason)));
2018-02-27 16:20:49 +00:00
reject(reason);
});
});
2018-08-12 17:01:47 +00:00
this.loading_icons.push({promise: promise, id: id});
return promise;
2018-02-27 16:20:49 +00:00
}
//$("<img width=\"16\" height=\"16\" alt=\"tick\" src=\"data:image/png;base64," + value.base64 + "\">")
2018-09-30 19:50:59 +00:00
generateTag(id: number) : JQuery<HTMLDivElement> {
2018-02-27 16:20:49 +00:00
if(id == 0)
2019-02-17 11:17:17 +00:00
return $.spawn("div").addClass("icon_empty");
2018-02-27 16:20:49 +00:00
else if(id < 1000)
2019-02-17 11:17:17 +00:00
return $.spawn("div").addClass("icon client-group_" + id);
2018-02-27 16:20:49 +00:00
2018-04-16 18:38:35 +00:00
let tag = $.spawn("div");
2019-02-17 15:06:43 +00:00
tag.addClass("icon-container icon_empty");
2018-02-27 16:20:49 +00:00
2018-03-07 18:06:52 +00:00
let img = $.spawn("img");
2018-02-27 16:20:49 +00:00
img.attr("width", 16).attr("height", 16).attr("alt", "");
let icon = this.resolveCached(id);
if(icon) {
2018-11-25 12:45:45 +00:00
const type = image_type(icon.base64);
const media = media_image_type(type);
console.debug(tr("Icon has an image type of %o (media: %o)"), type, media);
2018-11-25 12:45:45 +00:00
img.attr("src", "data:image/" + media + ";base64," + icon.base64);
2019-02-17 11:17:17 +00:00
tag.append(img).removeClass("icon_empty");
2018-02-27 16:20:49 +00:00
} else {
img.attr("src", "file://null");
2018-04-16 18:38:35 +00:00
let loader = $.spawn("div");
2018-02-27 16:20:49 +00:00
loader.addClass("icon_loading");
tag.append(loader);
this.loadIcon(id).then(icon => {
2018-11-25 12:45:45 +00:00
const type = image_type(icon.base64);
const media = media_image_type(type);
console.debug(tr("Icon has an image type of %o (media: %o)"), type, media);
2018-11-25 12:45:45 +00:00
img.attr("src", "data:image/" + media + ";base64," + icon.base64);
console.debug(tr("Icon %o loaded :)"), id);
2018-02-27 16:20:49 +00:00
img.css("opacity", 0);
2019-02-17 11:17:17 +00:00
tag.append(img).removeClass("icon_empty");
2018-02-27 16:20:49 +00:00
loader.animate({opacity: 0}, 50, function () {
$(this).detach();
img.animate({opacity: 1}, 150);
});
}).catch(reason => {
console.error(tr("Could not load icon %o. Reason: %p"), id, reason);
2018-05-09 09:50:05 +00:00
loader.removeClass("icon_loading").addClass("icon client-warning").attr("tag", "Could not load icon " + id);
2018-04-16 18:38:35 +00:00
});
}
return tag;
}
}
class Avatar {
clientUid: string;
avatarId: string;
2018-08-12 14:38:38 +00:00
base64?: string;
url?: string;
blob?: Blob;
2018-04-16 18:38:35 +00:00
}
class AvatarManager {
handle: FileManager;
private loading_avatars: {promise: Promise<Avatar>, name: string}[] = [];
private loaded_urls: string[] = [];
2018-04-16 18:38:35 +00:00
constructor(handle: FileManager) {
this.handle = handle;
}
downloadAvatar(client: ClientEntry) : Promise<DownloadFileTransfer> {
console.log(tr("Downloading avatar %s"), client.avatarId());
2018-04-16 18:38:35 +00:00
return this.handle.requestFileDownload("", "/avatar_" + client.avatarId());
}
resolveCached?(client: ClientEntry) : Avatar {
let avatar = localStorage.getItem("avatar_" + client.properties.client_unique_identifier);
if(avatar) {
let i = JSON.parse(avatar) as Avatar;
//TODO timestamp?
if(i.avatarId != client.properties.client_flag_avatar) return undefined;
2018-08-12 14:38:38 +00:00
if(i.base64) {
if(i.base64.length > 0)
2018-08-12 14:38:38 +00:00
return i;
else i.base64 = undefined;
}
if(i.url) {
for(let url of this.loaded_urls)
if(url == i.url) return i;
2018-04-16 18:38:35 +00:00
}
}
return undefined;
}
private load_finished(name: string) {
for(let entry of this.loading_avatars)
if(entry.name == name)
this.loading_avatars.remove(entry);
}
2018-04-16 18:38:35 +00:00
loadAvatar(client: ClientEntry) : Promise<Avatar> {
let name = client.avatarId();
for(let promise of this.loading_avatars)
if(promise.name == name) return promise.promise;
let promise = new Promise<Avatar>((resolve, reject) => {
2018-04-16 18:38:35 +00:00
let avatar = this.resolveCached(client);
if(avatar){
2018-08-12 17:01:47 +00:00
this.load_finished(name);
2018-04-16 18:38:35 +00:00
resolve(avatar);
return;
}
this.downloadAvatar(client).then(ft => {
2018-04-16 18:38:35 +00:00
let array = new Uint8Array(0);
ft.on_fail = reason => {
this.load_finished(name);
console.error(tr("Could not download avatar %o -> %s"), client.properties.client_flag_avatar, reason);
chat.serverChat().appendError(tr("Fail to download avatar for {0}. ({1})"), client.clientNickName(), JSON.stringify(reason));
2018-04-16 18:38:35 +00:00
reject(reason);
};
ft.on_start = () => {};
ft.on_data = (data: Uint8Array) => {
array = concatenate(Uint8Array, array, data);
};
ft.on_complete = () => {
let avatar = new Avatar();
2018-11-25 12:45:45 +00:00
if(array.length >= 1024 * 1024) {
2018-08-12 14:38:38 +00:00
let blob_image = new Blob([array]);
avatar.url = URL.createObjectURL(blob_image);
avatar.blob = blob_image;
this.loaded_urls.push(avatar.url);
2018-08-12 14:38:38 +00:00
} else {
avatar.base64 = btoa(String.fromCharCode.apply(null, array));
2018-08-12 14:38:38 +00:00
}
2018-04-16 18:38:35 +00:00
avatar.clientUid = client.clientUid();
avatar.avatarId = client.properties.client_flag_avatar;
localStorage.setItem("avatar_" + client.properties.client_unique_identifier, JSON.stringify(avatar));
this.load_finished(name);
2018-04-16 18:38:35 +00:00
resolve(avatar);
};
ft.startTransfer();
}).catch(reason => {
this.load_finished(name);
console.error(tr("Error while downloading avatar! (%s)"), JSON.stringify(reason));
chat.serverChat().appendError(tr("Failed to request avatar download for {0}. ({1})"), client.clientNickName(), JSON.stringify(reason));
2018-04-16 18:38:35 +00:00
reject(reason);
});
});
this.loading_avatars.push({promise: promise, name: name});
return promise;
2018-04-16 18:38:35 +00:00
}
generateTag(client: ClientEntry) {
let tag = $.spawn("div");
let img = $.spawn("img");
img.attr("alt", "");
let avatar = this.resolveCached(client);
if(avatar) {
if(avatar.url)
img.attr("src", avatar.url);
2018-11-25 12:45:45 +00:00
else {
const type = image_type(avatar.base64);
const media = media_image_type(type);
console.debug(tr("avatar has an image type of %o (media: %o)"), type, media);
2018-11-25 12:45:45 +00:00
img.attr("src", "data:image/" + media + ";base64," + avatar.base64);
}
2018-04-16 18:38:35 +00:00
tag.append(img);
} else {
2018-04-30 21:57:21 +00:00
let loader = $.spawn("img");
loader.attr("src", "img/loading_image.svg").css("width", "75%");
2018-04-16 18:38:35 +00:00
tag.append(loader);
this.loadAvatar(client).then(avatar => {
2018-08-12 14:38:38 +00:00
if(avatar.url)
img.attr("src", avatar.url);
2018-11-25 12:45:45 +00:00
else {
const type = image_type(avatar.base64);
const media = media_image_type(type);
console.debug(tr("Avatar has an image type of %o (media: %o)"), type, media);
2018-11-25 12:45:45 +00:00
img.attr("src", "data:image/" + media + ";base64," + avatar.base64);
}
2018-04-16 18:38:35 +00:00
console.debug("Avatar " + client.clientNickName() + " loaded :)");
img.css("opacity", 0);
tag.append(img);
loader.animate({opacity: 0}, 50, function () {
$(this).detach();
img.animate({opacity: 1}, 150);
});
}).catch(reason => {
console.error(tr("Could not load avatar for %s. Reason: %s"), client.clientNickName(), reason);
2018-04-16 18:38:35 +00:00
//TODO Broken image
loader.addClass("icon client-warning").attr("tag", tr("Could not load avatar ") + client.clientNickName());
2018-02-27 16:20:49 +00:00
});
}
return tag;
}
}