Simple mini clone of torrra

This commit is contained in:
Alexander
2026-07-02 17:02:49 +02:00
commit 80ebf1cb63
24 changed files with 6424 additions and 0 deletions
+8
View File
@@ -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
View File
@@ -0,0 +1,10 @@
# Devenv
.devenv*
devenv.local.nix
devenv.local.yaml
# direnv
.direnv
# pre-commit
.pre-commit-config.yaml
Generated
+3888
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
[workspace]
resolver = "2"
members = ["crates/torad", "crates/tora", "crates/proto"]
+11
View File
@@ -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;
}
}
+9
View File
@@ -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},
};
+14
View File
@@ -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"] }
+35
View File
@@ -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,
}
+143
View File
@@ -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);
}
}
+18
View File
@@ -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"] }
+122
View File
@@ -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(())
}
+11
View File
@@ -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())
}
+137
View File
@@ -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() => {},
}
}
+234
View File
@@ -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(),
}))
}
}
+22
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+11
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
version: v2
modules:
- path: .
lint:
use:
- STANDARD
breaking:
use:
- FILE
+53
View 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;
}