Simple mini clone of torrra
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
export GIT_CONFIG_GLOBAL=/dev/null
|
||||
|
||||
eval "$(devenv direnvrc)"
|
||||
|
||||
# You can pass flags to the devenv command
|
||||
# For example: use devenv --impure --option services.postgres.enable:bool true
|
||||
use devenv
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
# Devenv
|
||||
.devenv*
|
||||
devenv.local.nix
|
||||
devenv.local.yaml
|
||||
|
||||
# direnv
|
||||
.direnv
|
||||
|
||||
# pre-commit
|
||||
.pre-commit-config.yaml
|
||||
Generated
+3888
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["crates/torad", "crates/tora", "crates/proto"]
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "tora-proto"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
pbjson = "0.9.0"
|
||||
prost = "0.14.4"
|
||||
serde = "1.0.228"
|
||||
tonic = "0.14.6"
|
||||
tonic-prost = "0.14.6"
|
||||
@@ -0,0 +1,306 @@
|
||||
// @generated
|
||||
// This file is @generated by prost-build.
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct AddRequest {
|
||||
#[prost(string, tag = "1")]
|
||||
pub magnet: ::prost::alloc::string::String,
|
||||
/// Optional output directory override. Empty means use torad's default.
|
||||
#[prost(string, tag = "2")]
|
||||
pub output_dir: ::prost::alloc::string::String,
|
||||
}
|
||||
impl ::prost::Name for AddRequest {
|
||||
const NAME: &'static str = "AddRequest";
|
||||
const PACKAGE: &'static str = "torrent";
|
||||
fn full_name() -> ::prost::alloc::string::String {
|
||||
"torrent.AddRequest".into()
|
||||
}
|
||||
fn type_url() -> ::prost::alloc::string::String {
|
||||
"/torrent.AddRequest".into()
|
||||
}
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct AddResponse {
|
||||
#[prost(string, tag = "1")]
|
||||
pub id: ::prost::alloc::string::String,
|
||||
}
|
||||
impl ::prost::Name for AddResponse {
|
||||
const NAME: &'static str = "AddResponse";
|
||||
const PACKAGE: &'static str = "torrent";
|
||||
fn full_name() -> ::prost::alloc::string::String {
|
||||
"torrent.AddResponse".into()
|
||||
}
|
||||
fn type_url() -> ::prost::alloc::string::String {
|
||||
"/torrent.AddResponse".into()
|
||||
}
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct StatusRequest {
|
||||
#[prost(string, tag = "1")]
|
||||
pub id: ::prost::alloc::string::String,
|
||||
}
|
||||
impl ::prost::Name for StatusRequest {
|
||||
const NAME: &'static str = "StatusRequest";
|
||||
const PACKAGE: &'static str = "torrent";
|
||||
fn full_name() -> ::prost::alloc::string::String {
|
||||
"torrent.StatusRequest".into()
|
||||
}
|
||||
fn type_url() -> ::prost::alloc::string::String {
|
||||
"/torrent.StatusRequest".into()
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct ListRequest {}
|
||||
impl ::prost::Name for ListRequest {
|
||||
const NAME: &'static str = "ListRequest";
|
||||
const PACKAGE: &'static str = "torrent";
|
||||
fn full_name() -> ::prost::alloc::string::String {
|
||||
"torrent.ListRequest".into()
|
||||
}
|
||||
fn type_url() -> ::prost::alloc::string::String {
|
||||
"/torrent.ListRequest".into()
|
||||
}
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ListResponse {
|
||||
#[prost(message, repeated, tag = "1")]
|
||||
pub torrents: ::prost::alloc::vec::Vec<TorrentStatus>,
|
||||
}
|
||||
impl ::prost::Name for ListResponse {
|
||||
const NAME: &'static str = "ListResponse";
|
||||
const PACKAGE: &'static str = "torrent";
|
||||
fn full_name() -> ::prost::alloc::string::String {
|
||||
"torrent.ListResponse".into()
|
||||
}
|
||||
fn type_url() -> ::prost::alloc::string::String {
|
||||
"/torrent.ListResponse".into()
|
||||
}
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct TorrentStatus {
|
||||
#[prost(string, tag = "1")]
|
||||
pub id: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "2")]
|
||||
pub info_hash: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "3")]
|
||||
pub name: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "4")]
|
||||
pub source: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "5")]
|
||||
pub output_path: ::prost::alloc::string::String,
|
||||
#[prost(uint64, tag = "6")]
|
||||
pub total_bytes: u64,
|
||||
#[prost(uint64, tag = "7")]
|
||||
pub downloaded_bytes: u64,
|
||||
#[prost(enumeration = "State", tag = "8")]
|
||||
pub state: i32,
|
||||
#[prost(string, tag = "9")]
|
||||
pub error_message: ::prost::alloc::string::String,
|
||||
}
|
||||
impl ::prost::Name for TorrentStatus {
|
||||
const NAME: &'static str = "TorrentStatus";
|
||||
const PACKAGE: &'static str = "torrent";
|
||||
fn full_name() -> ::prost::alloc::string::String {
|
||||
"torrent.TorrentStatus".into()
|
||||
}
|
||||
fn type_url() -> ::prost::alloc::string::String {
|
||||
"/torrent.TorrentStatus".into()
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||
#[repr(i32)]
|
||||
pub enum State {
|
||||
Unspecified = 0,
|
||||
Pending = 1,
|
||||
Downloading = 2,
|
||||
Paused = 3,
|
||||
Finished = 4,
|
||||
Error = 5,
|
||||
}
|
||||
impl State {
|
||||
/// String value of the enum field names used in the ProtoBuf definition.
|
||||
///
|
||||
/// The values are not transformed in any way and thus are considered stable
|
||||
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
|
||||
pub fn as_str_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Unspecified => "STATE_UNSPECIFIED",
|
||||
Self::Pending => "PENDING",
|
||||
Self::Downloading => "DOWNLOADING",
|
||||
Self::Paused => "PAUSED",
|
||||
Self::Finished => "FINISHED",
|
||||
Self::Error => "ERROR",
|
||||
}
|
||||
}
|
||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||
match value {
|
||||
"STATE_UNSPECIFIED" => Some(Self::Unspecified),
|
||||
"PENDING" => Some(Self::Pending),
|
||||
"DOWNLOADING" => Some(Self::Downloading),
|
||||
"PAUSED" => Some(Self::Paused),
|
||||
"FINISHED" => Some(Self::Finished),
|
||||
"ERROR" => Some(Self::Error),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Encoded file descriptor set for the `torrent` package
|
||||
pub const FILE_DESCRIPTOR_SET: &[u8] = &[
|
||||
0x0a, 0x94, 0x13, 0x0a, 0x0d, 0x74, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x12, 0x07, 0x74, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x22, 0x43, 0x0a, 0x0a, 0x41,
|
||||
0x64, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x61, 0x67,
|
||||
0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x61, 0x67, 0x6e, 0x65,
|
||||
0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x64, 0x69, 0x72, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x44, 0x69, 0x72,
|
||||
0x22, 0x1d, 0x0a, 0x0b, 0x41, 0x64, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22,
|
||||
0x1f, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64,
|
||||
0x22, 0x0d, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22,
|
||||
0x42, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||
0x32, 0x0a, 0x08, 0x74, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
|
||||
0x0b, 0x32, 0x16, 0x2e, 0x74, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x2e, 0x54, 0x6f, 0x72, 0x72,
|
||||
0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x08, 0x74, 0x6f, 0x72, 0x72, 0x65,
|
||||
0x6e, 0x74, 0x73, 0x22, 0xa0, 0x02, 0x0a, 0x0d, 0x54, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x53,
|
||||
0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x6e, 0x66, 0x6f, 0x5f, 0x68, 0x61,
|
||||
0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x69, 0x6e, 0x66, 0x6f, 0x48, 0x61,
|
||||
0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
||||
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1f,
|
||||
0x0a, 0x0b, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x0a, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x50, 0x61, 0x74, 0x68, 0x12,
|
||||
0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x06,
|
||||
0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73,
|
||||
0x12, 0x29, 0x0a, 0x10, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x5f, 0x62,
|
||||
0x79, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x64, 0x6f, 0x77, 0x6e,
|
||||
0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x24, 0x0a, 0x05, 0x73,
|
||||
0x74, 0x61, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x74, 0x6f, 0x72,
|
||||
0x72, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74,
|
||||
0x65, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61,
|
||||
0x67, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d,
|
||||
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2a, 0x61, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12,
|
||||
0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49,
|
||||
0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e,
|
||||
0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, 0x49,
|
||||
0x4e, 0x47, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x41, 0x55, 0x53, 0x45, 0x44, 0x10, 0x03,
|
||||
0x12, 0x0c, 0x0a, 0x08, 0x46, 0x49, 0x4e, 0x49, 0x53, 0x48, 0x45, 0x44, 0x10, 0x04, 0x12, 0x09,
|
||||
0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x32, 0xab, 0x01, 0x0a, 0x08, 0x54, 0x6f,
|
||||
0x72, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x30, 0x0a, 0x03, 0x41, 0x64, 0x64, 0x12, 0x13, 0x2e,
|
||||
0x74, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x64, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||
0x73, 0x74, 0x1a, 0x14, 0x2e, 0x74, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x64, 0x64,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74,
|
||||
0x75, 0x73, 0x12, 0x16, 0x2e, 0x74, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x61,
|
||||
0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x74, 0x6f, 0x72,
|
||||
0x72, 0x65, 0x6e, 0x74, 0x2e, 0x54, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74,
|
||||
0x75, 0x73, 0x12, 0x33, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x14, 0x2e, 0x74, 0x6f, 0x72,
|
||||
0x72, 0x65, 0x6e, 0x74, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x1a, 0x15, 0x2e, 0x74, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4a, 0xe5, 0x0c, 0x0a, 0x06, 0x12, 0x04, 0x00, 0x00,
|
||||
0x34, 0x01, 0x0a, 0x08, 0x0a, 0x01, 0x0c, 0x12, 0x03, 0x00, 0x00, 0x12, 0x0a, 0x08, 0x0a, 0x01,
|
||||
0x02, 0x12, 0x03, 0x02, 0x00, 0x10, 0x0a, 0x0a, 0x0a, 0x02, 0x06, 0x00, 0x12, 0x04, 0x04, 0x00,
|
||||
0x0b, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x06, 0x00, 0x01, 0x12, 0x03, 0x04, 0x08, 0x10, 0x0a, 0x49,
|
||||
0x0a, 0x04, 0x06, 0x00, 0x02, 0x00, 0x12, 0x03, 0x06, 0x02, 0x2c, 0x1a, 0x3c, 0x20, 0x41, 0x64,
|
||||
0x64, 0x20, 0x61, 0x20, 0x74, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x72, 0x6f, 0x6d,
|
||||
0x20, 0x61, 0x20, 0x6d, 0x61, 0x67, 0x6e, 0x65, 0x74, 0x20, 0x6c, 0x69, 0x6e, 0x6b, 0x20, 0x61,
|
||||
0x6e, 0x64, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x20, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61,
|
||||
0x64, 0x69, 0x6e, 0x67, 0x20, 0x69, 0x74, 0x2e, 0x0a, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02,
|
||||
0x00, 0x01, 0x12, 0x03, 0x06, 0x06, 0x09, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x00, 0x02,
|
||||
0x12, 0x03, 0x06, 0x0a, 0x14, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x00, 0x03, 0x12, 0x03,
|
||||
0x06, 0x1f, 0x2a, 0x0a, 0x3a, 0x0a, 0x04, 0x06, 0x00, 0x02, 0x01, 0x12, 0x03, 0x08, 0x02, 0x34,
|
||||
0x1a, 0x2d, 0x20, 0x47, 0x65, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65,
|
||||
0x6e, 0x74, 0x20, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x20, 0x6f, 0x66, 0x20, 0x61, 0x20, 0x73,
|
||||
0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x2e, 0x0a, 0x0a,
|
||||
0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x01, 0x01, 0x12, 0x03, 0x08, 0x06, 0x0c, 0x0a, 0x0c, 0x0a,
|
||||
0x05, 0x06, 0x00, 0x02, 0x01, 0x02, 0x12, 0x03, 0x08, 0x0d, 0x1a, 0x0a, 0x0c, 0x0a, 0x05, 0x06,
|
||||
0x00, 0x02, 0x01, 0x03, 0x12, 0x03, 0x08, 0x25, 0x32, 0x0a, 0x35, 0x0a, 0x04, 0x06, 0x00, 0x02,
|
||||
0x02, 0x12, 0x03, 0x0a, 0x02, 0x2f, 0x1a, 0x28, 0x20, 0x4c, 0x69, 0x73, 0x74, 0x20, 0x61, 0x6c,
|
||||
0x6c, 0x20, 0x74, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x20, 0x6b, 0x6e, 0x6f, 0x77, 0x6e,
|
||||
0x20, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x0a,
|
||||
0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x02, 0x01, 0x12, 0x03, 0x0a, 0x06, 0x0a, 0x0a, 0x0c,
|
||||
0x0a, 0x05, 0x06, 0x00, 0x02, 0x02, 0x02, 0x12, 0x03, 0x0a, 0x0b, 0x16, 0x0a, 0x0c, 0x0a, 0x05,
|
||||
0x06, 0x00, 0x02, 0x02, 0x03, 0x12, 0x03, 0x0a, 0x21, 0x2d, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x00,
|
||||
0x12, 0x04, 0x0d, 0x00, 0x11, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x00, 0x01, 0x12, 0x03, 0x0d,
|
||||
0x08, 0x12, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x00, 0x05, 0x12, 0x03, 0x0e, 0x02, 0x08,
|
||||
0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x00, 0x02, 0x00, 0x12, 0x03, 0x0e, 0x02, 0x14, 0x0a, 0x0c, 0x0a,
|
||||
0x05, 0x04, 0x00, 0x02, 0x00, 0x01, 0x12, 0x03, 0x0e, 0x09, 0x0f, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
|
||||
0x00, 0x02, 0x00, 0x03, 0x12, 0x03, 0x0e, 0x12, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02,
|
||||
0x01, 0x05, 0x12, 0x03, 0x10, 0x02, 0x08, 0x0a, 0x53, 0x0a, 0x04, 0x04, 0x00, 0x02, 0x01, 0x12,
|
||||
0x03, 0x10, 0x02, 0x18, 0x1a, 0x46, 0x20, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x20,
|
||||
0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x20, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79,
|
||||
0x20, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x2e, 0x20, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
||||
0x20, 0x6d, 0x65, 0x61, 0x6e, 0x73, 0x20, 0x75, 0x73, 0x65, 0x20, 0x74, 0x6f, 0x72, 0x61, 0x64,
|
||||
0x27, 0x73, 0x20, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x2e, 0x0a, 0x0a, 0x0c, 0x0a, 0x05,
|
||||
0x04, 0x00, 0x02, 0x01, 0x01, 0x12, 0x03, 0x10, 0x09, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00,
|
||||
0x02, 0x01, 0x03, 0x12, 0x03, 0x10, 0x16, 0x17, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x01, 0x12, 0x04,
|
||||
0x13, 0x00, 0x15, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x01, 0x01, 0x12, 0x03, 0x13, 0x08, 0x13,
|
||||
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x01, 0x02, 0x00, 0x05, 0x12, 0x03, 0x14, 0x02, 0x08, 0x0a, 0x0b,
|
||||
0x0a, 0x04, 0x04, 0x01, 0x02, 0x00, 0x12, 0x03, 0x14, 0x02, 0x10, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
|
||||
0x01, 0x02, 0x00, 0x01, 0x12, 0x03, 0x14, 0x09, 0x0b, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x01, 0x02,
|
||||
0x00, 0x03, 0x12, 0x03, 0x14, 0x0e, 0x0f, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x02, 0x12, 0x04, 0x17,
|
||||
0x00, 0x19, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x02, 0x01, 0x12, 0x03, 0x17, 0x08, 0x15, 0x0a,
|
||||
0x0c, 0x0a, 0x05, 0x04, 0x02, 0x02, 0x00, 0x05, 0x12, 0x03, 0x18, 0x02, 0x08, 0x0a, 0x0b, 0x0a,
|
||||
0x04, 0x04, 0x02, 0x02, 0x00, 0x12, 0x03, 0x18, 0x02, 0x10, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x02,
|
||||
0x02, 0x00, 0x01, 0x12, 0x03, 0x18, 0x09, 0x0b, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x02, 0x02, 0x00,
|
||||
0x03, 0x12, 0x03, 0x18, 0x0e, 0x0f, 0x0a, 0x09, 0x0a, 0x02, 0x04, 0x03, 0x12, 0x03, 0x1b, 0x00,
|
||||
0x16, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x03, 0x01, 0x12, 0x03, 0x1b, 0x08, 0x13, 0x0a, 0x0a, 0x0a,
|
||||
0x02, 0x04, 0x04, 0x12, 0x04, 0x1d, 0x00, 0x1f, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x04, 0x01,
|
||||
0x12, 0x03, 0x1d, 0x08, 0x14, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x04, 0x02, 0x00, 0x04, 0x12, 0x03,
|
||||
0x1e, 0x02, 0x0a, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x04, 0x02, 0x00, 0x12, 0x03, 0x1e, 0x02, 0x26,
|
||||
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x04, 0x02, 0x00, 0x06, 0x12, 0x03, 0x1e, 0x0b, 0x18, 0x0a, 0x0c,
|
||||
0x0a, 0x05, 0x04, 0x04, 0x02, 0x00, 0x01, 0x12, 0x03, 0x1e, 0x19, 0x21, 0x0a, 0x0c, 0x0a, 0x05,
|
||||
0x04, 0x04, 0x02, 0x00, 0x03, 0x12, 0x03, 0x1e, 0x24, 0x25, 0x0a, 0x0a, 0x0a, 0x02, 0x05, 0x00,
|
||||
0x12, 0x04, 0x21, 0x00, 0x28, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x05, 0x00, 0x01, 0x12, 0x03, 0x21,
|
||||
0x05, 0x0a, 0x0a, 0x0c, 0x0a, 0x05, 0x05, 0x00, 0x02, 0x00, 0x01, 0x12, 0x03, 0x22, 0x02, 0x13,
|
||||
0x0a, 0x0b, 0x0a, 0x04, 0x05, 0x00, 0x02, 0x00, 0x12, 0x03, 0x22, 0x02, 0x18, 0x0a, 0x0c, 0x0a,
|
||||
0x05, 0x05, 0x00, 0x02, 0x00, 0x02, 0x12, 0x03, 0x22, 0x16, 0x17, 0x0a, 0x0c, 0x0a, 0x05, 0x05,
|
||||
0x00, 0x02, 0x01, 0x01, 0x12, 0x03, 0x23, 0x02, 0x09, 0x0a, 0x0b, 0x0a, 0x04, 0x05, 0x00, 0x02,
|
||||
0x01, 0x12, 0x03, 0x23, 0x02, 0x0e, 0x0a, 0x0c, 0x0a, 0x05, 0x05, 0x00, 0x02, 0x01, 0x02, 0x12,
|
||||
0x03, 0x23, 0x0c, 0x0d, 0x0a, 0x0c, 0x0a, 0x05, 0x05, 0x00, 0x02, 0x02, 0x01, 0x12, 0x03, 0x24,
|
||||
0x02, 0x0d, 0x0a, 0x0b, 0x0a, 0x04, 0x05, 0x00, 0x02, 0x02, 0x12, 0x03, 0x24, 0x02, 0x12, 0x0a,
|
||||
0x0c, 0x0a, 0x05, 0x05, 0x00, 0x02, 0x02, 0x02, 0x12, 0x03, 0x24, 0x10, 0x11, 0x0a, 0x0c, 0x0a,
|
||||
0x05, 0x05, 0x00, 0x02, 0x03, 0x01, 0x12, 0x03, 0x25, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x05,
|
||||
0x00, 0x02, 0x03, 0x12, 0x03, 0x25, 0x02, 0x0d, 0x0a, 0x0c, 0x0a, 0x05, 0x05, 0x00, 0x02, 0x03,
|
||||
0x02, 0x12, 0x03, 0x25, 0x0b, 0x0c, 0x0a, 0x0c, 0x0a, 0x05, 0x05, 0x00, 0x02, 0x04, 0x01, 0x12,
|
||||
0x03, 0x26, 0x02, 0x0a, 0x0a, 0x0b, 0x0a, 0x04, 0x05, 0x00, 0x02, 0x04, 0x12, 0x03, 0x26, 0x02,
|
||||
0x0f, 0x0a, 0x0c, 0x0a, 0x05, 0x05, 0x00, 0x02, 0x04, 0x02, 0x12, 0x03, 0x26, 0x0d, 0x0e, 0x0a,
|
||||
0x0c, 0x0a, 0x05, 0x05, 0x00, 0x02, 0x05, 0x01, 0x12, 0x03, 0x27, 0x02, 0x07, 0x0a, 0x0b, 0x0a,
|
||||
0x04, 0x05, 0x00, 0x02, 0x05, 0x12, 0x03, 0x27, 0x02, 0x0c, 0x0a, 0x0c, 0x0a, 0x05, 0x05, 0x00,
|
||||
0x02, 0x05, 0x02, 0x12, 0x03, 0x27, 0x0a, 0x0b, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x05, 0x12, 0x04,
|
||||
0x2a, 0x00, 0x34, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x05, 0x01, 0x12, 0x03, 0x2a, 0x08, 0x15,
|
||||
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x00, 0x05, 0x12, 0x03, 0x2b, 0x02, 0x08, 0x0a, 0x0b,
|
||||
0x0a, 0x04, 0x04, 0x05, 0x02, 0x00, 0x12, 0x03, 0x2b, 0x02, 0x10, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
|
||||
0x05, 0x02, 0x00, 0x01, 0x12, 0x03, 0x2b, 0x09, 0x0b, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02,
|
||||
0x00, 0x03, 0x12, 0x03, 0x2b, 0x0e, 0x0f, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x01, 0x05,
|
||||
0x12, 0x03, 0x2c, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x05, 0x02, 0x01, 0x12, 0x03, 0x2c,
|
||||
0x02, 0x17, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x01, 0x01, 0x12, 0x03, 0x2c, 0x09, 0x12,
|
||||
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x01, 0x03, 0x12, 0x03, 0x2c, 0x15, 0x16, 0x0a, 0x0c,
|
||||
0x0a, 0x05, 0x04, 0x05, 0x02, 0x02, 0x05, 0x12, 0x03, 0x2d, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04,
|
||||
0x04, 0x05, 0x02, 0x02, 0x12, 0x03, 0x2d, 0x02, 0x12, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02,
|
||||
0x02, 0x01, 0x12, 0x03, 0x2d, 0x09, 0x0d, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x02, 0x03,
|
||||
0x12, 0x03, 0x2d, 0x10, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x03, 0x05, 0x12, 0x03,
|
||||
0x2e, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x05, 0x02, 0x03, 0x12, 0x03, 0x2e, 0x02, 0x14,
|
||||
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x03, 0x01, 0x12, 0x03, 0x2e, 0x09, 0x0f, 0x0a, 0x0c,
|
||||
0x0a, 0x05, 0x04, 0x05, 0x02, 0x03, 0x03, 0x12, 0x03, 0x2e, 0x12, 0x13, 0x0a, 0x0c, 0x0a, 0x05,
|
||||
0x04, 0x05, 0x02, 0x04, 0x05, 0x12, 0x03, 0x2f, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x05,
|
||||
0x02, 0x04, 0x12, 0x03, 0x2f, 0x02, 0x19, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x04, 0x01,
|
||||
0x12, 0x03, 0x2f, 0x09, 0x14, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x04, 0x03, 0x12, 0x03,
|
||||
0x2f, 0x17, 0x18, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x05, 0x05, 0x12, 0x03, 0x30, 0x02,
|
||||
0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x05, 0x02, 0x05, 0x12, 0x03, 0x30, 0x02, 0x19, 0x0a, 0x0c,
|
||||
0x0a, 0x05, 0x04, 0x05, 0x02, 0x05, 0x01, 0x12, 0x03, 0x30, 0x09, 0x14, 0x0a, 0x0c, 0x0a, 0x05,
|
||||
0x04, 0x05, 0x02, 0x05, 0x03, 0x12, 0x03, 0x30, 0x17, 0x18, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05,
|
||||
0x02, 0x06, 0x05, 0x12, 0x03, 0x31, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x05, 0x02, 0x06,
|
||||
0x12, 0x03, 0x31, 0x02, 0x1e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x06, 0x01, 0x12, 0x03,
|
||||
0x31, 0x09, 0x19, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x06, 0x03, 0x12, 0x03, 0x31, 0x1c,
|
||||
0x1d, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x07, 0x06, 0x12, 0x03, 0x32, 0x02, 0x07, 0x0a,
|
||||
0x0b, 0x0a, 0x04, 0x04, 0x05, 0x02, 0x07, 0x12, 0x03, 0x32, 0x02, 0x12, 0x0a, 0x0c, 0x0a, 0x05,
|
||||
0x04, 0x05, 0x02, 0x07, 0x01, 0x12, 0x03, 0x32, 0x08, 0x0d, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05,
|
||||
0x02, 0x07, 0x03, 0x12, 0x03, 0x32, 0x10, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x08,
|
||||
0x05, 0x12, 0x03, 0x33, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x05, 0x02, 0x08, 0x12, 0x03,
|
||||
0x33, 0x02, 0x1b, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x08, 0x01, 0x12, 0x03, 0x33, 0x09,
|
||||
0x16, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x08, 0x03, 0x12, 0x03, 0x33, 0x19, 0x1a, 0x62,
|
||||
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
];
|
||||
include!("torrent.serde.rs");
|
||||
include!("torrent.tonic.rs");
|
||||
// @@protoc_insertion_point(module)
|
||||
@@ -0,0 +1,795 @@
|
||||
// @generated
|
||||
impl serde::Serialize for AddRequest {
|
||||
#[allow(deprecated)]
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
let mut len = 0;
|
||||
if !self.magnet.is_empty() {
|
||||
len += 1;
|
||||
}
|
||||
if !self.output_dir.is_empty() {
|
||||
len += 1;
|
||||
}
|
||||
let mut struct_ser = serializer.serialize_struct("torrent.AddRequest", len)?;
|
||||
if !self.magnet.is_empty() {
|
||||
struct_ser.serialize_field("magnet", &self.magnet)?;
|
||||
}
|
||||
if !self.output_dir.is_empty() {
|
||||
struct_ser.serialize_field("outputDir", &self.output_dir)?;
|
||||
}
|
||||
struct_ser.end()
|
||||
}
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for AddRequest {
|
||||
#[allow(deprecated)]
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
const FIELDS: &[&str] = &["magnet", "output_dir", "outputDir"];
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum GeneratedField {
|
||||
Magnet,
|
||||
OutputDir,
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for GeneratedField {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<GeneratedField, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct GeneratedVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
|
||||
type Value = GeneratedField;
|
||||
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter<'_>,
|
||||
) -> std::fmt::Result {
|
||||
write!(formatter, "expected one of: {:?}", &FIELDS)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn visit_str<E>(self, value: &str) -> std::result::Result<GeneratedField, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
match value {
|
||||
"magnet" => Ok(GeneratedField::Magnet),
|
||||
"outputDir" | "output_dir" => Ok(GeneratedField::OutputDir),
|
||||
_ => Err(serde::de::Error::unknown_field(value, FIELDS)),
|
||||
}
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_identifier(GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
struct GeneratedVisitor;
|
||||
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
|
||||
type Value = AddRequest;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("struct torrent.AddRequest")
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map_: V) -> std::result::Result<AddRequest, V::Error>
|
||||
where
|
||||
V: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let mut magnet__ = None;
|
||||
let mut output_dir__ = None;
|
||||
while let Some(k) = map_.next_key()? {
|
||||
match k {
|
||||
GeneratedField::Magnet => {
|
||||
if magnet__.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("magnet"));
|
||||
}
|
||||
magnet__ = Some(map_.next_value()?);
|
||||
}
|
||||
GeneratedField::OutputDir => {
|
||||
if output_dir__.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("outputDir"));
|
||||
}
|
||||
output_dir__ = Some(map_.next_value()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(AddRequest {
|
||||
magnet: magnet__.unwrap_or_default(),
|
||||
output_dir: output_dir__.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_struct("torrent.AddRequest", FIELDS, GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
impl serde::Serialize for AddResponse {
|
||||
#[allow(deprecated)]
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
let mut len = 0;
|
||||
if !self.id.is_empty() {
|
||||
len += 1;
|
||||
}
|
||||
let mut struct_ser = serializer.serialize_struct("torrent.AddResponse", len)?;
|
||||
if !self.id.is_empty() {
|
||||
struct_ser.serialize_field("id", &self.id)?;
|
||||
}
|
||||
struct_ser.end()
|
||||
}
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for AddResponse {
|
||||
#[allow(deprecated)]
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
const FIELDS: &[&str] = &["id"];
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum GeneratedField {
|
||||
Id,
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for GeneratedField {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<GeneratedField, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct GeneratedVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
|
||||
type Value = GeneratedField;
|
||||
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter<'_>,
|
||||
) -> std::fmt::Result {
|
||||
write!(formatter, "expected one of: {:?}", &FIELDS)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn visit_str<E>(self, value: &str) -> std::result::Result<GeneratedField, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
match value {
|
||||
"id" => Ok(GeneratedField::Id),
|
||||
_ => Err(serde::de::Error::unknown_field(value, FIELDS)),
|
||||
}
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_identifier(GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
struct GeneratedVisitor;
|
||||
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
|
||||
type Value = AddResponse;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("struct torrent.AddResponse")
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map_: V) -> std::result::Result<AddResponse, V::Error>
|
||||
where
|
||||
V: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let mut id__ = None;
|
||||
while let Some(k) = map_.next_key()? {
|
||||
match k {
|
||||
GeneratedField::Id => {
|
||||
if id__.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("id"));
|
||||
}
|
||||
id__ = Some(map_.next_value()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(AddResponse {
|
||||
id: id__.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_struct("torrent.AddResponse", FIELDS, GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
impl serde::Serialize for ListRequest {
|
||||
#[allow(deprecated)]
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
let len = 0;
|
||||
let struct_ser = serializer.serialize_struct("torrent.ListRequest", len)?;
|
||||
struct_ser.end()
|
||||
}
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for ListRequest {
|
||||
#[allow(deprecated)]
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
const FIELDS: &[&str] = &[];
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum GeneratedField {}
|
||||
impl<'de> serde::Deserialize<'de> for GeneratedField {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<GeneratedField, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct GeneratedVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
|
||||
type Value = GeneratedField;
|
||||
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter<'_>,
|
||||
) -> std::fmt::Result {
|
||||
write!(formatter, "expected one of: {:?}", &FIELDS)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn visit_str<E>(self, value: &str) -> std::result::Result<GeneratedField, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Err(serde::de::Error::unknown_field(value, FIELDS))
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_identifier(GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
struct GeneratedVisitor;
|
||||
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
|
||||
type Value = ListRequest;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("struct torrent.ListRequest")
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map_: V) -> std::result::Result<ListRequest, V::Error>
|
||||
where
|
||||
V: serde::de::MapAccess<'de>,
|
||||
{
|
||||
while map_.next_key::<GeneratedField>()?.is_some() {
|
||||
let _ = map_.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
Ok(ListRequest {})
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_struct("torrent.ListRequest", FIELDS, GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
impl serde::Serialize for ListResponse {
|
||||
#[allow(deprecated)]
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
let mut len = 0;
|
||||
if !self.torrents.is_empty() {
|
||||
len += 1;
|
||||
}
|
||||
let mut struct_ser = serializer.serialize_struct("torrent.ListResponse", len)?;
|
||||
if !self.torrents.is_empty() {
|
||||
struct_ser.serialize_field("torrents", &self.torrents)?;
|
||||
}
|
||||
struct_ser.end()
|
||||
}
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for ListResponse {
|
||||
#[allow(deprecated)]
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
const FIELDS: &[&str] = &["torrents"];
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum GeneratedField {
|
||||
Torrents,
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for GeneratedField {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<GeneratedField, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct GeneratedVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
|
||||
type Value = GeneratedField;
|
||||
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter<'_>,
|
||||
) -> std::fmt::Result {
|
||||
write!(formatter, "expected one of: {:?}", &FIELDS)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn visit_str<E>(self, value: &str) -> std::result::Result<GeneratedField, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
match value {
|
||||
"torrents" => Ok(GeneratedField::Torrents),
|
||||
_ => Err(serde::de::Error::unknown_field(value, FIELDS)),
|
||||
}
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_identifier(GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
struct GeneratedVisitor;
|
||||
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
|
||||
type Value = ListResponse;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("struct torrent.ListResponse")
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map_: V) -> std::result::Result<ListResponse, V::Error>
|
||||
where
|
||||
V: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let mut torrents__ = None;
|
||||
while let Some(k) = map_.next_key()? {
|
||||
match k {
|
||||
GeneratedField::Torrents => {
|
||||
if torrents__.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("torrents"));
|
||||
}
|
||||
torrents__ = Some(map_.next_value()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ListResponse {
|
||||
torrents: torrents__.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_struct("torrent.ListResponse", FIELDS, GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
impl serde::Serialize for State {
|
||||
#[allow(deprecated)]
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let variant = match self {
|
||||
Self::Unspecified => "STATE_UNSPECIFIED",
|
||||
Self::Pending => "PENDING",
|
||||
Self::Downloading => "DOWNLOADING",
|
||||
Self::Paused => "PAUSED",
|
||||
Self::Finished => "FINISHED",
|
||||
Self::Error => "ERROR",
|
||||
};
|
||||
serializer.serialize_str(variant)
|
||||
}
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for State {
|
||||
#[allow(deprecated)]
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
const FIELDS: &[&str] = &[
|
||||
"STATE_UNSPECIFIED",
|
||||
"PENDING",
|
||||
"DOWNLOADING",
|
||||
"PAUSED",
|
||||
"FINISHED",
|
||||
"ERROR",
|
||||
];
|
||||
|
||||
struct GeneratedVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
|
||||
type Value = State;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(formatter, "expected one of: {:?}", &FIELDS)
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, v: i64) -> std::result::Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
i32::try_from(v)
|
||||
.ok()
|
||||
.and_then(|x| x.try_into().ok())
|
||||
.ok_or_else(|| {
|
||||
serde::de::Error::invalid_value(serde::de::Unexpected::Signed(v), &self)
|
||||
})
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, v: u64) -> std::result::Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
i32::try_from(v)
|
||||
.ok()
|
||||
.and_then(|x| x.try_into().ok())
|
||||
.ok_or_else(|| {
|
||||
serde::de::Error::invalid_value(serde::de::Unexpected::Unsigned(v), &self)
|
||||
})
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
match value {
|
||||
"STATE_UNSPECIFIED" => Ok(State::Unspecified),
|
||||
"PENDING" => Ok(State::Pending),
|
||||
"DOWNLOADING" => Ok(State::Downloading),
|
||||
"PAUSED" => Ok(State::Paused),
|
||||
"FINISHED" => Ok(State::Finished),
|
||||
"ERROR" => Ok(State::Error),
|
||||
_ => Err(serde::de::Error::unknown_variant(value, FIELDS)),
|
||||
}
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_any(GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
impl serde::Serialize for StatusRequest {
|
||||
#[allow(deprecated)]
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
let mut len = 0;
|
||||
if !self.id.is_empty() {
|
||||
len += 1;
|
||||
}
|
||||
let mut struct_ser = serializer.serialize_struct("torrent.StatusRequest", len)?;
|
||||
if !self.id.is_empty() {
|
||||
struct_ser.serialize_field("id", &self.id)?;
|
||||
}
|
||||
struct_ser.end()
|
||||
}
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for StatusRequest {
|
||||
#[allow(deprecated)]
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
const FIELDS: &[&str] = &["id"];
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum GeneratedField {
|
||||
Id,
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for GeneratedField {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<GeneratedField, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct GeneratedVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
|
||||
type Value = GeneratedField;
|
||||
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter<'_>,
|
||||
) -> std::fmt::Result {
|
||||
write!(formatter, "expected one of: {:?}", &FIELDS)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn visit_str<E>(self, value: &str) -> std::result::Result<GeneratedField, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
match value {
|
||||
"id" => Ok(GeneratedField::Id),
|
||||
_ => Err(serde::de::Error::unknown_field(value, FIELDS)),
|
||||
}
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_identifier(GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
struct GeneratedVisitor;
|
||||
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
|
||||
type Value = StatusRequest;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("struct torrent.StatusRequest")
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map_: V) -> std::result::Result<StatusRequest, V::Error>
|
||||
where
|
||||
V: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let mut id__ = None;
|
||||
while let Some(k) = map_.next_key()? {
|
||||
match k {
|
||||
GeneratedField::Id => {
|
||||
if id__.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("id"));
|
||||
}
|
||||
id__ = Some(map_.next_value()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(StatusRequest {
|
||||
id: id__.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_struct("torrent.StatusRequest", FIELDS, GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
impl serde::Serialize for TorrentStatus {
|
||||
#[allow(deprecated)]
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
let mut len = 0;
|
||||
if !self.id.is_empty() {
|
||||
len += 1;
|
||||
}
|
||||
if !self.info_hash.is_empty() {
|
||||
len += 1;
|
||||
}
|
||||
if !self.name.is_empty() {
|
||||
len += 1;
|
||||
}
|
||||
if !self.source.is_empty() {
|
||||
len += 1;
|
||||
}
|
||||
if !self.output_path.is_empty() {
|
||||
len += 1;
|
||||
}
|
||||
if self.total_bytes != 0 {
|
||||
len += 1;
|
||||
}
|
||||
if self.downloaded_bytes != 0 {
|
||||
len += 1;
|
||||
}
|
||||
if self.state != 0 {
|
||||
len += 1;
|
||||
}
|
||||
if !self.error_message.is_empty() {
|
||||
len += 1;
|
||||
}
|
||||
let mut struct_ser = serializer.serialize_struct("torrent.TorrentStatus", len)?;
|
||||
if !self.id.is_empty() {
|
||||
struct_ser.serialize_field("id", &self.id)?;
|
||||
}
|
||||
if !self.info_hash.is_empty() {
|
||||
struct_ser.serialize_field("infoHash", &self.info_hash)?;
|
||||
}
|
||||
if !self.name.is_empty() {
|
||||
struct_ser.serialize_field("name", &self.name)?;
|
||||
}
|
||||
if !self.source.is_empty() {
|
||||
struct_ser.serialize_field("source", &self.source)?;
|
||||
}
|
||||
if !self.output_path.is_empty() {
|
||||
struct_ser.serialize_field("outputPath", &self.output_path)?;
|
||||
}
|
||||
if self.total_bytes != 0 {
|
||||
#[allow(clippy::needless_borrow)]
|
||||
#[allow(clippy::needless_borrows_for_generic_args)]
|
||||
struct_ser.serialize_field(
|
||||
"totalBytes",
|
||||
ToString::to_string(&self.total_bytes).as_str(),
|
||||
)?;
|
||||
}
|
||||
if self.downloaded_bytes != 0 {
|
||||
#[allow(clippy::needless_borrow)]
|
||||
#[allow(clippy::needless_borrows_for_generic_args)]
|
||||
struct_ser.serialize_field(
|
||||
"downloadedBytes",
|
||||
ToString::to_string(&self.downloaded_bytes).as_str(),
|
||||
)?;
|
||||
}
|
||||
if self.state != 0 {
|
||||
let v = State::try_from(self.state).map_err(|_| {
|
||||
serde::ser::Error::custom(format!("Invalid variant {}", self.state))
|
||||
})?;
|
||||
struct_ser.serialize_field("state", &v)?;
|
||||
}
|
||||
if !self.error_message.is_empty() {
|
||||
struct_ser.serialize_field("errorMessage", &self.error_message)?;
|
||||
}
|
||||
struct_ser.end()
|
||||
}
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for TorrentStatus {
|
||||
#[allow(deprecated)]
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
const FIELDS: &[&str] = &[
|
||||
"id",
|
||||
"info_hash",
|
||||
"infoHash",
|
||||
"name",
|
||||
"source",
|
||||
"output_path",
|
||||
"outputPath",
|
||||
"total_bytes",
|
||||
"totalBytes",
|
||||
"downloaded_bytes",
|
||||
"downloadedBytes",
|
||||
"state",
|
||||
"error_message",
|
||||
"errorMessage",
|
||||
];
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum GeneratedField {
|
||||
Id,
|
||||
InfoHash,
|
||||
Name,
|
||||
Source,
|
||||
OutputPath,
|
||||
TotalBytes,
|
||||
DownloadedBytes,
|
||||
State,
|
||||
ErrorMessage,
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for GeneratedField {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<GeneratedField, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct GeneratedVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
|
||||
type Value = GeneratedField;
|
||||
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter<'_>,
|
||||
) -> std::fmt::Result {
|
||||
write!(formatter, "expected one of: {:?}", &FIELDS)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn visit_str<E>(self, value: &str) -> std::result::Result<GeneratedField, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
match value {
|
||||
"id" => Ok(GeneratedField::Id),
|
||||
"infoHash" | "info_hash" => Ok(GeneratedField::InfoHash),
|
||||
"name" => Ok(GeneratedField::Name),
|
||||
"source" => Ok(GeneratedField::Source),
|
||||
"outputPath" | "output_path" => Ok(GeneratedField::OutputPath),
|
||||
"totalBytes" | "total_bytes" => Ok(GeneratedField::TotalBytes),
|
||||
"downloadedBytes" | "downloaded_bytes" => {
|
||||
Ok(GeneratedField::DownloadedBytes)
|
||||
}
|
||||
"state" => Ok(GeneratedField::State),
|
||||
"errorMessage" | "error_message" => Ok(GeneratedField::ErrorMessage),
|
||||
_ => Err(serde::de::Error::unknown_field(value, FIELDS)),
|
||||
}
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_identifier(GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
struct GeneratedVisitor;
|
||||
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
|
||||
type Value = TorrentStatus;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("struct torrent.TorrentStatus")
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map_: V) -> std::result::Result<TorrentStatus, V::Error>
|
||||
where
|
||||
V: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let mut id__ = None;
|
||||
let mut info_hash__ = None;
|
||||
let mut name__ = None;
|
||||
let mut source__ = None;
|
||||
let mut output_path__ = None;
|
||||
let mut total_bytes__ = None;
|
||||
let mut downloaded_bytes__ = None;
|
||||
let mut state__ = None;
|
||||
let mut error_message__ = None;
|
||||
while let Some(k) = map_.next_key()? {
|
||||
match k {
|
||||
GeneratedField::Id => {
|
||||
if id__.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("id"));
|
||||
}
|
||||
id__ = Some(map_.next_value()?);
|
||||
}
|
||||
GeneratedField::InfoHash => {
|
||||
if info_hash__.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("infoHash"));
|
||||
}
|
||||
info_hash__ = Some(map_.next_value()?);
|
||||
}
|
||||
GeneratedField::Name => {
|
||||
if name__.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("name"));
|
||||
}
|
||||
name__ = Some(map_.next_value()?);
|
||||
}
|
||||
GeneratedField::Source => {
|
||||
if source__.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("source"));
|
||||
}
|
||||
source__ = Some(map_.next_value()?);
|
||||
}
|
||||
GeneratedField::OutputPath => {
|
||||
if output_path__.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("outputPath"));
|
||||
}
|
||||
output_path__ = Some(map_.next_value()?);
|
||||
}
|
||||
GeneratedField::TotalBytes => {
|
||||
if total_bytes__.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("totalBytes"));
|
||||
}
|
||||
total_bytes__ = Some(
|
||||
map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?
|
||||
.0,
|
||||
);
|
||||
}
|
||||
GeneratedField::DownloadedBytes => {
|
||||
if downloaded_bytes__.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("downloadedBytes"));
|
||||
}
|
||||
downloaded_bytes__ = Some(
|
||||
map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?
|
||||
.0,
|
||||
);
|
||||
}
|
||||
GeneratedField::State => {
|
||||
if state__.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("state"));
|
||||
}
|
||||
state__ = Some(map_.next_value::<State>()? as i32);
|
||||
}
|
||||
GeneratedField::ErrorMessage => {
|
||||
if error_message__.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("errorMessage"));
|
||||
}
|
||||
error_message__ = Some(map_.next_value()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(TorrentStatus {
|
||||
id: id__.unwrap_or_default(),
|
||||
info_hash: info_hash__.unwrap_or_default(),
|
||||
name: name__.unwrap_or_default(),
|
||||
source: source__.unwrap_or_default(),
|
||||
output_path: output_path__.unwrap_or_default(),
|
||||
total_bytes: total_bytes__.unwrap_or_default(),
|
||||
downloaded_bytes: downloaded_bytes__.unwrap_or_default(),
|
||||
state: state__.unwrap_or_default(),
|
||||
error_message: error_message__.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_struct("torrent.TorrentStatus", FIELDS, GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
// @generated
|
||||
/// Generated client implementations.
|
||||
pub mod torrents_client {
|
||||
#![allow(
|
||||
unused_variables,
|
||||
dead_code,
|
||||
missing_docs,
|
||||
clippy::wildcard_imports,
|
||||
clippy::let_unit_value
|
||||
)]
|
||||
use tonic::codegen::http::Uri;
|
||||
use tonic::codegen::*;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TorrentsClient<T> {
|
||||
inner: tonic::client::Grpc<T>,
|
||||
}
|
||||
impl TorrentsClient<tonic::transport::Channel> {
|
||||
/// Attempt to create a new client by connecting to a given endpoint.
|
||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
||||
where
|
||||
D: TryInto<tonic::transport::Endpoint>,
|
||||
D::Error: Into<StdError>,
|
||||
{
|
||||
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
||||
Ok(Self::new(conn))
|
||||
}
|
||||
}
|
||||
impl<T> TorrentsClient<T>
|
||||
where
|
||||
T: tonic::client::GrpcService<tonic::body::Body>,
|
||||
T::Error: Into<StdError>,
|
||||
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
||||
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
||||
{
|
||||
pub fn new(inner: T) -> Self {
|
||||
let inner = tonic::client::Grpc::new(inner);
|
||||
Self { inner }
|
||||
}
|
||||
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
||||
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
||||
Self { inner }
|
||||
}
|
||||
pub fn with_interceptor<F>(
|
||||
inner: T,
|
||||
interceptor: F,
|
||||
) -> TorrentsClient<InterceptedService<T, F>>
|
||||
where
|
||||
F: tonic::service::Interceptor,
|
||||
T::ResponseBody: Default,
|
||||
T: tonic::codegen::Service<
|
||||
http::Request<tonic::body::Body>,
|
||||
Response = http::Response<
|
||||
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
|
||||
>,
|
||||
>,
|
||||
<T as tonic::codegen::Service<http::Request<tonic::body::Body>>>::Error:
|
||||
Into<StdError> + std::marker::Send + std::marker::Sync,
|
||||
{
|
||||
TorrentsClient::new(InterceptedService::new(inner, interceptor))
|
||||
}
|
||||
/// Compress requests with the given encoding.
|
||||
///
|
||||
/// This requires the server to support it otherwise it might respond with an
|
||||
/// error.
|
||||
#[must_use]
|
||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.inner = self.inner.send_compressed(encoding);
|
||||
self
|
||||
}
|
||||
/// Enable decompressing responses.
|
||||
#[must_use]
|
||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.inner = self.inner.accept_compressed(encoding);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of a decoded message.
|
||||
///
|
||||
/// Default: `4MB`
|
||||
#[must_use]
|
||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.inner = self.inner.max_decoding_message_size(limit);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of an encoded message.
|
||||
///
|
||||
/// Default: `usize::MAX`
|
||||
#[must_use]
|
||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.inner = self.inner.max_encoding_message_size(limit);
|
||||
self
|
||||
}
|
||||
pub async fn add(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::AddRequest>,
|
||||
) -> std::result::Result<tonic::Response<super::AddResponse>, tonic::Status> {
|
||||
self.inner.ready().await.map_err(|e| {
|
||||
tonic::Status::unknown(format!("Service was not ready: {}", e.into()))
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static("/torrent.Torrents/Add");
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("torrent.Torrents", "Add"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn status(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::StatusRequest>,
|
||||
) -> std::result::Result<tonic::Response<super::TorrentStatus>, tonic::Status> {
|
||||
self.inner.ready().await.map_err(|e| {
|
||||
tonic::Status::unknown(format!("Service was not ready: {}", e.into()))
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static("/torrent.Torrents/Status");
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("torrent.Torrents", "Status"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn list(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::ListRequest>,
|
||||
) -> std::result::Result<tonic::Response<super::ListResponse>, tonic::Status> {
|
||||
self.inner.ready().await.map_err(|e| {
|
||||
tonic::Status::unknown(format!("Service was not ready: {}", e.into()))
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static("/torrent.Torrents/List");
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("torrent.Torrents", "List"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generated server implementations.
|
||||
pub mod torrents_server {
|
||||
#![allow(
|
||||
unused_variables,
|
||||
dead_code,
|
||||
missing_docs,
|
||||
clippy::wildcard_imports,
|
||||
clippy::let_unit_value
|
||||
)]
|
||||
use tonic::codegen::*;
|
||||
/// Generated trait containing gRPC methods that should be implemented for use with TorrentsServer.
|
||||
#[async_trait]
|
||||
pub trait Torrents: std::marker::Send + std::marker::Sync + 'static {
|
||||
async fn add(
|
||||
&self,
|
||||
request: tonic::Request<super::AddRequest>,
|
||||
) -> std::result::Result<tonic::Response<super::AddResponse>, tonic::Status>;
|
||||
async fn status(
|
||||
&self,
|
||||
request: tonic::Request<super::StatusRequest>,
|
||||
) -> std::result::Result<tonic::Response<super::TorrentStatus>, tonic::Status>;
|
||||
async fn list(
|
||||
&self,
|
||||
request: tonic::Request<super::ListRequest>,
|
||||
) -> std::result::Result<tonic::Response<super::ListResponse>, tonic::Status>;
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct TorrentsServer<T> {
|
||||
inner: Arc<T>,
|
||||
accept_compression_encodings: EnabledCompressionEncodings,
|
||||
send_compression_encodings: EnabledCompressionEncodings,
|
||||
max_decoding_message_size: Option<usize>,
|
||||
max_encoding_message_size: Option<usize>,
|
||||
}
|
||||
impl<T> TorrentsServer<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self::from_arc(Arc::new(inner))
|
||||
}
|
||||
pub fn from_arc(inner: Arc<T>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
accept_compression_encodings: Default::default(),
|
||||
send_compression_encodings: Default::default(),
|
||||
max_decoding_message_size: None,
|
||||
max_encoding_message_size: None,
|
||||
}
|
||||
}
|
||||
pub fn with_interceptor<F>(inner: T, interceptor: F) -> InterceptedService<Self, F>
|
||||
where
|
||||
F: tonic::service::Interceptor,
|
||||
{
|
||||
InterceptedService::new(Self::new(inner), interceptor)
|
||||
}
|
||||
/// Enable decompressing requests with the given encoding.
|
||||
#[must_use]
|
||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.accept_compression_encodings.enable(encoding);
|
||||
self
|
||||
}
|
||||
/// Compress responses with the given encoding, if the client supports it.
|
||||
#[must_use]
|
||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.send_compression_encodings.enable(encoding);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of a decoded message.
|
||||
///
|
||||
/// Default: `4MB`
|
||||
#[must_use]
|
||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.max_decoding_message_size = Some(limit);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of an encoded message.
|
||||
///
|
||||
/// Default: `usize::MAX`
|
||||
#[must_use]
|
||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.max_encoding_message_size = Some(limit);
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<T, B> tonic::codegen::Service<http::Request<B>> for TorrentsServer<T>
|
||||
where
|
||||
T: Torrents,
|
||||
B: Body + std::marker::Send + 'static,
|
||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
||||
{
|
||||
type Response = http::Response<tonic::body::Body>;
|
||||
type Error = std::convert::Infallible;
|
||||
type Future = BoxFuture<Self::Response, Self::Error>;
|
||||
fn poll_ready(
|
||||
&mut self,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<std::result::Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||
match req.uri().path() {
|
||||
"/torrent.Torrents/Add" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct AddSvc<T: Torrents>(pub Arc<T>);
|
||||
impl<T: Torrents> tonic::server::UnaryService<super::AddRequest> for AddSvc<T> {
|
||||
type Response = super::AddResponse;
|
||||
type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::AddRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move { <T as Torrents>::add(&inner, request).await };
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = AddSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/torrent.Torrents/Status" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct StatusSvc<T: Torrents>(pub Arc<T>);
|
||||
impl<T: Torrents> tonic::server::UnaryService<super::StatusRequest> for StatusSvc<T> {
|
||||
type Response = super::TorrentStatus;
|
||||
type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::StatusRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move { <T as Torrents>::status(&inner, request).await };
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = StatusSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/torrent.Torrents/List" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct ListSvc<T: Torrents>(pub Arc<T>);
|
||||
impl<T: Torrents> tonic::server::UnaryService<super::ListRequest> for ListSvc<T> {
|
||||
type Response = super::ListResponse;
|
||||
type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::ListRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move { <T as Torrents>::list(&inner, request).await };
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = ListSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
_ => Box::pin(async move {
|
||||
let mut response = http::Response::new(tonic::body::Body::default());
|
||||
let headers = response.headers_mut();
|
||||
headers.insert(
|
||||
tonic::Status::GRPC_STATUS,
|
||||
(tonic::Code::Unimplemented as i32).into(),
|
||||
);
|
||||
headers.insert(
|
||||
http::header::CONTENT_TYPE,
|
||||
tonic::metadata::GRPC_CONTENT_TYPE,
|
||||
);
|
||||
Ok(response)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> Clone for TorrentsServer<T> {
|
||||
fn clone(&self) -> Self {
|
||||
let inner = self.inner.clone();
|
||||
Self {
|
||||
inner,
|
||||
accept_compression_encodings: self.accept_compression_encodings,
|
||||
send_compression_encodings: self.send_compression_encodings,
|
||||
max_decoding_message_size: self.max_decoding_message_size,
|
||||
max_encoding_message_size: self.max_encoding_message_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generated gRPC service name
|
||||
pub const SERVICE_NAME: &str = "torrent.Torrents";
|
||||
impl<T> tonic::server::NamedService for TorrentsServer<T> {
|
||||
const NAME: &'static str = SERVICE_NAME;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
pub mod torrent {
|
||||
include!("generated/torrent/torrent.rs");
|
||||
}
|
||||
|
||||
pub use torrent::{
|
||||
AddRequest, AddResponse, ListRequest, ListResponse, State, StatusRequest, TorrentStatus,
|
||||
torrents_client::TorrentsClient,
|
||||
torrents_server::{Torrents, TorrentsServer},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "tora"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.103"
|
||||
clap = { version = "4.6.1", features = ["derive", "env"] }
|
||||
hyper-util = { version = "0.1.20", features = ["tokio"] }
|
||||
tokio = { version = "1.52.3", features = ["rt-multi-thread", "macros", "net"] }
|
||||
tonic = "0.14.6"
|
||||
tonic-health = "0.14.6"
|
||||
tora-proto = { version = "0.1.0", path = "../proto" }
|
||||
tower = { version = "0.5.3", features = ["util"] }
|
||||
@@ -0,0 +1,35 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "tora", about = "CLI client for the tora torrent daemon")]
|
||||
pub struct Cli {
|
||||
/// Path to torad's Unix domain socket.
|
||||
#[arg(long, global = true, env = "TORAD_SOCKET")]
|
||||
pub socket: Option<PathBuf>,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Command {
|
||||
/// Check whether torad is reachable and healthy.
|
||||
Health,
|
||||
/// Start downloading a torrent from a magnet link.
|
||||
Download {
|
||||
/// The magnet link to download.
|
||||
magnet: String,
|
||||
/// Directory to save the torrent into. Defaults to torad's configured download directory.
|
||||
#[arg(long)]
|
||||
output_dir: Option<String>,
|
||||
},
|
||||
/// Show the status of a single torrent.
|
||||
Status {
|
||||
/// The torrent id returned by `tora download`.
|
||||
id: String,
|
||||
},
|
||||
/// List all torrents known to torad.
|
||||
List,
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
mod cli;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use cli::{Cli, Command};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tokio::net::UnixStream;
|
||||
use tonic::transport::{Channel, Endpoint, Uri};
|
||||
use tonic_health::pb::HealthCheckRequest;
|
||||
use tonic_health::pb::health_check_response::ServingStatus;
|
||||
use tonic_health::pb::health_client::HealthClient;
|
||||
use tora_proto::{AddRequest, ListRequest, State, StatusRequest, TorrentStatus, TorrentsClient};
|
||||
use tower::service_fn;
|
||||
|
||||
fn default_socket_path() -> PathBuf {
|
||||
std::env::temp_dir().join("torad.sock")
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
let socket = cli.socket.unwrap_or_else(default_socket_path);
|
||||
|
||||
match cli.command {
|
||||
Command::Health => health(&socket).await,
|
||||
Command::Download { magnet, output_dir } => download(&socket, magnet, output_dir).await,
|
||||
Command::Status { id } => status(&socket, id).await,
|
||||
Command::List => list(&socket).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect(socket: &Path) -> Result<Channel> {
|
||||
let connect_path = socket.to_path_buf();
|
||||
let channel = Endpoint::try_from("http://[::]:50051")?
|
||||
.connect_with_connector(service_fn(move |_: Uri| {
|
||||
let socket = connect_path.clone();
|
||||
async move { Ok::<_, std::io::Error>(TokioIo::new(UnixStream::connect(socket).await?)) }
|
||||
}))
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"could not reach torad at {} - is it running? start it with `torad`",
|
||||
socket.display()
|
||||
)
|
||||
})?;
|
||||
Ok(channel)
|
||||
}
|
||||
|
||||
async fn health(socket: &Path) -> Result<()> {
|
||||
let channel = connect(socket).await?;
|
||||
|
||||
let mut client = HealthClient::new(channel);
|
||||
let response = client
|
||||
.check(HealthCheckRequest {
|
||||
service: String::new(),
|
||||
})
|
||||
.await
|
||||
.context("health check RPC failed")?;
|
||||
|
||||
let status =
|
||||
ServingStatus::try_from(response.into_inner().status).unwrap_or(ServingStatus::Unknown);
|
||||
|
||||
println!("{}", status.as_str_name());
|
||||
if status != ServingStatus::Serving {
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download(socket: &Path, magnet: String, output_dir: Option<String>) -> Result<()> {
|
||||
let channel = connect(socket).await?;
|
||||
let mut client = TorrentsClient::new(channel);
|
||||
|
||||
let response = client
|
||||
.add(AddRequest {
|
||||
magnet,
|
||||
output_dir: output_dir.unwrap_or_default(),
|
||||
})
|
||||
.await
|
||||
.context("Add RPC failed")?;
|
||||
|
||||
println!("{}", response.into_inner().id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn status(socket: &Path, id: String) -> Result<()> {
|
||||
let channel = connect(socket).await?;
|
||||
let mut client = TorrentsClient::new(channel);
|
||||
|
||||
let response = client
|
||||
.status(StatusRequest { id })
|
||||
.await
|
||||
.context("Status RPC failed")?;
|
||||
|
||||
print_status(&response.into_inner());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list(socket: &Path) -> Result<()> {
|
||||
let channel = connect(socket).await?;
|
||||
let mut client = TorrentsClient::new(channel);
|
||||
|
||||
let response = client
|
||||
.list(ListRequest {})
|
||||
.await
|
||||
.context("List RPC failed")?
|
||||
.into_inner();
|
||||
|
||||
if response.torrents.is_empty() {
|
||||
println!("no torrents");
|
||||
return Ok(());
|
||||
}
|
||||
for torrent in &response.torrents {
|
||||
print_status(torrent);
|
||||
println!();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_status(status: &TorrentStatus) {
|
||||
let state = State::try_from(status.state).unwrap_or(State::Unspecified);
|
||||
println!("id: {}", status.id);
|
||||
println!("info_hash: {}", status.info_hash);
|
||||
println!(
|
||||
"name: {}",
|
||||
if status.name.is_empty() {
|
||||
"(unknown)"
|
||||
} else {
|
||||
&status.name
|
||||
}
|
||||
);
|
||||
println!("state: {}", state.as_str_name());
|
||||
println!(
|
||||
"progress: {} / {} bytes",
|
||||
status.downloaded_bytes, status.total_bytes
|
||||
);
|
||||
println!("output_path: {}", status.output_path);
|
||||
if !status.error_message.is_empty() {
|
||||
println!("error: {}", status.error_message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "torad"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.103"
|
||||
clap = { version = "4.6.1", features = ["derive", "env"] }
|
||||
librqbit = { version = "8.1.1", default-features = false, features = ["rust-tls", "http-api-client"] }
|
||||
sqlx = { version = "0.9.0", features = ["postgres", "runtime-tokio", "tls-rustls", "uuid"] }
|
||||
tokio = { version = "1.52.3", features = ["rt-multi-thread", "macros", "net", "signal"] }
|
||||
tokio-stream = { version = "0.1.18", features = ["net"] }
|
||||
tonic = "0.14.6"
|
||||
tonic-health = "0.14.6"
|
||||
tora-proto = { version = "0.1.0", path = "../proto" }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = "0.3.23"
|
||||
uuid = { version = "1.23.4", features = ["v4"] }
|
||||
@@ -0,0 +1,122 @@
|
||||
use anyhow::Result;
|
||||
use sqlx::postgres::{PgPool, PgPoolOptions};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn connect(database_url: &str) -> Result<PgPool> {
|
||||
let pool = PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(database_url)
|
||||
.await?;
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
pub async fn ping(pool: &PgPool) -> Result<()> {
|
||||
sqlx::query("SELECT 1").execute(pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type)]
|
||||
#[sqlx(type_name = "torrent_state", rename_all = "lowercase")]
|
||||
pub enum TorrentState {
|
||||
Pending,
|
||||
Downloading,
|
||||
Paused,
|
||||
Finished,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, sqlx::FromRow)]
|
||||
pub struct TorrentRow {
|
||||
pub id: Uuid,
|
||||
pub info_hash: String,
|
||||
pub name: Option<String>,
|
||||
pub source: String,
|
||||
pub output_path: String,
|
||||
pub total_bytes: Option<i64>,
|
||||
pub downloaded_bytes: i64,
|
||||
pub state: TorrentState,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
/// Insert a new pending torrent, or return the existing row if the info_hash is already tracked.
|
||||
pub async fn insert_pending(
|
||||
pool: &PgPool,
|
||||
info_hash: &str,
|
||||
source: &str,
|
||||
output_path: &str,
|
||||
) -> Result<TorrentRow> {
|
||||
let row = sqlx::query_as::<_, TorrentRow>(
|
||||
"INSERT INTO torrents (info_hash, source, output_path)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (info_hash) DO UPDATE SET updated_at = now()
|
||||
RETURNING id, info_hash, name, source, output_path, total_bytes, downloaded_bytes, state, error_message",
|
||||
)
|
||||
.bind(info_hash)
|
||||
.bind(source)
|
||||
.bind(output_path)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
pub async fn get(pool: &PgPool, id: Uuid) -> Result<Option<TorrentRow>> {
|
||||
let row = sqlx::query_as::<_, TorrentRow>(
|
||||
"SELECT id, info_hash, name, source, output_path, total_bytes, downloaded_bytes, state, error_message
|
||||
FROM torrents WHERE id = $1",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
pub async fn list(pool: &PgPool) -> Result<Vec<TorrentRow>> {
|
||||
let rows = sqlx::query_as::<_, TorrentRow>(
|
||||
"SELECT id, info_hash, name, source, output_path, total_bytes, downloaded_bytes, state, error_message
|
||||
FROM torrents ORDER BY added_at DESC",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub async fn list_pending(pool: &PgPool) -> Result<Vec<TorrentRow>> {
|
||||
let rows = sqlx::query_as::<_, TorrentRow>(
|
||||
"SELECT id, info_hash, name, source, output_path, total_bytes, downloaded_bytes, state, error_message
|
||||
FROM torrents WHERE state = 'pending' ORDER BY added_at",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub struct Progress<'a> {
|
||||
pub name: Option<&'a str>,
|
||||
pub total_bytes: i64,
|
||||
pub downloaded_bytes: i64,
|
||||
pub state: TorrentState,
|
||||
pub error_message: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub async fn update_progress(pool: &PgPool, id: Uuid, progress: Progress<'_>) -> Result<()> {
|
||||
sqlx::query(
|
||||
"UPDATE torrents
|
||||
SET name = COALESCE($2, name),
|
||||
total_bytes = $3,
|
||||
downloaded_bytes = $4,
|
||||
state = $5,
|
||||
error_message = $6,
|
||||
updated_at = now(),
|
||||
completed_at = CASE WHEN $5 = 'finished' THEN now() ELSE completed_at END
|
||||
WHERE id = $1",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(progress.name)
|
||||
.bind(progress.total_bytes)
|
||||
.bind(progress.downloaded_bytes)
|
||||
.bind(progress.state)
|
||||
.bind(progress.error_message)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
use anyhow::{Context, Result};
|
||||
use librqbit::Magnet;
|
||||
|
||||
/// Extract the BTIH info hash from a magnet link, normalized to lowercase hex.
|
||||
pub fn info_hash(magnet: &str) -> Result<String> {
|
||||
let parsed = Magnet::parse(magnet).context("failed to parse magnet link")?;
|
||||
let id20 = parsed
|
||||
.as_id20()
|
||||
.context("magnet link has no v1 (BTIH) info hash")?;
|
||||
Ok(id20.as_string())
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
mod db;
|
||||
mod magnet;
|
||||
mod torrents;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use tokio::net::UnixListener;
|
||||
use tokio_stream::wrappers::UnixListenerStream;
|
||||
use tonic::transport::Server;
|
||||
use tonic_health::ServingStatus;
|
||||
use tonic_health::server::HealthReporter;
|
||||
use tora_proto::TorrentsServer;
|
||||
use tracing::{error, info};
|
||||
|
||||
use torrents::{GrpcTorrents, TorrentManager};
|
||||
|
||||
const HEALTH_POLL_INTERVAL: Duration = Duration::from_secs(3);
|
||||
const HEALTH_QUERY_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "torad", about = "tora background daemon")]
|
||||
struct Args {
|
||||
/// Path to the Unix domain socket to serve on.
|
||||
#[arg(long, env = "TORAD_SOCKET")]
|
||||
socket: Option<PathBuf>,
|
||||
|
||||
/// Postgres connection string.
|
||||
#[arg(long, env = "DATABASE_URL")]
|
||||
database_url: String,
|
||||
|
||||
/// Directory torrents are downloaded into by default.
|
||||
#[arg(long)]
|
||||
download_dir: Option<PathBuf>,
|
||||
|
||||
/// Path to write torad's PID to, so it can be killed without hunting for the process
|
||||
/// (e.g. `kill $(cat /tmp/torad.pid)`).
|
||||
#[arg(long, env = "TORAD_PID_FILE")]
|
||||
pid_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn default_socket_path() -> PathBuf {
|
||||
std::env::temp_dir().join("torad.sock")
|
||||
}
|
||||
|
||||
fn default_pid_file_path() -> PathBuf {
|
||||
std::env::temp_dir().join("torad.pid")
|
||||
}
|
||||
|
||||
fn default_download_dir() -> PathBuf {
|
||||
std::env::var_os("HOME")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("Downloads")
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let args = Args::parse();
|
||||
let socket = args.socket.unwrap_or_else(default_socket_path);
|
||||
let pid_file = args.pid_file.unwrap_or_else(default_pid_file_path);
|
||||
let download_dir = args.download_dir.unwrap_or_else(default_download_dir);
|
||||
|
||||
let pool = db::connect(&args.database_url)
|
||||
.await
|
||||
.context("failed to connect to Postgres")?;
|
||||
info!("connected to Postgres");
|
||||
|
||||
let manager = TorrentManager::new(pool.clone(), download_dir)
|
||||
.await
|
||||
.context("failed to start torrent manager")?;
|
||||
manager.clone().spawn_poller();
|
||||
|
||||
std::fs::write(&pid_file, std::process::id().to_string())
|
||||
.with_context(|| format!("failed to write pid file at {}", pid_file.display()))?;
|
||||
|
||||
if socket.exists() {
|
||||
std::fs::remove_file(&socket)
|
||||
.with_context(|| format!("failed to remove stale socket at {}", socket.display()))?;
|
||||
}
|
||||
let listener = UnixListener::bind(&socket)
|
||||
.with_context(|| format!("failed to bind socket at {}", socket.display()))?;
|
||||
let uds_stream = UnixListenerStream::new(listener);
|
||||
|
||||
let (health_reporter, health_service) = tonic_health::server::health_reporter();
|
||||
tokio::spawn(poll_db_health(pool, health_reporter));
|
||||
|
||||
info!(socket = %socket.display(), "torad listening");
|
||||
|
||||
Server::builder()
|
||||
.add_service(health_service)
|
||||
.add_service(TorrentsServer::new(GrpcTorrents::new(manager)))
|
||||
.serve_with_incoming_shutdown(uds_stream, shutdown_signal())
|
||||
.await
|
||||
.context("gRPC server error")?;
|
||||
|
||||
info!("shutting down");
|
||||
let _ = std::fs::remove_file(&socket);
|
||||
let _ = std::fs::remove_file(&pid_file);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn poll_db_health(pool: sqlx::PgPool, reporter: HealthReporter) {
|
||||
let mut interval = tokio::time::interval(HEALTH_POLL_INTERVAL);
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let status = match tokio::time::timeout(HEALTH_QUERY_TIMEOUT, db::ping(&pool)).await {
|
||||
Ok(Ok(())) => ServingStatus::Serving,
|
||||
Ok(Err(err)) => {
|
||||
error!(error = %err, "postgres health check query failed");
|
||||
ServingStatus::NotServing
|
||||
}
|
||||
Err(_) => {
|
||||
error!("postgres health check timed out");
|
||||
ServingStatus::NotServing
|
||||
}
|
||||
};
|
||||
reporter.set_service_status("", status).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
|
||||
let mut sigint = signal(SignalKind::interrupt()).expect("failed to install SIGINT handler");
|
||||
let mut sigterm = signal(SignalKind::terminate()).expect("failed to install SIGTERM handler");
|
||||
|
||||
tokio::select! {
|
||||
_ = sigint.recv() => {},
|
||||
_ = sigterm.recv() => {},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use librqbit::{AddTorrent, AddTorrentOptions, ManagedTorrent, Session, TorrentStatsState};
|
||||
use sqlx::PgPool;
|
||||
use tokio::sync::Mutex;
|
||||
use tonic::{Request, Response, Status};
|
||||
use tora_proto::{
|
||||
AddRequest, AddResponse, ListRequest, ListResponse, State as ProtoState, StatusRequest,
|
||||
TorrentStatus, Torrents,
|
||||
};
|
||||
use tracing::{error, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::db::{self, TorrentRow, TorrentState};
|
||||
use crate::magnet;
|
||||
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(2);
|
||||
|
||||
pub struct TorrentManager {
|
||||
pool: PgPool,
|
||||
session: Arc<Session>,
|
||||
default_output_dir: PathBuf,
|
||||
tracked: Mutex<HashMap<Uuid, Arc<ManagedTorrent>>>,
|
||||
}
|
||||
|
||||
impl TorrentManager {
|
||||
pub async fn new(pool: PgPool, download_dir: PathBuf) -> Result<Arc<Self>> {
|
||||
std::fs::create_dir_all(&download_dir).with_context(|| {
|
||||
format!(
|
||||
"failed to create download directory {}",
|
||||
download_dir.display()
|
||||
)
|
||||
})?;
|
||||
let session = Session::new(download_dir.clone())
|
||||
.await
|
||||
.context("failed to create librqbit session")?;
|
||||
Ok(Arc::new(Self {
|
||||
pool,
|
||||
session,
|
||||
default_output_dir: download_dir,
|
||||
tracked: Mutex::new(HashMap::new()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn add(&self, magnet: &str, output_dir: Option<&str>) -> Result<TorrentRow> {
|
||||
let info_hash = magnet::info_hash(magnet)?;
|
||||
let output_path = output_dir
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| self.default_output_dir.display().to_string());
|
||||
db::insert_pending(&self.pool, &info_hash, magnet, &output_path).await
|
||||
}
|
||||
|
||||
pub async fn get(&self, id: Uuid) -> Result<Option<TorrentRow>> {
|
||||
db::get(&self.pool, id).await
|
||||
}
|
||||
|
||||
pub async fn list(&self) -> Result<Vec<TorrentRow>> {
|
||||
db::list(&self.pool).await
|
||||
}
|
||||
|
||||
pub fn spawn_poller(self: Arc<Self>) {
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(POLL_INTERVAL);
|
||||
loop {
|
||||
interval.tick().await;
|
||||
self.pick_up_pending().await;
|
||||
self.report_progress().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn pick_up_pending(&self) {
|
||||
let pending = match db::list_pending(&self.pool).await {
|
||||
Ok(rows) => rows,
|
||||
Err(err) => {
|
||||
error!(error = %err, "failed to list pending torrents");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if pending.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut tracked = self.tracked.lock().await;
|
||||
for row in pending {
|
||||
if tracked.contains_key(&row.id) {
|
||||
continue;
|
||||
}
|
||||
let options = AddTorrentOptions {
|
||||
output_folder: Some(row.output_path.clone()),
|
||||
overwrite: true,
|
||||
..Default::default()
|
||||
};
|
||||
match self
|
||||
.session
|
||||
.add_torrent(AddTorrent::from_url(row.source.clone()), Some(options))
|
||||
.await
|
||||
{
|
||||
Ok(response) => match response.into_handle() {
|
||||
Some(handle) => {
|
||||
info!(id = %row.id, info_hash = %row.info_hash, "torrent added to session");
|
||||
tracked.insert(row.id, handle);
|
||||
}
|
||||
None => warn!(id = %row.id, "add_torrent returned no handle"),
|
||||
},
|
||||
Err(err) => {
|
||||
error!(id = %row.id, error = %err, "failed to add torrent to session");
|
||||
let message = err.to_string();
|
||||
let progress = db::Progress {
|
||||
name: None,
|
||||
total_bytes: 0,
|
||||
downloaded_bytes: 0,
|
||||
state: TorrentState::Error,
|
||||
error_message: Some(message.as_str()),
|
||||
};
|
||||
if let Err(err) = db::update_progress(&self.pool, row.id, progress).await {
|
||||
error!(error = %err, "failed to persist torrent add error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn report_progress(&self) {
|
||||
let tracked = self.tracked.lock().await;
|
||||
for (id, handle) in tracked.iter() {
|
||||
let stats = handle.stats();
|
||||
let name = handle.name();
|
||||
let state = if stats.finished {
|
||||
TorrentState::Finished
|
||||
} else {
|
||||
match stats.state {
|
||||
TorrentStatsState::Initializing => TorrentState::Pending,
|
||||
TorrentStatsState::Live => TorrentState::Downloading,
|
||||
TorrentStatsState::Paused => TorrentState::Paused,
|
||||
TorrentStatsState::Error => TorrentState::Error,
|
||||
}
|
||||
};
|
||||
let progress = db::Progress {
|
||||
name: name.as_deref(),
|
||||
total_bytes: i64::try_from(stats.total_bytes).unwrap_or(i64::MAX),
|
||||
downloaded_bytes: i64::try_from(stats.progress_bytes).unwrap_or(i64::MAX),
|
||||
state,
|
||||
error_message: stats.error.as_deref(),
|
||||
};
|
||||
if let Err(err) = db::update_progress(&self.pool, *id, progress).await {
|
||||
error!(id = %id, error = %err, "failed to persist torrent progress");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn proto_state(state: TorrentState) -> ProtoState {
|
||||
match state {
|
||||
TorrentState::Pending => ProtoState::Pending,
|
||||
TorrentState::Downloading => ProtoState::Downloading,
|
||||
TorrentState::Paused => ProtoState::Paused,
|
||||
TorrentState::Finished => ProtoState::Finished,
|
||||
TorrentState::Error => ProtoState::Error,
|
||||
}
|
||||
}
|
||||
|
||||
fn row_to_status(row: TorrentRow) -> TorrentStatus {
|
||||
TorrentStatus {
|
||||
id: row.id.to_string(),
|
||||
info_hash: row.info_hash,
|
||||
name: row.name.unwrap_or_default(),
|
||||
source: row.source,
|
||||
output_path: row.output_path,
|
||||
total_bytes: row.total_bytes.unwrap_or(0) as u64,
|
||||
downloaded_bytes: row.downloaded_bytes as u64,
|
||||
state: proto_state(row.state) as i32,
|
||||
error_message: row.error_message.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
// ---- gRPC Torrents service. This is the only interface `tora` talks to. ----
|
||||
|
||||
pub struct GrpcTorrents {
|
||||
manager: Arc<TorrentManager>,
|
||||
}
|
||||
|
||||
impl GrpcTorrents {
|
||||
pub fn new(manager: Arc<TorrentManager>) -> Self {
|
||||
Self { manager }
|
||||
}
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl Torrents for GrpcTorrents {
|
||||
async fn add(&self, request: Request<AddRequest>) -> Result<Response<AddResponse>, Status> {
|
||||
let req = request.into_inner();
|
||||
let output_dir = (!req.output_dir.is_empty()).then_some(req.output_dir.as_str());
|
||||
let row = self
|
||||
.manager
|
||||
.add(&req.magnet, output_dir)
|
||||
.await
|
||||
.map_err(|err| Status::internal(err.to_string()))?;
|
||||
Ok(Response::new(AddResponse {
|
||||
id: row.id.to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn status(
|
||||
&self,
|
||||
request: Request<StatusRequest>,
|
||||
) -> Result<Response<TorrentStatus>, Status> {
|
||||
let id = Uuid::parse_str(&request.into_inner().id)
|
||||
.map_err(|_| Status::invalid_argument("invalid id"))?;
|
||||
let row = self
|
||||
.manager
|
||||
.get(id)
|
||||
.await
|
||||
.map_err(|err| Status::internal(err.to_string()))?
|
||||
.ok_or_else(|| Status::not_found("torrent not found"))?;
|
||||
Ok(Response::new(row_to_status(row)))
|
||||
}
|
||||
|
||||
async fn list(&self, _request: Request<ListRequest>) -> Result<Response<ListResponse>, Status> {
|
||||
let rows = self
|
||||
.manager
|
||||
.list()
|
||||
.await
|
||||
.map_err(|err| Status::internal(err.to_string()))?;
|
||||
Ok(Response::new(ListResponse {
|
||||
torrents: rows.into_iter().map(row_to_status).collect(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
CREATE TYPE torrent_state AS ENUM (
|
||||
'pending',
|
||||
'downloading',
|
||||
'paused',
|
||||
'finished',
|
||||
'error'
|
||||
);
|
||||
|
||||
CREATE TABLE torrents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
info_hash TEXT NOT NULL UNIQUE,
|
||||
name TEXT,
|
||||
source TEXT NOT NULL,
|
||||
output_path TEXT NOT NULL,
|
||||
total_bytes BIGINT,
|
||||
downloaded_bytes BIGINT NOT NULL DEFAULT 0,
|
||||
state torrent_state NOT NULL DEFAULT 'pending',
|
||||
error_message TEXT,
|
||||
added_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
completed_at TIMESTAMPTZ
|
||||
);
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"nodes": {
|
||||
"devenv": {
|
||||
"locked": {
|
||||
"dir": "src/modules",
|
||||
"lastModified": 1782938471,
|
||||
"narHash": "sha256-m//AHi+NJN+1eTTdEaL3oXcuTHy1XtV6XiyGtsP9PJE=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "46197d1e4c2ca0cc1f0635927b3bee3d62b51779",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"dir": "src/modules",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1767039857,
|
||||
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||
"owner": "NixOS",
|
||||
"repo": "flake-compat",
|
||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1782908218,
|
||||
"narHash": "sha256-wLMOrPgVyeF3XmP+qfYcLqnVdTxikdcSvbIY7rA9jTA=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "9f7e99119ece7705299595299f3b031f39356de1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"inputs": {
|
||||
"nixpkgs-src": "nixpkgs-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1782924808,
|
||||
"narHash": "sha256-tn2ahNv3ZNXyVHdPx/uw+N0xa7YSMtXUr6OEvu+s8ng=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"rev": "e2c881cb8d5f1cac6cef9d71f0d450a55691b999",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "rolling",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1782636521,
|
||||
"narHash": "sha256-OG8laCOGtkxlB1JH3XqOnXxnkGJme4mQdXo5hkhCQIY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e1c1b84752fb0897897380a3cae9dc7fcab91ca3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"git-hooks": "git-hooks",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1780220602,
|
||||
"narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "db947814a175b7ca6ded66e21383d938df01c227",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
|
||||
{
|
||||
languages.rust.enable = true;
|
||||
git-hooks.hooks = {
|
||||
clippy = {
|
||||
enable = true;
|
||||
settings.allFeatures = true;
|
||||
};
|
||||
treefmt.enable = true;
|
||||
};
|
||||
|
||||
treefmt = {
|
||||
enable = true;
|
||||
config.programs = {
|
||||
nixfmt.enable = true;
|
||||
rustfmt.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
packages = with pkgs; [
|
||||
git
|
||||
just
|
||||
protobuf
|
||||
buf
|
||||
grpcurl
|
||||
|
||||
# Protobuf generators
|
||||
protoc-gen-prost-crate
|
||||
protoc-gen-prost-serde
|
||||
protoc-gen-tonic
|
||||
protoc-gen-prost
|
||||
|
||||
opencode
|
||||
];
|
||||
|
||||
services.postgres = {
|
||||
enable = true;
|
||||
initialDatabases = [
|
||||
{
|
||||
name = "toradb";
|
||||
schema = ./db/schema.sql;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
env.PGDATABASE = "toradb";
|
||||
env.DATABASE_URL = "postgresql://${builtins.getEnv "USER"}@localhost/${config.env.PGDATABASE}?host=${config.env.PGHOST}";
|
||||
env.TORAD_SOCKET = "${config.env.DEVENV_RUNTIME}/torad.sock";
|
||||
env.TORAD_PID_FILE = "${config.env.DEVENV_RUNTIME}/torad.pid";
|
||||
|
||||
processes.torad = {
|
||||
exec = ''
|
||||
cargo run -p torad -- --socket "$TORAD_SOCKET" --pid-file "$TORAD_PID_FILE"
|
||||
'';
|
||||
# Waits for postgres's own readiness probe (pg_isready + SELECT 1), not just process start.
|
||||
after = [ "devenv:processes:postgres" ];
|
||||
};
|
||||
|
||||
outputs = {
|
||||
tora = config.languages.rust.import ./. { };
|
||||
};
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
inputs:
|
||||
git-hooks:
|
||||
url: github:cachix/git-hooks.nix
|
||||
inputs:
|
||||
nixpkgs:
|
||||
follows: nixpkgs
|
||||
nixpkgs:
|
||||
url: github:cachix/devenv-nixpkgs/rolling
|
||||
treefmt-nix:
|
||||
url: github:numtide/treefmt-nix
|
||||
inputs:
|
||||
nixpkgs:
|
||||
follows: nixpkgs
|
||||
@@ -0,0 +1,11 @@
|
||||
version: v2
|
||||
plugins:
|
||||
- local: protoc-gen-prost
|
||||
out: ../crates/proto/src/generated
|
||||
opt:
|
||||
- file_descriptor_set
|
||||
- enable_type_names
|
||||
- local: protoc-gen-prost-serde
|
||||
out: ../crates/proto/src/generated
|
||||
- local: protoc-gen-tonic
|
||||
out: ../crates/proto/src/generated
|
||||
@@ -0,0 +1,9 @@
|
||||
version: v2
|
||||
modules:
|
||||
- path: .
|
||||
lint:
|
||||
use:
|
||||
- STANDARD
|
||||
breaking:
|
||||
use:
|
||||
- FILE
|
||||
@@ -0,0 +1,53 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package torrent;
|
||||
|
||||
service Torrents {
|
||||
// Add a torrent from a magnet link and start downloading it.
|
||||
rpc Add(AddRequest) returns (AddResponse);
|
||||
// Get the current status of a single torrent.
|
||||
rpc Status(StatusRequest) returns (TorrentStatus);
|
||||
// List all torrents known to the daemon.
|
||||
rpc List(ListRequest) returns (ListResponse);
|
||||
}
|
||||
|
||||
message AddRequest {
|
||||
string magnet = 1;
|
||||
// Optional output directory override. Empty means use torad's default.
|
||||
string output_dir = 2;
|
||||
}
|
||||
|
||||
message AddResponse {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message StatusRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message ListRequest {}
|
||||
|
||||
message ListResponse {
|
||||
repeated TorrentStatus torrents = 1;
|
||||
}
|
||||
|
||||
enum State {
|
||||
STATE_UNSPECIFIED = 0;
|
||||
PENDING = 1;
|
||||
DOWNLOADING = 2;
|
||||
PAUSED = 3;
|
||||
FINISHED = 4;
|
||||
ERROR = 5;
|
||||
}
|
||||
|
||||
message TorrentStatus {
|
||||
string id = 1;
|
||||
string info_hash = 2;
|
||||
string name = 3;
|
||||
string source = 4;
|
||||
string output_path = 5;
|
||||
uint64 total_bytes = 6;
|
||||
uint64 downloaded_bytes = 7;
|
||||
State state = 8;
|
||||
string error_message = 9;
|
||||
}
|
||||
Reference in New Issue
Block a user