feat: adding image processing

This commit is contained in:
Chubby Granny Chaser 2024-09-14 00:09:34 +01:00
commit 6e543fecb4
No known key found for this signature in database
18 changed files with 165 additions and 77 deletions

View File

@ -4,7 +4,7 @@ import {
defineConfig, defineConfig,
loadEnv, loadEnv,
swcPlugin, swcPlugin,
externalizeDepsPlugin externalizeDepsPlugin,
} from "electron-vite"; } from "electron-vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import { sentryVitePlugin } from "@sentry/vite-plugin"; import { sentryVitePlugin } from "@sentry/vite-plugin";
@ -13,7 +13,7 @@ import svgr from "vite-plugin-svgr";
var sentryPlugin = sentryVitePlugin({ var sentryPlugin = sentryVitePlugin({
authToken: process.env.SENTRY_AUTH_TOKEN, authToken: process.env.SENTRY_AUTH_TOKEN,
org: "hydra-launcher", org: "hydra-launcher",
project: "hydra-launcher" project: "hydra-launcher",
}); });
var electron_vite_config_default = defineConfig(({ mode }) => { var electron_vite_config_default = defineConfig(({ mode }) => {
loadEnv(mode); loadEnv(mode);
@ -22,37 +22,35 @@ var electron_vite_config_default = defineConfig(({ mode }) => {
build: { build: {
sourcemap: true, sourcemap: true,
rollupOptions: { rollupOptions: {
external: ["better-sqlite3"] external: ["better-sqlite3"],
} },
}, },
resolve: { resolve: {
alias: { alias: {
"@main": resolve("src/main"), "@main": resolve("src/main"),
"@locales": resolve("src/locales"), "@locales": resolve("src/locales"),
"@resources": resolve("resources"), "@resources": resolve("resources"),
"@shared": resolve("src/shared") "@shared": resolve("src/shared"),
}
}, },
plugins: [externalizeDepsPlugin(), swcPlugin(), sentryPlugin] },
plugins: [externalizeDepsPlugin(), swcPlugin(), sentryPlugin],
}, },
preload: { preload: {
plugins: [externalizeDepsPlugin()] plugins: [externalizeDepsPlugin()],
}, },
renderer: { renderer: {
build: { build: {
sourcemap: true sourcemap: true,
}, },
resolve: { resolve: {
alias: { alias: {
"@renderer": resolve("src/renderer/src"), "@renderer": resolve("src/renderer/src"),
"@locales": resolve("src/locales"), "@locales": resolve("src/locales"),
"@shared": resolve("src/shared") "@shared": resolve("src/shared"),
} },
},
plugins: [svgr(), react(), vanillaExtractPlugin(), sentryPlugin],
}, },
plugins: [svgr(), react(), vanillaExtractPlugin(), sentryPlugin]
}
}; };
}); });
export { export { electron_vite_config_default as default };
electron_vite_config_default as default
};

View File

@ -54,7 +54,6 @@
"electron-log": "^5.1.4", "electron-log": "^5.1.4",
"electron-updater": "^6.1.8", "electron-updater": "^6.1.8",
"fetch-cookie": "^3.0.1", "fetch-cookie": "^3.0.1",
"file-type": "^19.0.0",
"flexsearch": "^0.7.43", "flexsearch": "^0.7.43",
"i18next": "^23.11.2", "i18next": "^23.11.2",
"i18next-browser-languagedetector": "^7.2.1", "i18next-browser-languagedetector": "^7.2.1",

View File

@ -4,3 +4,4 @@ cx_Logging; sys_platform == 'win32'
lief; sys_platform == 'win32' lief; sys_platform == 'win32'
pywin32; sys_platform == 'win32' pywin32; sys_platform == 'win32'
psutil psutil
Pillow

View File

@ -286,6 +286,10 @@
"friend_code_copied": "Friend code copied", "friend_code_copied": "Friend code copied",
"undo_friendship_modal_text": "This will undo your friendship with {{displayName}}", "undo_friendship_modal_text": "This will undo your friendship with {{displayName}}",
"privacy_hint": "To adjust who can see this, go to the <0>Settings</0>", "privacy_hint": "To adjust who can see this, go to the <0>Settings</0>",
"locked_profile": "This profile is private" "locked_profile": "This profile is private",
"image_process_failure": "Failure while processing the image",
"required_field": "This field is required",
"displayname_min_length": "Display name must be at least 3 characters long",
"displayname_max_length": "Display name must be at most 50 characters long"
} }
} }

View File

@ -289,6 +289,10 @@
"friend_code_copied": "Código de amigo copiado", "friend_code_copied": "Código de amigo copiado",
"undo_friendship_modal_text": "Isso irá remover sua amizade com {{displayName}}", "undo_friendship_modal_text": "Isso irá remover sua amizade com {{displayName}}",
"privacy_hint": "Pra controlar quem pode ver seu perfil, acesse a <0>Tela de Configurações</0>", "privacy_hint": "Pra controlar quem pode ver seu perfil, acesse a <0>Tela de Configurações</0>",
"profile_locked": "Este perfil é privado" "profile_locked": "Este perfil é privado",
"image_process_failure": "Falha ao processar a imagem",
"required_field": "Este campo é obrigatório",
"displayname_min_length": "Nome de exibição deve ter pelo menos 3 caracteres",
"displayname_max_length": "Nome de exibição deve ter no máximo 50 caracteres"
} }
} }

View File

@ -277,6 +277,7 @@
"pending": "Pendentes", "pending": "Pendentes",
"no_pending_invites": "Não tens convites de amizade pendentes", "no_pending_invites": "Não tens convites de amizade pendentes",
"no_blocked_users": "Não tens nenhum utilizador bloqueado", "no_blocked_users": "Não tens nenhum utilizador bloqueado",
"friend_code_copied": "Código de amigo copiado" "friend_code_copied": "Código de amigo copiado",
"image_process_failure": "Falha ao processar a imagem"
} }
} }

View File

@ -3,11 +3,8 @@ import path from "node:path";
export const defaultDownloadsPath = app.getPath("downloads"); export const defaultDownloadsPath = app.getPath("downloads");
export const databasePath = path.join( export const databaseDirectory = path.join(app.getPath("appData"), "hydra");
app.getPath("appData"), export const databasePath = path.join(databaseDirectory, "hydra.db");
"hydra",
"hydra.db"
);
export const logsPath = path.join(app.getPath("appData"), "hydra", "logs"); export const logsPath = path.join(app.getPath("appData"), "hydra", "logs");

View File

@ -54,6 +54,7 @@ import "./profile/get-me";
import "./profile/undo-friendship"; import "./profile/undo-friendship";
import "./profile/update-friend-request"; import "./profile/update-friend-request";
import "./profile/update-profile"; import "./profile/update-profile";
import "./profile/process-profile-image";
import "./profile/send-friend-request"; import "./profile/send-friend-request";
import { isPortableVersion } from "@main/helpers"; import { isPortableVersion } from "@main/helpers";

View File

@ -0,0 +1,11 @@
import { registerEvent } from "../register-event";
import { PythonInstance } from "@main/services";
const processProfileImage = async (
_event: Electron.IpcMainInvokeEvent,
path: string
) => {
return PythonInstance.processProfileImage(path);
};
registerEvent("processProfileImage", processProfileImage);

View File

@ -1,50 +1,54 @@
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { HydraApi, logger } from "@main/services"; import { HydraApi, PythonInstance } from "@main/services";
import axios from "axios";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { fileTypeFromFile } from "file-type";
import type { UpdateProfileRequest, UserProfile } from "@types"; import type { UpdateProfileRequest, UserProfile } from "@types";
import { omit } from "lodash-es";
import axios from "axios";
interface PresignedResponse {
presignedUrl: string;
profileImageUrl: string;
}
const patchUserProfile = async (updateProfile: UpdateProfileRequest) => { const patchUserProfile = async (updateProfile: UpdateProfileRequest) => {
return HydraApi.patch("/profile", updateProfile); return HydraApi.patch<UserProfile>("/profile", updateProfile);
};
const getNewProfileImageUrl = async (localImageUrl: string) => {
const { imagePath, mimeType } =
await PythonInstance.processProfileImage(localImageUrl);
const stats = fs.statSync(imagePath);
const fileBuffer = fs.readFileSync(imagePath);
const fileSizeInBytes = stats.size;
const { presignedUrl, profileImageUrl } =
await HydraApi.post<PresignedResponse>(`/presigned-urls/profile-image`, {
imageExt: path.extname(imagePath).slice(1),
imageLength: fileSizeInBytes,
});
await axios.put(presignedUrl, fileBuffer, {
headers: {
"Content-Type": mimeType,
},
});
return profileImageUrl;
}; };
const updateProfile = async ( const updateProfile = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
updateProfile: UpdateProfileRequest updateProfile: UpdateProfileRequest
): Promise<UserProfile> => { ) => {
if (!updateProfile.profileImageUrl) { if (!updateProfile.profileImageUrl) {
return patchUserProfile(updateProfile); return patchUserProfile(omit(updateProfile, "profileImageUrl"));
} }
const newProfileImagePath = updateProfile.profileImageUrl; const profileImageUrl = await getNewProfileImageUrl(
updateProfile.profileImageUrl
const stats = fs.statSync(newProfileImagePath); ).catch(() => undefined);
const fileBuffer = fs.readFileSync(newProfileImagePath);
const fileSizeInBytes = stats.size;
const profileImageUrl = await HydraApi.post(`/presigned-urls/profile-image`, {
imageExt: path.extname(newProfileImagePath).slice(1),
imageLength: fileSizeInBytes,
})
.then(async (preSignedResponse) => {
const { presignedUrl, profileImageUrl } = preSignedResponse;
const mimeType = await fileTypeFromFile(newProfileImagePath);
await axios.put(presignedUrl, fileBuffer, {
headers: {
"Content-Type": mimeType?.mime,
},
});
return profileImageUrl as string;
})
.catch((err) => {
logger.error("Error uploading profile image", err);
return undefined;
});
return patchUserProfile({ ...updateProfile, profileImageUrl }); return patchUserProfile({ ...updateProfile, profileImageUrl });
}; };

View File

@ -4,12 +4,14 @@ import updater from "electron-updater";
import i18n from "i18next"; import i18n from "i18next";
import path from "node:path"; import path from "node:path";
import url from "node:url"; import url from "node:url";
import fs from "node:fs";
import { electronApp, optimizer } from "@electron-toolkit/utils"; import { electronApp, optimizer } from "@electron-toolkit/utils";
import { logger, PythonInstance, WindowManager } from "@main/services"; import { logger, PythonInstance, WindowManager } from "@main/services";
import { dataSource } from "@main/data-source"; import { dataSource } from "@main/data-source";
import resources from "@locales"; import resources from "@locales";
import { userPreferencesRepository } from "@main/repository"; import { userPreferencesRepository } from "@main/repository";
import { knexClient, migrationConfig } from "./knex-client"; import { knexClient, migrationConfig } from "./knex-client";
import { databaseDirectory } from "./constants";
const { autoUpdater } = updater; const { autoUpdater } = updater;
@ -54,6 +56,10 @@ if (process.defaultApp) {
} }
const runMigrations = async () => { const runMigrations = async () => {
if (!fs.existsSync(databaseDirectory)) {
fs.mkdirSync(databaseDirectory, { recursive: true });
}
await knexClient.migrate.list(migrationConfig).then((result) => { await knexClient.migrate.list(migrationConfig).then((result) => {
logger.log( logger.log(
"Migrations to run:", "Migrations to run:",

View File

@ -166,6 +166,14 @@ export class PythonInstance {
this.downloadingGameId = -1; this.downloadingGameId = -1;
} }
static async processProfileImage(imagePath: string) {
return this.rpc
.post<{ imagePath: string; mimeType: string }>("/profile-image", {
image_path: imagePath,
})
.then((response) => response.data);
}
private static async handleRpcError(_error: unknown) { private static async handleRpcError(_error: unknown) {
await this.rpc.get("/healthcheck").catch(() => { await this.rpc.get("/healthcheck").catch(() => {
logger.error( logger.error(

View File

@ -145,6 +145,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("undoFriendship", userId), ipcRenderer.invoke("undoFriendship", userId),
updateProfile: (updateProfile: UpdateProfileRequest) => updateProfile: (updateProfile: UpdateProfileRequest) =>
ipcRenderer.invoke("updateProfile", updateProfile), ipcRenderer.invoke("updateProfile", updateProfile),
processProfileImage: (imagePath: string) =>
ipcRenderer.invoke("processProfileImage", imagePath),
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"), getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
updateFriendRequest: (userId: string, action: FriendRequestAction) => updateFriendRequest: (userId: string, action: FriendRequestAction) =>
ipcRenderer.invoke("updateFriendRequest", userId, action), ipcRenderer.invoke("updateFriendRequest", userId, action),

View File

@ -150,6 +150,10 @@ declare global {
updateProfile: ( updateProfile: (
updateProfile: UpdateProfileRequest updateProfile: UpdateProfileRequest
) => Promise<UserProfile>; ) => Promise<UserProfile>;
updateProfile: (updateProfile: UpdateProfileProps) => Promise<UserProfile>;
processProfileImage: (
path: string
) => Promise<{ imagePath: string; mimeType: string }>;
getFriendRequests: () => Promise<FriendRequest[]>; getFriendRequests: () => Promise<FriendRequest[]>;
updateFriendRequest: ( updateFriendRequest: (
userId: string, userId: string,

View File

@ -102,7 +102,7 @@ export function EditProfileModal(
filters: [ filters: [
{ {
name: "Image", name: "Image",
extensions: ["jpg", "jpeg", "png"], extensions: ["jpg", "jpeg", "png", "gif", "webp"],
}, },
], ],
}); });
@ -110,7 +110,14 @@ export function EditProfileModal(
if (filePaths && filePaths.length > 0) { if (filePaths && filePaths.length > 0) {
const path = filePaths[0]; const path = filePaths[0];
onChange(path); const { imagePath } = await window.electron
.processProfileImage(path)
.catch(() => {
showErrorToast(t("image_process_failure"));
return { imagePath: null };
});
onChange(imagePath);
} }
}; };

View File

@ -4,6 +4,7 @@ import json
import urllib.parse import urllib.parse
import psutil import psutil
from torrent_downloader import TorrentDownloader from torrent_downloader import TorrentDownloader
from profile_image_processor import ProfileImageProcessor
torrent_port = sys.argv[1] torrent_port = sys.argv[1]
http_port = sys.argv[2] http_port = sys.argv[2]
@ -73,7 +74,6 @@ class Handler(BaseHTTPRequestHandler):
def do_POST(self): def do_POST(self):
global torrent_downloader global torrent_downloader
if self.path == "/action":
if self.headers.get(self.rpc_password_header) != rpc_password: if self.headers.get(self.rpc_password_header) != rpc_password:
self.send_response(401) self.send_response(401)
self.end_headers() self.end_headers()
@ -83,6 +83,21 @@ class Handler(BaseHTTPRequestHandler):
post_data = self.rfile.read(content_length) post_data = self.rfile.read(content_length)
data = json.loads(post_data.decode('utf-8')) data = json.loads(post_data.decode('utf-8'))
if self.path == "/profile-image":
parsed_image_path = data['image_path']
try:
parsed_image_path, mime_type = ProfileImageProcessor.process_image(parsed_image_path)
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({'imagePath': parsed_image_path, 'mimeType': mime_type}).encode('utf-8'))
except:
self.send_response(400)
self.end_headers()
elif self.path == "/action":
if torrent_downloader is None: if torrent_downloader is None:
torrent_downloader = TorrentDownloader(torrent_port) torrent_downloader = TorrentDownloader(torrent_port)
@ -99,6 +114,10 @@ class Handler(BaseHTTPRequestHandler):
self.send_response(200) self.send_response(200)
self.end_headers() self.end_headers()
else:
self.send_response(404)
self.end_headers()
if __name__ == "__main__": if __name__ == "__main__":
httpd = HTTPServer(("", int(http_port)), Handler) httpd = HTTPServer(("", int(http_port)), Handler)

View File

@ -0,0 +1,31 @@
from PIL import Image
import tempfile
import os, uuid
class ProfileImageProcessor:
@staticmethod
def get_parsed_image_data(image_path):
Image.MAX_IMAGE_PIXELS = 933120000
image = Image.open(image_path)
try:
image.seek(1)
except EOFError:
mime_type = image.get_format_mimetype()
return image_path, mime_type
else:
newUUID = str(uuid.uuid4())
new_image_path = os.path.join(tempfile.gettempdir(), newUUID) + ".webp"
image.save(new_image_path)
new_image = Image.open(new_image_path)
mime_type = new_image.get_format_mimetype()
return new_image_path, mime_type
@staticmethod
def process_image(image_path):
return ProfileImageProcessor.get_parsed_image_data(image_path)

View File

@ -4347,15 +4347,6 @@ file-type@^18.7.0:
strtok3 "^7.0.0" strtok3 "^7.0.0"
token-types "^5.0.1" token-types "^5.0.1"
file-type@^19.0.0:
version "19.0.0"
resolved "https://registry.npmjs.org/file-type/-/file-type-19.0.0.tgz"
integrity sha512-s7cxa7/leUWLiXO78DVVfBVse+milos9FitauDLG1pI7lNaJ2+5lzPnr2N24ym+84HVwJL6hVuGfgVE+ALvU8Q==
dependencies:
readable-web-to-node-stream "^3.0.2"
strtok3 "^7.0.0"
token-types "^5.0.1"
file-uri-to-path@1.0.0: file-uri-to-path@1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz" resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz"