From 2d9a990ffb1a4c246497c050dc5da01920564736 Mon Sep 17 00:00:00 2001 From: David Ludwig Date: Tue, 6 Apr 2021 12:37:46 -0500 Subject: [PATCH] Torrent client IPC added --- services/torrent-webui/.env.example | 3 +- services/torrent-webui/src/server/common.ts | 36 ++++ .../src/server/services/TorrentClientIpc.ts | 157 ++++++++++++++++++ 3 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 services/torrent-webui/src/server/common.ts create mode 100644 services/torrent-webui/src/server/services/TorrentClientIpc.ts diff --git a/services/torrent-webui/.env.example b/services/torrent-webui/.env.example index 71be24b..7bbc02f 100644 --- a/services/torrent-webui/.env.example +++ b/services/torrent-webui/.env.example @@ -1 +1,2 @@ -SERVER_PORT = +SERVER_PORT = 3100 +TORRENT_CLIENT_IPC_SOCKET = /tmp/torrent_client.sock diff --git a/services/torrent-webui/src/server/common.ts b/services/torrent-webui/src/server/common.ts new file mode 100644 index 0000000..c1aecf1 --- /dev/null +++ b/services/torrent-webui/src/server/common.ts @@ -0,0 +1,36 @@ +export interface ITorrent { + name: string, + infoHash: string, + progress: number, + state: TorrentState +} + +export enum TorrentState { + Ready = 0x1, + Paused = 0x2, + Done = 0x4 +} + +export interface ISerializedTorrent { + name : string; + infoHash : string; + downloaded : number; + uploaded : number; + ratio : number; + size : number; + downloadSpeed: number; + uploadSpeed : number; + numPeers : number; + progress : number; + path : string; + state : TorrentState; + files : ISerializedFile[]; +} + +export interface ISerializedFile { + path : string; + size : number; + downloaded: number; + progress : number; + selected : boolean; +} diff --git a/services/torrent-webui/src/server/services/TorrentClientIpc.ts b/services/torrent-webui/src/server/services/TorrentClientIpc.ts new file mode 100644 index 0000000..f91b533 --- /dev/null +++ b/services/torrent-webui/src/server/services/TorrentClientIpc.ts @@ -0,0 +1,157 @@ +import ipc from "node-ipc"; +import { Socket } from "net"; +import { ISerializedTorrent, ITorrent } from "../common"; + +interface IResponse { + response?: any, + error?: string | Error +} + +export default class TorrentClientIpc +{ + /** + * Indicate if there is an active connection to the IPC + */ + private __isConnected: boolean; + + /** + * The active IPC socket + */ + protected socket!: Socket; + + /** + * Create a new IPC client for the torrent client + */ + constructor() { + ipc.config.id = "torrent_webui"; + ipc.config.retry = 1500; + ipc.config.silent = true; + + this.__isConnected = false; + } + + /** + * Boot the torrent client IPC service + */ + public boot() { + return new Promise((resolve, reject) => { + ipc.connectTo("torrent_client", process.env["TORRENT_CLIENT_IPC_SOCKET"], () => { + this.socket = ipc.of["torrent_client"]; + this.installSocketEventHandlers(this.socket); + this.installSocketMessageHandlers(this.socket); + resolve(); + }); + }); + } + + /** + * Install the event handlers for the IPC socket + */ + protected installSocketEventHandlers(socket: Socket) { + socket.on("connect", () => this.onConnect()); + socket.on("error", (error: any) => this.onError(error)); + socket.on("disconnect", () => this.onDisconnect()); + socket.on("destroy", () => this.onDestroy()); + } + + protected installSocketMessageHandlers(socket: Socket) { + } + + // Socket Event Handlers ----------------------------------------------------------------------- + + protected onConnect() { + console.log("IPC: Connection established"); + this.__isConnected = true; + } + + protected onError(error: string | Error) { + console.log("IPC: Error occurred:", error); + } + + protected onDisconnect() { + console.log("IPC: Disconnected"); + this.__isConnected = false; + } + + protected onDestroy() { + console.log("IPC: Destroyed"); + } + + // Methods ------------------------------------------------------------------------------------- + + /** + * Perform a general request to the torrent client + */ + protected async request(method: string, message?: any) { + return new Promise((resolve, reject) => { + if (!this.isConnected) { + throw new Error("Not connected to torrent client"); + } + let respond = (response: any) => { + clearTimeout(timeout); + resolve(response); + } + // Include timeout mechanism in the off chance something breaks + let timeout = setTimeout(() => { + this.socket.off(method, respond); + reject("Torrent client IPC request timeout") + }, 1000); + this.socket.once(method, respond); + this.socket.emit(method, message); + }); + } + + /** + * Add a torrent to the client + * @param torrent Magnet URI or file buffer + */ + public async add(torrent: string | Buffer) { + let response = await this.request("add", torrent); + if (response.error) { + throw new Error("Failed to add torrent"); + } + return response.response; + } + + /** + * Remove a torrent from the client + * @param torrent Torrent info hash + */ + public async remove(torrent: string) { + let response = await this.request("remove", torrent); + if (response.error) { + throw new Error("Failed to remove torrent"); + } + } + + /** + * Get a list of all torrents in the client + */ + public async list() { + let response = await this.request("list"); + if (response.error) { + console.error(response.error); + throw new Error("Failed to obtain torrent list"); + } + return response.response; + } + + /** + * Get full details of each of the provided torrents + * @param torrentIds Array of torrent info hashes + */ + public async details(...torrentIds: string[]) { + let response = await this.request("details", torrentIds); + if (response.error) { + console.error(response.error); + throw new Error("Failed to retrieve torrent details"); + } + return response.response; + } + + // Accessors ----------------------------------------------------------------------------------- + + get isConnected() { + return this.__isConnected; + } +}