257 lines
8.6 KiB
TypeScript
257 lines
8.6 KiB
TypeScript
/* Note: This will be included into the controller and renderer process */
|
|
import { LogCategory, logError, logWarn } from "tc-shared/log";
|
|
import * as loader from "tc-loader";
|
|
import { Stage } from "tc-loader";
|
|
import * as crypto from "crypto-js";
|
|
import { tra } from "tc-shared/i18n/localize";
|
|
|
|
export type LocalAvatarInfo = {
|
|
fileName: string,
|
|
fileSize: number,
|
|
fileHashMD5: string,
|
|
|
|
fileUploaded: number,
|
|
fileModified: number,
|
|
|
|
contentType: string,
|
|
|
|
resourceUrl: string | undefined,
|
|
}
|
|
|
|
export type LocalAvatarUpdateResult = {
|
|
status: "success"
|
|
} | {
|
|
status: "error",
|
|
reason: string
|
|
} | {
|
|
status: "cache-unavailable"
|
|
};
|
|
|
|
export type LocalAvatarLoadResult<T> = {
|
|
status: "success",
|
|
result: T,
|
|
} | {
|
|
status: "error",
|
|
reason: string
|
|
} | {
|
|
status: "cache-unavailable" | "empty-result"
|
|
};
|
|
|
|
const kMaxAvatarSize = 16 * 1024 * 1024;
|
|
|
|
export type OwnAvatarMode = "uploading" | "server";
|
|
export class OwnAvatarStorage {
|
|
private openedCache: Cache | undefined;
|
|
|
|
private static generateRequestUrl(serverUniqueId: string, mode: OwnAvatarMode): string {
|
|
return "https://_local_avatar/" + serverUniqueId + "/" + mode;
|
|
}
|
|
|
|
async initialize() {
|
|
if (!("caches" in window)) {
|
|
/* Not available (may unsecure context?) */
|
|
this.openedCache = undefined;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.openedCache = await caches.open("local-avatars");
|
|
} catch (error) {
|
|
logError(LogCategory.GENERAL, tr("Failed to open local avatar cache: %o"), error);
|
|
return;
|
|
}
|
|
}
|
|
|
|
private async loadAvatarRequest(serverUniqueId: string, mode: OwnAvatarMode): Promise<LocalAvatarLoadResult<Response>> {
|
|
if (!this.openedCache) {
|
|
return { status: "cache-unavailable" };
|
|
}
|
|
|
|
try {
|
|
const response = await this.openedCache.match(OwnAvatarStorage.generateRequestUrl(serverUniqueId, mode), {
|
|
ignoreMethod: true,
|
|
ignoreSearch: true,
|
|
});
|
|
|
|
if (!response) {
|
|
return { status: "empty-result" };
|
|
}
|
|
|
|
return { status: "success", result: response };
|
|
} catch (error) {
|
|
logError(LogCategory.GENERAL, tr("Failed to query local avatar cache: %o"), error);
|
|
return { status: "error", reason: tr("failed to query cache") };
|
|
}
|
|
}
|
|
|
|
async loadAvatarImage(serverUniqueId: string, mode: OwnAvatarMode): Promise<LocalAvatarLoadResult<ArrayBuffer>> {
|
|
const loadResult = await this.loadAvatarRequest(serverUniqueId, mode);
|
|
if (loadResult.status !== "success") {
|
|
return loadResult;
|
|
}
|
|
|
|
try {
|
|
return { status: "success", result: await loadResult.result.arrayBuffer() };
|
|
} catch (error) {
|
|
logError(LogCategory.GENERAL, tr("Failed to load avatar into a buffer: %o"), error);
|
|
return { status: "error", reason: tr("failed to load avatar into a buffer") };
|
|
}
|
|
}
|
|
|
|
async loadAvatar(serverUniqueId: string, mode: OwnAvatarMode, createResourceUrl: boolean): Promise<LocalAvatarLoadResult<LocalAvatarInfo>> {
|
|
const loadResult = await this.loadAvatarRequest(serverUniqueId, mode);
|
|
if (loadResult.status !== "success") {
|
|
return loadResult;
|
|
}
|
|
|
|
const headers = loadResult.result.headers;
|
|
const contentType = headers.get("Content-Type");
|
|
const avatarSize = parseInt(headers.get("Content-Length"));
|
|
const avatarHash = headers.get("X-Avatar-Hash");
|
|
const avatarName = headers.get("X-File-Name");
|
|
const avatarDateModified = parseInt(headers.get("X-File-Date-Modified"));
|
|
const avatarDateUploaded = parseInt(headers.get("X-File-Uploaded"));
|
|
|
|
if (!avatarHash) {
|
|
return { status: "error", reason: tr("missing response header file hash") };
|
|
}
|
|
|
|
if (!avatarName) {
|
|
return { status: "error", reason: tr("missing response header file name") };
|
|
}
|
|
|
|
if (isNaN(avatarSize)) {
|
|
return { status: "error", reason: tr("missing/invalid response header file size") };
|
|
}
|
|
|
|
if (isNaN(avatarDateModified)) {
|
|
return { status: "error", reason: tr("missing/invalid response header file modify date") };
|
|
}
|
|
|
|
if (isNaN(avatarDateUploaded)) {
|
|
return { status: "error", reason: tr("missing/invalid response header file upload date") };
|
|
}
|
|
|
|
let resourceUrl;
|
|
if (createResourceUrl) {
|
|
try {
|
|
resourceUrl = URL.createObjectURL(await loadResult.result.blob());
|
|
} catch (error) {
|
|
logError(LogCategory.GENERAL, tr("Failed to create avatar resource url: %o"), error);
|
|
return { status: "error", reason: tr("failed to generate resource url") };
|
|
}
|
|
}
|
|
|
|
return {
|
|
status: "success",
|
|
result: {
|
|
contentType: contentType,
|
|
|
|
fileName: avatarName,
|
|
fileSize: avatarSize,
|
|
fileHashMD5: avatarHash,
|
|
|
|
fileModified: avatarDateModified,
|
|
fileUploaded: avatarDateUploaded,
|
|
|
|
resourceUrl: resourceUrl,
|
|
}
|
|
};
|
|
}
|
|
|
|
async updateAvatar(serverUniqueId: string, mode: OwnAvatarMode, target: File): Promise<LocalAvatarUpdateResult> {
|
|
if (!this.openedCache) {
|
|
return { status: "cache-unavailable" };
|
|
}
|
|
|
|
if (target.size > kMaxAvatarSize) {
|
|
return { status: "error", reason: tra("Image exceeds maximum software size of {} bytes", kMaxAvatarSize) };
|
|
}
|
|
|
|
let md5Hash: string;
|
|
try {
|
|
const hasher = crypto.algo.MD5.create();
|
|
await target.stream().pipeTo(new WritableStream({
|
|
write(data) {
|
|
hasher.update(crypto.lib.WordArray.create(Array.from(data)));
|
|
}
|
|
}));
|
|
|
|
md5Hash = hasher.finalize().toString(crypto.enc.Hex);
|
|
} catch (error) {
|
|
logError(LogCategory.GENERAL, tr("Failed to create avatar md5 hash: %o"), error);
|
|
return { status: "error", reason: tr("failed to create md5 hash") };
|
|
}
|
|
|
|
const headers = new Headers();
|
|
headers.set("X-Avatar-Hash", md5Hash);
|
|
headers.set("X-File-Name", target.name);
|
|
headers.set("X-File-Date-Modified", target.lastModified.toString());
|
|
headers.set("X-File-Uploaded", Date.now().toString());
|
|
headers.set("Content-Type", target.type);
|
|
headers.set("Content-Length", target.size.toString());
|
|
|
|
try {
|
|
const response = new Response(target, { headers: headers });
|
|
await this.openedCache.put("https://_local_avatar/" + serverUniqueId + "/" + mode, response);
|
|
} catch (error) {
|
|
logError(LogCategory.GENERAL, tr("Failed to save local avatar: %o"), error);
|
|
return { status: "error", reason: tr("failed to save avatar to disk") };
|
|
}
|
|
|
|
return { status: "success" };
|
|
}
|
|
|
|
async removeAvatar(serverUniqueId: string, mode: OwnAvatarMode) {
|
|
if (!this.openedCache) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await this.openedCache.delete(OwnAvatarStorage.generateRequestUrl(serverUniqueId, mode));
|
|
} catch (error) {
|
|
logWarn(LogCategory.GENERAL, tr("Failed to delete avatar request: %o"), error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move the avatar file which is currently in "uploading" state to server
|
|
* @param serverUniqueId
|
|
*/
|
|
async avatarUploadSucceeded(serverUniqueId: string) {
|
|
if (!this.openedCache) {
|
|
return;
|
|
}
|
|
|
|
const request = await this.loadAvatarRequest(serverUniqueId, "uploading");
|
|
if (request.status !== "success") {
|
|
if (request.status !== "empty-result") {
|
|
logError(LogCategory.GENERAL, tr("Failed to save uploaded avatar. Request failed to load: %o"), request);
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await this.openedCache.put(OwnAvatarStorage.generateRequestUrl(serverUniqueId, "server"), request.result);
|
|
} catch (error) {
|
|
logError(LogCategory.GENERAL, tr("Failed to save uploaded avatar. Failed to store request: %o"), request);
|
|
return;
|
|
}
|
|
|
|
await this.removeAvatar(serverUniqueId, "uploading");
|
|
}
|
|
}
|
|
|
|
let instance: OwnAvatarStorage;
|
|
export function getOwnAvatarStorage(): OwnAvatarStorage {
|
|
return instance;
|
|
}
|
|
|
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
|
priority: 10,
|
|
name: "own avatar storage",
|
|
function: async () => {
|
|
instance = new OwnAvatarStorage();
|
|
await instance.initialize();
|
|
}
|
|
}); |