TeaWeb/shared/js/file/LocalIcons.ts

369 lines
15 KiB
TypeScript

import * as loader from "tc-loader";
import {Stage} from "tc-loader";
import {ImageCache, ImageType, imageType2MediaType, responseImageType} from "tc-shared/file/ImageCache";
import {AbstractIconManager, kIPCIconChannel, RemoteIcon, RemoteIconState, setIconManager} from "tc-shared/file/Icons";
import {LogCategory, logDebug, logError, logWarn} from "tc-shared/log";
import {server_connections} from "tc-shared/ConnectionManager";
import {ConnectionEvents, ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
import {FileTransferState, ResponseTransferTarget, TransferProvider, TransferTargetType} from "tc-shared/file/Transfer";
import {tr} from "tc-shared/i18n/localize";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {ErrorCode} from "tc-shared/connection/ErrorCode";
import {ChannelMessage, IPCChannel} from "tc-shared/ipc/BrowserIPC";
import * as ipc from "tc-shared/ipc/BrowserIPC";
/* TODO: Retry icon download after some time */
/* TODO: Download icon when we're connected to the server were we want the icon from and update the icon */
async function responseToImageUrl(response: Response) : Promise<string> {
if(!response.headers.has('X-media-bytes')) {
throw "missing media bytes";
}
const type = responseImageType(response.headers.get('X-media-bytes'));
if(type === ImageType.UNKNOWN) {
throw "unknown image type";
}
const media = imageType2MediaType(type);
const blob = await response.blob();
if(blob.type !== "image/" + media) {
return URL.createObjectURL(blob.slice(0, blob.size, "image/" + media));
} else {
return URL.createObjectURL(blob)
}
}
class LocalRemoteIcon extends RemoteIcon {
constructor(serverUniqueId: string, iconId: number) {
super(serverUniqueId, iconId);
}
destroy() {
super.destroy();
if(this.imageUrl && "revokeObjectURL" in URL) {
URL.revokeObjectURL(this.imageUrl);
}
}
public setImageUrl(url: string) {
super.setImageUrl(url);
}
public setErrorMessage(message: string) {
super.setErrorMessage(message);
}
public setState(state: RemoteIconState) {
super.setState(state);
}
}
export let localIconCache: ImageCache;
class IconManager extends AbstractIconManager {
private cachedIcons: {[key: string]: LocalRemoteIcon} = {};
private connectionStateChangeListener: {[key: string]: (handlerId: string, event: ConnectionEvents["notify_connection_state_changed"]) => void} = {};
private ipcChannel: IPCChannel;
constructor() {
super();
this.ipcChannel = ipc.getIpcInstance().createChannel(kIPCIconChannel);
this.ipcChannel.messageHandler = this.handleIpcMessage.bind(this);
server_connections.events().on("notify_handler_created", event => {
this.connectionStateChangeListener[event.handlerId] = this.handleHandlerStateChange.bind(this, event.handlerId);
event.handler.events().on("notify_connection_state_changed", this.connectionStateChangeListener[event.handlerId] as any);
});
server_connections.events().on("notify_handler_deleted", event => {
if(this.connectionStateChangeListener[event.handlerId]) {
event.handler.events().off("notify_connection_state_changed", this.connectionStateChangeListener[event.handlerId] as any);
delete this.connectionStateChangeListener[event.handlerId];
}
});
}
destroy() {
Object.values(this.cachedIcons).forEach(icon => icon.destroy());
this.cachedIcons = {};
/* TODO: Unregister server handler events */
}
private handleHandlerStateChange(handlerId: string, event: ConnectionEvents["notify_connection_state_changed"]) {
const connection = server_connections.findConnection(handlerId);
if(!connection) {
logWarn(LogCategory.CLIENT, tr("Received handler state changed event for invalid handler id %s"), handlerId);
return;
}
if(event.newState !== ConnectionState.CONNECTED) {
return;
}
/* update all empty icons */
Object.values(this.cachedIcons).forEach((icon: LocalRemoteIcon) => {
if(icon.serverUniqueId !== connection.getCurrentServerUniqueId()) {
return;
}
if(icon.getState() === "empty") {
this.wrapIconDownload(icon, connection, IconManager.iconUniqueKey(icon.iconId, icon.serverUniqueId)).then(() => {});
}
});
}
private handleIconStateChanged(icon: RemoteIcon) {
this.sendIconStateChange(icon);
}
private sendIconStateChange(icon: RemoteIcon, remoteId?: string) {
let data = {} as any;
data.iconUniqueId = IconManager.iconUniqueKey(icon.iconId, icon.serverUniqueId);
data.status = icon.getState();
switch (icon.getState()) {
case "loaded":
data.url = icon.hasImageUrl() ? icon.getImageUrl() : undefined;
break;
case "error":
data.errorMessage = icon.getErrorMessage();
break;
}
this.ipcChannel.sendMessage("notify-icon-status", data, remoteId);
}
private handleIpcMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) {
if(broadcast) { return; }
if(message.type === "initialize") {
this.ipcChannel.sendMessage("initialized", {}, remoteId);
return;
} else if(message.type === "icon-resolve") {
this.sendIconStateChange(this.resolveIcon(message.data.iconId, message.data.serverUniqueId, message.data.handlerId), remoteId);
}
}
resolveIcon(iconId: number, serverUniqueId: string, handlerIdHint: string): RemoteIcon {
serverUniqueId = serverUniqueId || "";
/* just to ensure */
iconId = iconId >>> 0;
const iconUniqueId = IconManager.iconUniqueKey(iconId, serverUniqueId);
if(this.cachedIcons[iconUniqueId]) {
return this.cachedIcons[iconUniqueId];
}
let icon = new LocalRemoteIcon(serverUniqueId, iconId);
this.cachedIcons[iconUniqueId] = icon;
icon.events.on("notify_state_changed", () => this.handleIconStateChanged(icon));
if(iconId >= 0 && iconId <= 1000) {
icon.setState("loaded");
} else {
this.loadIcon(icon, iconUniqueId, handlerIdHint).catch(error => {
if(typeof error !== "string") {
logError(LogCategory.FILE_TRANSFER, tr("Failed to load icon %d (%s): %o"), iconId, serverUniqueId, error);
icon.setErrorMessage(tr("load error, lookup the console"));
} else{
icon.setErrorMessage(error);
icon.setState("error");
}
});
}
return icon;
}
private async loadIcon(icon: LocalRemoteIcon, iconUniqueId: string, handlerIdHint: string) {
/* try to load the icon from the local cache */
const localCache = await localIconCache.resolveCached(iconUniqueId);
if(localCache) {
try {
const url = await responseToImageUrl(localCache);
icon.setImageUrl(url);
icon.setState("loaded");
logDebug(LogCategory.FILE_TRANSFER, tr("Loaded icon %d (%s) from local cache."), icon.iconId, icon.serverUniqueId);
return;
} catch (error) {
logWarn(LogCategory.FILE_TRANSFER, tr("Failed to decode locally cached icon %d (%s): %o. Invalidating cache."), icon.iconId, icon.serverUniqueId, error);
try {
await localIconCache.delete(iconUniqueId);
} catch (error) {
logWarn(LogCategory.FILE_TRANSFER, tr("Failed to delete invalid key from icon cache (%s): %o"), iconUniqueId, error);
}
}
}
/* try to fetch the icon from the server, if we're connected */
let handler = server_connections.findConnection(handlerIdHint);
if(handler) {
if(!handler.connected) {
logWarn(LogCategory.FILE_TRANSFER, tr("Received handler id hint for icon download, but handler %s is not connected. Trying others."), handlerIdHint);
handler = undefined;
} else if(icon.serverUniqueId && handler.channelTree.server.properties.virtualserver_unique_identifier !== icon.serverUniqueId) {
logWarn(LogCategory.FILE_TRANSFER,
tr("Received handler id hint for icon download, but handler %s is not connected to the expected server (%s <=> %s). Trying others."),
handlerIdHint, handler.channelTree.server.properties.virtualserver_unique_identifier, icon.serverUniqueId);
handler = undefined;
} else {
logDebug(LogCategory.FILE_TRANSFER, tr("Icon %s (%s) not found locally but the suggested handler is connected to the server. Downloading icon."), icon.iconId, icon.serverUniqueId);
}
/* we don't want any "handler not found" warning */
handlerIdHint = undefined;
}
if(!handler) {
if(handlerIdHint) {
logWarn(LogCategory.FILE_TRANSFER, tr("Received handler id hint for icon download, but handler %s does not exists. Trying others."), handlerIdHint);
}
const connections = server_connections.getAllConnectionHandlers()
.filter(handler => handler.connected)
.filter(handler => handler.channelTree.server.properties.virtualserver_unique_identifier === icon.serverUniqueId);
if(connections.length === 0) {
logDebug(LogCategory.FILE_TRANSFER, tr("Icon %s (%s) not found locally and we're currently not connected to the target server. Returning an empty result."), icon.iconId, icon.serverUniqueId);
icon.setState("empty");
return;
}
logDebug(LogCategory.FILE_TRANSFER, tr("Icon %s (%s) not found locally but we're connected to the server, using first available connection (%s). Downloading icon."), icon.iconId, icon.serverUniqueId, connections[0].handlerId);
handler = connections[0];
}
await this.wrapIconDownload(icon, handler, iconUniqueId);
}
private async wrapIconDownload(icon: LocalRemoteIcon, handler: ConnectionHandler, iconUniqueId: string) {
try {
await this.downloadIcon(icon, handler, iconUniqueId);
} catch (error) {
if(typeof error !== "string") {
logError(LogCategory.FILE_TRANSFER, tr("Failed to download icon %d (%s) from %s: %o"), icon.iconId, icon.serverUniqueId, handler.handlerId, error);
error = tr("download failed, lookup the console");
}
icon.setErrorMessage(error);
icon.setState("error");
return;
}
if(icon.getState() === "loading") {
icon.setErrorMessage(tr("unexpected loading state"));
icon.setState("error");
}
}
private async downloadIcon(icon: LocalRemoteIcon, handler: ConnectionHandler, iconUniqueId: string) {
const transfer = handler.fileManager.initializeFileDownload({
path: "",
name: "/icon_" + icon.iconId,
targetSupplier: async () => await TransferProvider.provider().createResponseTarget()
});
try {
await transfer.awaitFinished();
if(transfer.transferState() === FileTransferState.CANCELED) {
throw tr("download canceled");
} else if(transfer.transferState() === FileTransferState.ERRORED) {
throw transfer.currentError();
} else if(transfer.transferState() === FileTransferState.FINISHED) {
} else {
throw tr("Unknown transfer finished state");
}
} catch(error) {
if(error instanceof CommandResult) {
if(error.id === ErrorCode.FILE_NOT_FOUND) {
throw tr("Icon could not be found");
} else if(error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
throw tr("No permissions to download icon");
} else {
throw error.extra_message || error.message;
}
}
logError(LogCategory.FILE_TRANSFER, tr("Could not request download for icon %d: %o"), icon.iconId, error);
if(error === transfer.currentError()) {
throw transfer.currentErrorMessage();
}
throw typeof error === "string" ? error : tr("Failed to initialize icon download");
}
/* could only be tested here, because before we don't know which target we have */
if(transfer.target.type !== TransferTargetType.RESPONSE) {
throw "unsupported transfer target";
}
const response = transfer.target as ResponseTransferTarget;
if(!response.hasResponse()) {
throw tr("Transfer has no response");
}
try {
const url = await responseToImageUrl(response.getResponse().clone());
icon.setImageUrl(url);
icon.setState("loaded");
} catch (error) {
if(typeof error !== "string") {
logError(LogCategory.FILE_TRANSFER, tr("Failed to convert downloaded icon %d (%s) into an url: %o"), icon.iconId, icon.serverUniqueId, error);
error = tr("download failed, lookup the console");
}
icon.setErrorMessage(error);
icon.setState("error");
return;
}
try {
const resp = response.getResponse();
if(!resp.headers.has('X-media-bytes')) {
throw "missing media bytes";
}
const type = responseImageType(resp.headers.get('X-media-bytes'));
if(type === ImageType.UNKNOWN) {
throw "unknown image type";
}
const media = imageType2MediaType(type);
await localIconCache.putCache(iconUniqueId, response.getResponse(), "image/" + media);
} catch (error) {
logWarn(LogCategory.FILE_TRANSFER, tr("Failed to save icon %s (%s) into local icon cache: %o"), icon.iconId, icon.serverUniqueId, error);
}
}
}
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "icon init",
priority: 60,
function: async () => {
localIconCache = await ImageCache.load("icons");
setIconManager(new IconManager());
}
});
//TODO!
/*
window.addEventListener("beforeunload", () => {
icon_cache_loader.clear_memory_cache();
});
(window as any).flush_icon_cache = async () => {
icon_cache_loader.clear_memory_cache();
await icon_cache_loader.clear_cache();
server_connections.all_connections().forEach(e => {
e.fileManager.icons.flush_cache();
});
};
*/