最近有空把之前弄了一半的 aria2 的包继续搞了搞,写一点东西,记录过程中一些有趣的点。
naria2 は aria2 RPC インターフェース を呼び出し、抽象化された aria2 ダウンローダー JavaScript / TypeScript クライアントライブラリ をラップしたものであり、同時に aria2c を基にしたいくつかの追加機能を提供する CLI ツールアプリケーション でもあります。
クロスプラットフォームの統一インストール方法#
aria2 を使用する前に、まず aria2 をダウンロードする必要があります。これは一見無駄なことのように思えますが、aria2 は一般的にシステムにプリインストールされているものではないため、C++ で書かれたネイティブアプリケーションとして、そのインストールは必ずしも簡単ではありません。
したがって、いくつかの可能なソリューションがあります。たとえば、クライアントパッケージとして、なぜユーザーにダウンロードを手伝わせる必要があるのか、ユーザーがダウンロードしたら PATH
に追加して使えばいいのです。OK、問題ありません、ドキュメントに少し追加すればいいですが、やはりあまり優雅ではありません。
したがって、このライブラリを使用するユーザーにはあまり多くの詳細を意識させたくないと思っています:バックグラウンドで aria2 RPC インターフェースサービスのプロセスを起動します。ユーザーはただ知っていればいいのです:
- いくつかのシンプルな非同期関数を呼び出して、クライアントの初期化を完了すること;
- 抽象化された API を通じて、Torrent のさまざまなダウンロードや監視タスクを完了すること;
- すべてのロジック処理が完了したら、クライアントを破棄すること。
したがって、異なるプラットフォームのユーザーに対して、aria2 を簡単にダウンロードできる方法を提供したいと考えています。
リモートインストールスクリプトを取得して実行する#
現在、多くのクロスプラットフォームアプリケーションがこのインストール方法を選択しています。これらは、インストールスクリプトを配布するための Web サービスを立ち上げます。Rust の例を挙げると、上のコマンドをコピーして、エンターを押すだけで Rust をインストールできます。
しかし、この図からもわかるように、Windows のみが特別で、インストールパッケージをダウンロードする必要があります。なぜなら、これらの *nix システムで実行できるスクリプトは、Windows の PowerShell や CMD で実行するのが容易ではないからです。たとえば、親切な Microsoft は Invoke-WebRequest (iwr) に "curl" のエイリアスを付けましたが、使い方は curl とは全く異なります。
次に、配布スクリプトのサービスを維持するのは簡単でも簡単ではないです。簡単なのは、静的ページをデプロイするプラットフォームや Serverless 関数プラットフォームを適当に使えば、すぐにスクリプトを上げられるからです。しかし、ドメインを購入する必要があるのか、SSL 証明書を取得する必要があるのか、数年ごとにドメインを更新する必要があるのか、国内では壁の内外を考慮する必要があるのか、Cloudflare がいくつかの面倒な作業をしてくれたとしても、やはり少し面倒です。もちろん、簡単な方法もあります。感謝 egoist の数年間のプロジェクト bina は、あなたのプロジェクトの GitHub リリース内のプラットフォームを示すファイル名に基づいて、CLI アプリケーションを PATH
にインストールするためのスクリプトを自動生成します。
多くのパッケージマネージャーに配布する#
Rust アプリケーションのクラシックな地域描画。あなたの言う通りですが、Rust は Mozilla によって独自に開発された全く新しいメモリ安全なプログラミング言語です。コンパイルは「カール構」と呼ばれるビルドシステムで行われ、ここで参照されるポインタには「ライフサイクル」が付与され、安全性が導かれます。あなたは「開発者」と呼ばれる神秘的な役割を果たし、プログラミングの戦いの中で骨の驚くべきエラーに出会い、それを回避しながらコンパイルを進め、「Rust」の真実を徐々に発見していきます。
実際、私はパッケージマネージャーを使用して配布されたインストールスクリプトを実行することにはあまり違いがないと考えています。単にパッケージマネージャーは、"インストールするものを取得する" と "ローカルインストールスクリプトを実行する" のロジックを内部実装として作成し、パッケージのリストとリソースを維持して、労力を節約し、簡単に失敗しないようにします。もちろん、パッケージマネージャーはバージョンや依存関係の統一管理など、さらに多くのことを行うことができます。
それ以外にも、nix(あまり知らない)、tea(Web3 を組み合わせた?)などのパッケージ管理ソリューションがあります。この記事の目的はこれらのものを紹介することではなく、これらのセクションは単にいくつかのソリューションについて軽く話すだけです。
package.json の optionalDependencies を利用する#
Node のいくつかのクロスプラットフォーム機能のおかげで、npm に配布することを考えました。現在のフロントエンドツールチェーンに精通している場合、たとえば esbuild や swc のようなネイティブ言語で開発されたアプリケーションは、npm を通じて配布できます。
esbuild の例を挙げると、興味があるあなたはその npm ホームページを開いてみてください:
あなたは、さまざまなプラットフォームを含む多くの依存関係があることに気づくでしょう。さらに package.json
を開くと、これらは実際には optionalDependencies
であることがわかります:
{
"name": "esbuild",
"version": "0.19.5",
"description": "An extremely fast JavaScript and CSS bundler and minifier.",
"repository": "https://github.com/evanw/esbuild",
"scripts": {
"postinstall": "node install.js"
},
"main": "lib/main.js",
"types": "lib/main.d.ts",
"engines": {
"node": ">=12"
},
"bin": {
"esbuild": "bin/esbuild"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.19.5",
"@esbuild/android-arm64": "0.19.5",
"@esbuild/android-x64": "0.19.5",
"@esbuild/darwin-arm64": "0.19.5",
"@esbuild/darwin-x64": "0.19.5",
"@esbuild/freebsd-arm64": "0.19.5",
"@esbuild/freebsd-x64": "0.19.5",
"@esbuild/linux-arm": "0.19.5",
"@esbuild/linux-arm64": "0.19.5",
"@esbuild/linux-ia32": "0.19.5",
"@esbuild/linux-loong64": "0.19.5",
"@esbuild/linux-mips64el": "0.19.5",
"@esbuild/linux-ppc64": "0.19.5",
"@esbuild/linux-riscv64": "0.19.5",
"@esbuild/linux-s390x": "0.19.5",
"@esbuild/linux-x64": "0.19.5",
"@esbuild/netbsd-x64": "0.19.5",
"@esbuild/openbsd-x64": "0.19.5",
"@esbuild/sunos-x64": "0.19.5",
"@esbuild/win32-arm64": "0.19.5",
"@esbuild/win32-ia32": "0.19.5",
"@esbuild/win32-x64": "0.19.5"
},
"license": "MIT"
}
次に、別のパッケージを開くと、各 @esbuild/*
には実際には対応するプラットフォームの esbuild
バイナリファイルと package.json
しかないことがわかります。たとえば @esbuild/win32-x64
の例を挙げると、この package.json
は os
と cpu
の 2 つのフィールドを使用して、このパッケージがサポートするプラットフォームを指定しています。
{
"name": "@esbuild/win32-x64",
"version": "0.19.5",
"description": "The Windows 64-bit binary for esbuild, a JavaScript bundler.",
"repository": "https://github.com/evanw/esbuild",
"license": "MIT",
"preferUnplugged": true,
"engines": {
"node": ">=12"
},
"os": [
"win32"
],
"cpu": [
"x64"
]
}
optionalDependencies
のインストールロジックは、現在の process.platform
と process.arch
を取得して、各パッケージがそのプラットフォームに一致するかどうかを判断します。npm
は対応するプラットフォームのパッケージのみをダウンロードします(他のものを強制的にダウンロードさせるためのパラメータを渡すこともできます。試してみて、プロジェクト内の swc
と esbuild
がどのプラットフォームのバージョンを使用しているかを確認できます)。
さらに、esbuild
のパッケージ内の scripts
には postinstall
があり、これはスクリプトとして npm run
できるだけでなく、npm install
の後に自動的に実行されます(他のパッケージマネージャーも同様です)。
@naria2/node#
要するに、同様の設定方法を使用して、agalwood/Motrix からいくつかのビルド済みの aria2 バイナリを盗んで、npm に配布しました。具体的には:packages/binary を参照してください。
他のいくつかのものも同様の方法で npm に配布できると感じています。これにより、将来的には Volta / fnm を使って Node をインストールし、その後 npm を使って他の一般的なツールをダウンロードするだけで済みます。
次に、aria2 のバイナリをダウンロードしたら、いくつかのコードを書いて、現在の process.platform
と process.arch
に基づいて対応するバイナリパッケージを選択する必要があります:
function getPackage() {
const { platform, arch } = process;
switch (platform) {
case 'win32':
if (['x64', 'ia32'].includes(arch)) {
return `@naria2/win32-${arch}`;
}
case 'darwin':
if (['x64', 'arm64'].includes(arch)) {
return `@naria2/darwin-${arch}`;
}
case 'linux':
if (['x64', 'arm64'].includes(arch)) {
return `@naria2/linux-${arch}`;
}
}
throw new Error('naria2 はあなたのプラットフォームの aria2 バイナリを提供していません');
}
export const BINARY = getPackage();
export function getBinary() {
const pkg = require.resolve(BINARY + '/package.json');
const { platform } = process;
// Windows は .exe で終わるべき
const binary = path.join(path.dirname(pkg), platform === 'win32' ? 'aria2c.exe' : 'aria2c');
return binary;
}
最後に、getBinary()
を呼び出すだけで、現在のプラットフォームの aria2
バイナリの絶対パスを見つけることができます。
Node.js のコードから起動する#
上記の努力の結果、私たちは aria2 のバイナリを esbuild や swc のような方法で node_modules
にダウンロードしました。もちろん、これだけでは不十分で、次は "いくつかのシンプルな非同期関数を呼び出して、クライアントの初期化を完了する" という努力を続けます。
maria2 は使用される基本的な aria2 RPC インターフェースクライアントで、その抽象化の方法は私にインスピレーションを与えました。
- WebSocket 接続を使用:
import { open, aria2 } from 'maria2'
const conn = await open(
new WebSocket('ws://localhost:6800/jsonrpc')
// import { createWebSocket } from 'maria2/transport'
// createWebSocket('ws://localhost:6800/jsonrpc')
)
const version = await aria2.getVersion(conn)
- HTTP 接続を使用
import { open, aria2 } from 'maria2'
import { createHTTP } from 'maria2/transport'
const conn = await open(
createHTTP('http://localhost:6800/jsonrpc')
)
const version = await aria2.getVersion(conn)
私たちは、ローカルで起動した aria2
プロセスを、WebSocket 接続とともにラップしたものを作成できます。元々の WebSocket 接続の確立を、最初に node_modules
から aria2 プロセスを起動し、その後 WebSocket 接続を作成するという形に変えます。
export async function createChildProcess(): Promise<ChildProcessSocket> {
const child = spawn(, ['--enable-rpc']);
await new Promise((res, rej) => {
let spawn = false;
if (child.stdout) {
child.stdout.once('data', () => {
spawn = true;
res(undefined);
});
} else {
child.once('spawn', () => {
spawn = true;
res(undefined);
});
}
child.once('error', (e) => {
if (!spawn) {
rej(e);
}
});
});
return new WebSocket(`ws://127.0.0.1:6800/jsonrpc`);
}
上記のサンプルコードは多くの詳細を省略していますが、要するにこのような意味です。次は、README.md
に記載されている効果です:
npm i naria2 @naria2/node
使用時は、await createClient(createChildProcess())
の一行だけで aria2 プロセスの作成と WebSocket 接続の確立が完了し、同様に client.close()
には aria2
プロセスを閉じるロジックも含まれています。
import { createClient } from 'naria2'
import { createChildProcess } from '@naria2/node'
// クライアントを初期化
const client = await createClient(createChildProcess())
// マグネットのダウンロードを開始
const torrent = await client.downloadUri('...')
// トレントの進捗を監視
await torrent.watchFollowedBy((torrent) => {
console.log(`Downloading ${torrent.name}`)
})
// クライアントをシャットダウン
await client.shutdown()
もちろん、無視できない詳細もあるかもしれません。たとえば、ここでこのように起動した
aria2
プロセスは、スクリプトが異常終了した後も生存し続ける可能性があるため、Node.js コマンドラインプログラムの『死亡』 を参考にして、こうした境界状況の処理を行う必要があるかもしれません。
CLI アプリケーションとして起動#
私たちはすでに現在のプラットフォームの aria2
バイナリを持っており、aria2
はもともと CLI ダウンロードツールであるため、上記の方法を使ってクロスプラットフォームの CLI ダウンロードツールに変身させることができます。
したがって、Windows / Mac OS / Linux で、Node の環境があれば、他のパッケージと同様に npm i -g
で直接インストールできます。
npm i -g naria2c
最初は、非常にシンプルなことを行い、実際の aria2
パスを取得し、execa
を使用して実行し、良い名前を付けて naria2c
と呼びました。
#!/usr/bin/env node
import { execa } from 'execa'
import { getBinary } from '@naria2/node'
const binary = getBinary()
const childProcess = await execa(binary, process.argv.slice(2))
その後、さらに多くの詳細を追加しました。
naria2c の出力を変換する#
ラッピングを行ったため、現在実行されているのは表面的には aria2c
ではありませんが、naria2c --help
を使用すると、プログラムを実行するために aria2c
を使用するように指示されるため、ユーザーに混乱を引き起こすことになります。
したがって、aria2c
の出力ストリームとエラーストリームを変換し、aria2c
を naria2c
に変換する必要があります。
import { Transform } from 'stream'
childProcess.stdout.pipe(transformOutput()).pipe(process.stdout)
childProcess.stderr.pipe(transformOutput()).pipe(process.stderr)
function transformOutput() {
return new Transform({
transform(chunk, encoding, callback) {
let text = chunk.toString()
text = text.replace(/aria2c/g, 'naria2c')
callback(null, text)
}
});
}
ただし、これにより、Production Replacement | Vite のように、誤った置換が発生する可能性があります。
したがって、naria2c
の実際の選択肢は:
- 実行中に
-h, --help
または-v, --version
を確認し、ヘルプ情報やバージョン情報のコマンド出力内のaria2c
をフィルタリングすることができます; - 標準エラーストリームはヘルプ情報を含む可能性があるため、上記の変換ストリームを使用します(ただし、置換文字列の形式はより具体的にします)。
見た目はまるで自分が書いたかのように見えます(
aria2 プロセスの終了信号を透過する#
Node.js コマンドラインプログラムの『死亡』 に基づいて、いくつかの終了信号を aria2
プロセスに透過しました。
import { onDeath } from '@breadc/death';
const cancelDeath = onDeath(async (signal) => {
const killChildProcess = () => {
childProcess.kill(signal);
return new Promise((res) => {
if (childProcess.exitCode !== null || childProcess.killed) {
res();
} else {
childProcess.once('exit', () => {
res();
});
}
});
};
await Promise.race([
killChildProcess(),
sleep(5000);
]);
});
ただし、Windows プラットフォームと他のプラットフォームの信号メカニズムは異なるため、この部分は元の aria2c
の終了動作を模倣する方法がわからないようです。期待されるのは、Ctrl-C
を押すと aria2
が終了し、いくつかの要約情報を出力するか、aria2
の穏やかな終了をトリガーし、再度強制終了することです。
しかし、まあ、使えないわけではありません(
aria2 が自動的にプロキシ環境変数を使用するのを禁止する#
空港には BitTorrent に関連する監査ルールがあるため、aria2c
は自動的に環境変数の http_proxy
などに基づいてプロキシを設定します。うっかりプロキシを忘れてしまうと、空港の監査を引き起こす可能性があります。
私たちは手動で aria2c
プロセスを開くため、開始前にいくつかの前処理を行い、プロキシ関連の環境変数をクリアすることができます。
まず、新しいオプション -I, --ignore-proxy
を追加して、プロキシ関連の環境変数をクリアします。その後、プロセスを作成する際に、フィルタリングされた process.env
を渡します。
// コマンドライン引数の解析を省略
const env = options.ignoreProxy
? {
...process.env,
HTTP_PROXY: undefined,
HTTPS_PROXY: undefined,
ALL_PROXY: undefined,
http_proxy: undefined,
https_proxy: undefined,
all_proxy: undefined,
no_proxy: undefined
}
: process.env;
Web UI を起動するコマンドライン引数を追加する#
もしあなたが curl
などの CLI ツールのように、ただトレントをダウンロードしたいだけであれば、現在の方法で十分ですが、複数のトレントを操作したり、それらのリアルタイムの状態を監視したりするには、TUI の形式は明らかに便利ではありません。私たちはaria2 を操作するための GUI が欲しいと思っています。
したがって、私たちは naria2c
に Web UI に関連するパラメータ --ui
を追加し、aria2c
プロセスを起動する際に Web UI を起動します。
naria2c --ui
現在、Web UI はいくつかの簡単な視覚化と制御を実装しただけです。
naria2c --ui
の一つのコマンドで、aria2c
と Web UI の起動が完了します。しかし、これにはいくつかの詳細が関与しています:
--ui
パラメータはaria2c
がサポートしているものではなく、起動スクリプト内でフィルタリングして抽出する必要があります;- Web UI を開くには必ず RPC サービスを起動する必要があるため、
--ui
オプションが有効であることがわかれば、同時に--enable-rpc
を設定する必要があります; - Web UI は RPC サービスのポート番号と認証キーを知る必要がありますが、明らかにこの情報は繰り返し指定する必要はなく、
--rpc-secret
オプションを手動で解析するだけで済みます。
// aria2c のパラメータ
const args: string[] = []
// 欠落しているパラメータ
const missing = {
enableRpc: true
}
// Web UI に関連するパラメータ
const webui = {
enable: false,
secret: undefined
}
for (const arg of process.argv.slice(2)) {
// コマンドライン引数の解析を省略
}
// Web UI を起動し、RPC が起動していない場合はパラメータを補完
if (webui.enable && missing.enableRpc) {
args.push('--enable-rpc')
}
これにより、コマンドライン引数から必要な情報を取得でき、次に埋め込まれた Web UI を起動できます。
埋め込まれた Web UI#
前の小節に続いて、なぜ @naria2/node
に Web UI を埋め込む必要があるのでしょうか?
最初の頃を振り返ると:
naria2
は実際にはクライアントライブラリであり、ブラウザや Node などの環境で実行可能です;@naria2/node
は、ローカルで aria2 プロセスを直接開くなど、Node に関連するローカル実行サポートを提供します;naria2c
はaria2c
のクロスプラットフォームラッパーであり、単なる追加の産物です。
したがって、私たちはクライアントに多くの能力を提供することを優先的に考えるべきです:Web UI を起動することは、@naria2/node
に組み込むことができ、パッケージの能力としてその使用者に提供できます。
さらに、electron のような技術を使用してデスクトップ GUI アプリケーションを作成するのではなく(たとえば agalwood/Motrix)。第二に、デスクトップアプリケーションを作成することは、上記のようなパッケージのニーズを満たすことができず、多くの追加のパッケージ配布コストを追加する必要があります。aria2c
のバイナリをダウンロードするのと同じように。
実際、これが私の真のニーズでもあります:AnimeSpace、自動的に新しいアニメをダウンロードし、整理するソリューションです。AnimeSpace はコマンドラインプログラムであり、購読状況に基づいてアニメリソースのトレントを自動的にダウンロードします。現在のバージョンでは、ダウンロード時に進捗バーの TUI しか表示されず、非常に直感的ではありません。@naria2/node
のこの機能を導入することで、より豊富なダウンロード進捗 GUI を提供できます。
Web UI を起動する#
では、どのようにして Web UI を @naria2/node
に埋め込むのでしょうか?
実際には、フロントエンドアプリケーションのビルド成果物を、このパッケージのどこかのディレクトリにコピーするだけです(そして package.json
の files
項目を関連する設定に変更します)。その後、実行時にフロントエンドアプリケーションのビルド成果物のディレクトリを見つけ、その上に静的ファイルの HTTP サーバーを作成すればよいのです。
@naria2/node
に対応するパッケージ構造は以下のようになります:
@naria2/node
├─ client/ # フロントエンドアプリケーションのビルド成果物
│ ├─ assets/**
│ └─ index.html
├─ dist/** # TypeScript ソースのトランスパイル成果物
├─ ...
└─ package.json
潜在的な問題:
- パスは TypeScript トランスパイルビルド成果物のパスに依存します;
single-executable-applications
やインライン依存のパッケージ方式をサポートできません。なぜなら、これらの非コードの静的成果物がファイルシステム内の相対位置に依存しているからです。しかし、問題はそれほど大きくありません。
したがって、fileURLToPath(new URL('../client', import.meta.url))
の方法で Web UI 成果物のパスを見つけ、serve-static
を使用してローカルファイルの HTTP サーバーを起動します。
最終的に、ユーザーは直接 await launchWebUI({ ... })
を呼び出して Web UI を起動できるようになります。
export async function launchWebUI(options: WebUIOptions) {
const serveStatic = (await import('serve-static')).default;
const finalhandler = (await import('finalhandler')).default;
const http = await import('http');
const port = options.port ?? 6801;
const clientDir = fileURLToPath(new URL('../client', import.meta.url));
const serve = serveStatic(clientDir, { index: ['index.html'] });
const server = http.createServer(async (req, res) => {
serve(req, res, finalhandler(req, res));
});
server.listen(port);
return server;
}
export interface WebUIOptions {
port?: number;
rpc: {
port: number;
secret: string | undefined;
};
}
vite-plugin-naria2#
以前は React を書いたことがありませんでしたが、オンラインで多くの情報を得て、今回はこの技術を試してみることにしました。ここでは SSR は必要ないので、直接 Vite を使用し、さらに多くの React エコシステムのもの、react-router、TanStack Query、Zustand、tailwindcss、shadcn など、さらに多くの shadcn に依存するものを追加しました。最初は非常に複雑に見えましたが、特に shadcn が出してくる多くのコンポーネントコードを見て、頭が痛くなりましたが、実際には書くのは非常に便利で、デフォルトの機能も非常に強力です。
このアプリケーションの開発の最初の問題は、フロントエンドで使用するために aria2 サービスを起動する必要があることです。したがって、以前に書いたプラグイン vite-plugin-cloudflare-functions の考え方を参考にしました。
Vite プラグイン内で aria2 を起動し、仮想モジュールを介して接続情報をフロントエンドに渡す。これにより、別のサービスを起動したり、別途設定したりする必要がなくなります。
export default function Naria2(): Plugin[] {
const childProcessRuntime = {
process: undefined as ChildProcessSocket | undefined,
url: undefined as string | undefined,
secret: undefined as string | undefined
};
return [
{
name: 'vite-plugin-naria2:runtime',
apply: 'serve',
async configureServer(server) {
if (!childProcessRuntime.url) {
// Vite dev サーバー内で aria2 を起動
const childProcess = await createChildProcess();
childProcessRuntime.process = childProcess;
childProcessRuntime.url = `ws://127.0.0.1:${childProcess.getOptions().listenPort}/jsonrpc`;
childProcessRuntime.secret = childProcess.getOptions().secret;
}
},
closeBundle() {
childProcessRuntime.process?.close?.();
childProcessRuntime.url = undefined;
childProcessRuntime.secret = undefined;
}
},
{
name: 'vite-plugin-naria2:build',
resolveId(id) {
if (id === '~naria2/jsonrpc') {
return '\0' + id;
}
},
load(id) {
if (id === '\0~naria2/jsonrpc') {
// 仮想モジュールを介して情報をフロントエンドに渡す
const socketCode = childProcessRuntime.url
? `new WebSocket(${JSON.stringify(childProcessRuntime.url)})`
: 'undefined';
const clientCode = `socket ? await createClient(socket, { ${
childProcessRuntime.secret
? 'secret: ' + JSON.stringify(childProcessRuntime.secret)
: ''
} }) : undefined`;
return [
`import { createClient } from 'naria2';`,
`export const socket = ${socketCode};`,
`export const client = ${clientCode};`
].join('\n');
}
}
}
];
}
ローカルファイルエクスプローラーを開く#
私たちはこの Web UI に、一般的な機能を追加したいと思っています。たとえば、ローカルファイルエクスプローラーを開くことです。
しかし、ブラウザのセキュリティ制限により、実際にはこれを直接行うのはあまり便利ではありません。
しかし、私たちは実際にバックエンドサービスを起動しているため、このサービスを使用して開けばいいのです。
export async function launchWebUI(options: WebUIOptions) {
// ...
const handler = await createWebUIHandler(options);
const server = http.createServer(async (req, res) => {
if (await handler(req, res)) {
return;
}
serve(req, res, finalhandler(req, res));
});
// ...
}
export async function createWebUIHandler(options: Pick<WebUIOptions, 'rpc'>) {
return async (req: IncomingMessage, res: ServerResponse<IncomingMessage>) => {
if (!req.url) return false;
try {
const url = new URL(req.url, `http://${req.headers.host}`);
if (url.pathname === '/_/open') {
return await handleWebUIOpenRequest(url, req, res);
}
return false;
} catch (error) {
return false;
}
};
}
export async function handleWebUIOpenRequest(
url: URL,
req: IncomingMessage,
res: ServerResponse<IncomingMessage>
) {
try {
const auth = req.headers.authorization;
const dir = url.searchParams.get('dir');
if (dir) {
const open = (await import('open')).default;
await open(dir).catch(() => {});
res.write(JSON.stringify({ status: 'OK', open: p }));
}
res.setHeader('Content-Type', 'application/json');
res.end();
return true;
} catch (error) {
return false;
}
}
私たちは元々の静的ファイル HTTP サービスに、ファイルを開くためのインターフェース /_/open?dir=...
を追加しました。フロントエンドがこのインターフェースにリクエストを送信すると、ファイルエクスプローラーが開く効果が得られます。
セキュリティ上の問題がある可能性があるため、実際にはトークンを追加で検証するか、この機能を無効にするオプションを使用することができます。
aria2 RPC ポートをプロキシする#
あまり必要ではありませんが、このページをサーバーにデプロイすることも考えられます。そのためには、インターネット上で 2 つのポートを開く必要があります。1 つはフロントエンドアプリケーション用、もう 1 つは aria2 RPC サービス用です。追加の設定が必要になるか、nginx などを使用して RPC サービスの位置を発見する必要があります。
前のセクションと同様に、私たちはすでにバックエンドサービスを持っているため、このサービスを最大限に活用しない手はありません。
直接このサービスの /jsonrpc
ルートに、aria2 RPC サービスへのプロキシを追加します。したがって、デプロイする際にはアプリケーションの位置を公開するだけで済み、RPC サービスに接続するには同じオリジンの /jsonrpc
にアクセスするだけで済みます。
export async function createWebUIHandler(options: Pick<WebUIOptions, 'rpc'>) {
// プロキシミドルウェアを作成
const { createProxyMiddleware } = await import('http-proxy-middleware');
const proxyMiddleware = createProxyMiddleware({
target: `http://127.0.0.1:${options.rpc.port}`,
changeOrigin: false,
ws: true,
logLevel: 'silent'
});
return async (req: IncomingMessage, res: ServerResponse<IncomingMessage>) => {
if (!req.url) return false;
try {
const url = new URL(req.url, `http://${req.headers.host}`);
if (url.pathname === '/jsonrpc') {
// リクエストを転送
proxyMiddleware(req as any, res as any, () => {});
return true;
} else if (url.pathname === '/_/open') {
return await handleWebUIOpenRequest(url, req, res);
}
return false;
} catch (error) {
return false;
}
};
}
http-proxy-middleware に基づいて、HTTP リクエストのプロキシ転送作業を完了します。このライブラリは WebSocket リクエストの転送もサポートしており、直接使用できます。