feat: insert torrent in tracker on torrent upload

This commit is contained in:
FrenchGithubUser
2025-11-03 20:45:29 +01:00
parent d72bd8baa2
commit 37ad464856
28 changed files with 567 additions and 124 deletions

5
Cargo.lock generated
View File

@@ -59,9 +59,9 @@ dependencies = [
[[package]]
name = "actix-http"
version = "3.11.0"
version = "3.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44dfe5c9e0004c623edc65391dfd51daa201e7e30ebd9c9bedf873048ec32bc2"
checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49"
dependencies = [
"actix-codec",
"actix-rt",
@@ -577,6 +577,7 @@ dependencies = [
"serde",
"sqlx",
"thiserror 2.0.16",
"utoipa",
]
[[package]]

View File

@@ -36,3 +36,6 @@ REDIS_PORT=6379
# Used for the backend to make requests to the tracker
# and vice-versa
ARCADIA_TRACKER_API_KEY=change_me
# used to make requests from the backend to the tracker
# ARCADIA_TRACKER_URL is used for public access, and this one only for the backend
ARCADIA_TRACKER_URL_INTERNAL=http://localhost:8081

View File

@@ -71,3 +71,6 @@ REDIS_PORT=6379
# Used for the backend to make requests to the tracker
# and vice-versa
ARCADIA_TRACKER_API_KEY=change_me
# used to make requests from the backend to the tracker
# ARCADIA_TRACKER_URL is used for public access, and this one only for the backend
ARCADIA_TRACKER_URL_INTERNAL=http://localhost:8081

View File

@@ -58,6 +58,8 @@ pub struct TrackerConfig {
pub name: String,
#[envconfig(from = "ARCADIA_TRACKER_URL")]
pub url: Url,
#[envconfig(from = "ARCADIA_TRACKER_URL_INTERNAL")]
pub url_internal: Url,
#[envconfig(from = "ARCADIA_TRACKER_API_KEY")]
pub api_key: String,

View File

@@ -1,5 +1,8 @@
use actix_multipart::form::MultipartForm;
use actix_web::{web::Data, HttpResponse};
use arcadia_shared::tracker::models::torrent::APIInsertTorrent;
use log::debug;
use reqwest::Client;
use crate::{middlewares::auth_middleware::Authdata, Arcadia};
use arcadia_common::error::Result;
@@ -30,5 +33,36 @@ pub async fn exec<R: RedisPoolInterface + 'static>(
let torrent = arc.pool.create_torrent(&form, user.sub).await?;
let client = Client::new();
let mut url = arc.env.tracker.url_internal.clone();
url.path_segments_mut()
.unwrap()
.push("api")
.push("torrents");
let payload = APIInsertTorrent {
id: torrent.id as u32,
info_hash: torrent.info_hash,
is_deleted: false,
seeders: 0,
leechers: 0,
times_completed: 0,
download_factor: torrent.upload_factor as u8,
upload_factor: torrent.download_factor as u8,
};
let res = client
.put(url)
.header("x-api-key", arc.env.tracker.api_key.clone())
.json(&payload)
.send()
.await?;
debug!(
"Tried to insert new torrent into tracker's db and got: {:?}",
res
);
Ok(HttpResponse::Created().json(torrent))
}

View File

@@ -0,0 +1,56 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n id,\n upload_factor,\n download_factor,\n seeders,\n leechers,\n times_completed,\n CASE\n WHEN deleted_at IS NOT NULL THEN TRUE\n ELSE FALSE\n END AS \"is_deleted!\"\n FROM torrents\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "upload_factor",
"type_info": "Int2"
},
{
"ordinal": 2,
"name": "download_factor",
"type_info": "Int2"
},
{
"ordinal": 3,
"name": "seeders",
"type_info": "Int8"
},
{
"ordinal": 4,
"name": "leechers",
"type_info": "Int8"
},
{
"ordinal": 5,
"name": "times_completed",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "is_deleted!",
"type_info": "Bool"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false,
false,
false,
null
]
},
"hash": "46d7eee133e0653d9f11ab67f5d6faec7050c9b4c6a8c78e2097015d3e0fb7fb"
}

View File

@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n id,\n passkey as \"passkey: Passkey\"\n FROM users\n WHERE banned = FALSE\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "passkey: Passkey",
"type_info": "Varchar"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false
]
},
"hash": "4edda78ffd766d9ec15eb015fe5b985755924b0f0b44d5cf9411059cfbc5c757"
}

View File

@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO peers (\n peer_id,\n ip,\n port,\n agent,\n uploaded,\n downloaded,\n \"left\",\n active,\n seeder,\n created_at,\n updated_at,\n torrent_id,\n user_id\n )\n SELECT\n t.peer_id,\n t.ip,\n t.port,\n t.agent,\n t.uploaded,\n t.downloaded,\n t.\"left\",\n t.active,\n t.seeder,\n -- stored as timestamp without time zone in DB\n (t.created_at AT TIME ZONE 'UTC')::timestamp,\n (t.updated_at AT TIME ZONE 'UTC')::timestamp,\n t.torrent_id,\n t.user_id\n FROM (\n SELECT * FROM unnest(\n $1::bytea[],\n $2::inet[],\n $3::int[],\n $4::varchar[],\n $5::bigint[],\n $6::bigint[],\n $7::bigint[],\n $8::boolean[],\n $9::boolean[],\n $10::timestamptz[],\n $11::timestamptz[],\n $12::int[],\n $13::int[]\n ) AS t(\n peer_id,\n ip,\n port,\n agent,\n uploaded,\n downloaded,\n \"left\",\n active,\n seeder,\n created_at,\n updated_at,\n torrent_id,\n user_id\n )\n ) AS t\n ON CONFLICT (user_id, torrent_id, peer_id) DO UPDATE SET\n ip = EXCLUDED.ip,\n port = EXCLUDED.port,\n agent = EXCLUDED.agent,\n uploaded = EXCLUDED.uploaded,\n downloaded = EXCLUDED.downloaded,\n \"left\" = EXCLUDED.\"left\",\n active = EXCLUDED.active,\n seeder = EXCLUDED.seeder,\n updated_at = EXCLUDED.updated_at\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"ByteaArray",
"InetArray",
"Int4Array",
"VarcharArray",
"Int8Array",
"Int8Array",
"Int8Array",
"BoolArray",
"BoolArray",
"TimestamptzArray",
"TimestamptzArray",
"Int4Array",
"Int4Array"
]
},
"nullable": []
},
"hash": "599587c7ce69b090843274603171c411af859ae256fc01eaf66af2aa2a922900"
}

View File

@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE users\n SET\n uploaded = uploaded + updates.uploaded_delta,\n downloaded = downloaded + updates.downloaded_delta,\n real_uploaded = real_uploaded + updates.uploaded_delta,\n real_downloaded = real_downloaded + updates.downloaded_delta\n FROM (\n SELECT * FROM unnest($1::int[], $2::bigint[], $3::bigint[], $4::bigint[], $5::bigint[]) AS\n t(user_id, uploaded_delta, downloaded_delta, real_uploaded_delta, real_downloaded_delta)\n ) AS updates\n WHERE users.id = updates.user_id\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4Array",
"Int8Array",
"Int8Array",
"Int8Array",
"Int8Array"
]
},
"nullable": []
},
"hash": "68c566af855b2cb1e46b77cd829934488b1d4da1086f74b7491d7468753f539a"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE torrents\n SET\n release_name = $2,\n release_group = $3,\n description = $4,\n uploaded_as_anonymous = $5,\n mediainfo = $6,\n container = $7,\n duration = $8,\n audio_codec = $9,\n audio_bitrate = $10,\n audio_bitrate_sampling = $11,\n audio_channels = $12,\n video_codec = $13,\n features = $14,\n subtitle_languages = $15,\n video_resolution = $16,\n video_resolution_other_x = $17,\n video_resolution_other_y = $18,\n languages = $19,\n extras = $20,\n updated_at = NOW()\n WHERE id = $1 AND deleted_at IS NULL\n RETURNING\n id, upload_factor, download_factor, seeders, leechers,\n times_completed, snatched, edition_group_id, created_at, updated_at,\n created_by_id,\n deleted_at AS \"deleted_at!: _\",\n deleted_by_id AS \"deleted_by_id!: _\",\n extras AS \"extras!: _\",\n languages AS \"languages!: _\",\n release_name, release_group, description, file_amount_per_type,\n uploaded_as_anonymous, file_list, mediainfo, trumpable, staff_checked,\n container, size, duration,\n audio_codec AS \"audio_codec: _\",\n audio_bitrate,\n audio_bitrate_sampling AS \"audio_bitrate_sampling: _\",\n audio_channels AS \"audio_channels: _\",\n video_codec AS \"video_codec: _\",\n features AS \"features!: _\",\n subtitle_languages AS \"subtitle_languages!: _\",\n video_resolution AS \"video_resolution!: _\",\n video_resolution_other_x,\n video_resolution_other_y\n ",
"query": "\n UPDATE torrents\n SET\n release_name = $2,\n release_group = $3,\n description = $4,\n uploaded_as_anonymous = $5,\n mediainfo = $6,\n container = $7,\n duration = $8,\n audio_codec = $9,\n audio_bitrate = $10,\n audio_bitrate_sampling = $11,\n audio_channels = $12,\n video_codec = $13,\n features = $14,\n subtitle_languages = $15,\n video_resolution = $16,\n video_resolution_other_x = $17,\n video_resolution_other_y = $18,\n languages = $19,\n extras = $20,\n updated_at = NOW()\n WHERE id = $1 AND deleted_at IS NULL\n RETURNING\n id, info_hash as \"info_hash: InfoHash\", upload_factor, download_factor, seeders, leechers,\n times_completed, snatched, edition_group_id, created_at, updated_at,\n created_by_id,\n deleted_at AS \"deleted_at!: _\",\n deleted_by_id AS \"deleted_by_id!: _\",\n extras AS \"extras!: _\",\n languages AS \"languages!: _\",\n release_name, release_group, description, file_amount_per_type,\n uploaded_as_anonymous, file_list, mediainfo, trumpable, staff_checked,\n container, size, duration,\n audio_codec AS \"audio_codec: _\",\n audio_bitrate,\n audio_bitrate_sampling AS \"audio_bitrate_sampling: _\",\n audio_channels AS \"audio_channels: _\",\n video_codec AS \"video_codec: _\",\n features AS \"features!: _\",\n subtitle_languages AS \"subtitle_languages!: _\",\n video_resolution AS \"video_resolution!: _\",\n video_resolution_other_x,\n video_resolution_other_y\n ",
"describe": {
"columns": [
{
@@ -10,66 +10,71 @@
},
{
"ordinal": 1,
"name": "info_hash: InfoHash",
"type_info": "Bytea"
},
{
"ordinal": 2,
"name": "upload_factor",
"type_info": "Int2"
},
{
"ordinal": 2,
"ordinal": 3,
"name": "download_factor",
"type_info": "Int2"
},
{
"ordinal": 3,
"ordinal": 4,
"name": "seeders",
"type_info": "Int8"
},
{
"ordinal": 4,
"ordinal": 5,
"name": "leechers",
"type_info": "Int8"
},
{
"ordinal": 5,
"ordinal": 6,
"name": "times_completed",
"type_info": "Int4"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "snatched",
"type_info": "Int8"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "edition_group_id",
"type_info": "Int4"
},
{
"ordinal": 8,
"ordinal": 9,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 9,
"ordinal": 10,
"name": "updated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"ordinal": 11,
"name": "created_by_id",
"type_info": "Int4"
},
{
"ordinal": 11,
"ordinal": 12,
"name": "deleted_at!: _",
"type_info": "Timestamptz"
},
{
"ordinal": 12,
"ordinal": 13,
"name": "deleted_by_id!: _",
"type_info": "Int4"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "extras!: _",
"type_info": {
"Custom": {
@@ -96,7 +101,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "languages!: _",
"type_info": {
"Custom": {
@@ -166,67 +171,67 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "release_name",
"type_info": "Text"
},
{
"ordinal": 16,
"ordinal": 17,
"name": "release_group",
"type_info": "Varchar"
},
{
"ordinal": 17,
"ordinal": 18,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 18,
"ordinal": 19,
"name": "file_amount_per_type",
"type_info": "Jsonb"
},
{
"ordinal": 19,
"ordinal": 20,
"name": "uploaded_as_anonymous",
"type_info": "Bool"
},
{
"ordinal": 20,
"ordinal": 21,
"name": "file_list",
"type_info": "Jsonb"
},
{
"ordinal": 21,
"ordinal": 22,
"name": "mediainfo",
"type_info": "Text"
},
{
"ordinal": 22,
"ordinal": 23,
"name": "trumpable",
"type_info": "Text"
},
{
"ordinal": 23,
"ordinal": 24,
"name": "staff_checked",
"type_info": "Bool"
},
{
"ordinal": 24,
"ordinal": 25,
"name": "container",
"type_info": "Varchar"
},
{
"ordinal": 25,
"ordinal": 26,
"name": "size",
"type_info": "Int8"
},
{
"ordinal": 26,
"ordinal": 27,
"name": "duration",
"type_info": "Int4"
},
{
"ordinal": 27,
"ordinal": 28,
"name": "audio_codec: _",
"type_info": {
"Custom": {
@@ -249,12 +254,12 @@
}
},
{
"ordinal": 28,
"ordinal": 29,
"name": "audio_bitrate",
"type_info": "Int4"
},
{
"ordinal": 29,
"ordinal": 30,
"name": "audio_bitrate_sampling: _",
"type_info": {
"Custom": {
@@ -284,7 +289,7 @@
}
},
{
"ordinal": 30,
"ordinal": 31,
"name": "audio_channels: _",
"type_info": {
"Custom": {
@@ -303,7 +308,7 @@
}
},
{
"ordinal": 31,
"ordinal": 32,
"name": "video_codec: _",
"type_info": {
"Custom": {
@@ -326,7 +331,7 @@
}
},
{
"ordinal": 32,
"ordinal": 33,
"name": "features!: _",
"type_info": {
"Custom": {
@@ -355,7 +360,7 @@
}
},
{
"ordinal": 33,
"ordinal": 34,
"name": "subtitle_languages!: _",
"type_info": {
"Custom": {
@@ -425,7 +430,7 @@
}
},
{
"ordinal": 34,
"ordinal": 35,
"name": "video_resolution!: _",
"type_info": {
"Custom": {
@@ -449,12 +454,12 @@
}
},
{
"ordinal": 35,
"ordinal": 36,
"name": "video_resolution_other_x",
"type_info": "Int4"
},
{
"ordinal": 36,
"ordinal": 37,
"name": "video_resolution_other_y",
"type_info": "Int4"
}
@@ -765,6 +770,7 @@
false,
false,
false,
false,
true,
true,
true,
@@ -793,5 +799,5 @@
true
]
},
"hash": "bb6c8c54e372a54f7a8345ce2308f10a11e77d60305e1e865f2e2e28da9ee622"
"hash": "780af85dcf7cbfead514338b6b0ef6039e8c0b82703143d854df8c0faba9f950"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n id, upload_factor, download_factor, seeders, leechers,\n times_completed, snatched, edition_group_id, created_at, updated_at,\n created_by_id,\n deleted_at AS \"deleted_at!: _\",\n deleted_by_id AS \"deleted_by_id!: _\",\n extras AS \"extras!: _\",\n languages AS \"languages!: _\",\n release_name, release_group, description, file_amount_per_type,\n uploaded_as_anonymous, file_list, mediainfo, trumpable, staff_checked,\n container, size, duration,\n audio_codec AS \"audio_codec: _\",\n audio_bitrate,\n audio_bitrate_sampling AS \"audio_bitrate_sampling: _\",\n audio_channels AS \"audio_channels: _\",\n video_codec AS \"video_codec: _\",\n features AS \"features!: _\",\n subtitle_languages AS \"subtitle_languages!: _\",\n video_resolution AS \"video_resolution!: _\",\n video_resolution_other_x,\n video_resolution_other_y\n FROM torrents\n WHERE id = $1 AND deleted_at is NULL\n ",
"query": "\n SELECT\n id, info_hash as \"info_hash: InfoHash\", upload_factor, download_factor, seeders, leechers,\n times_completed, snatched, edition_group_id, created_at, updated_at,\n created_by_id,\n deleted_at AS \"deleted_at!: _\",\n deleted_by_id AS \"deleted_by_id!: _\",\n extras AS \"extras!: _\",\n languages AS \"languages!: _\",\n release_name, release_group, description, file_amount_per_type,\n uploaded_as_anonymous, file_list, mediainfo, trumpable, staff_checked,\n container, size, duration,\n audio_codec AS \"audio_codec: _\",\n audio_bitrate,\n audio_bitrate_sampling AS \"audio_bitrate_sampling: _\",\n audio_channels AS \"audio_channels: _\",\n video_codec AS \"video_codec: _\",\n features AS \"features!: _\",\n subtitle_languages AS \"subtitle_languages!: _\",\n video_resolution AS \"video_resolution!: _\",\n video_resolution_other_x,\n video_resolution_other_y\n FROM torrents\n WHERE id = $1 AND deleted_at is NULL\n ",
"describe": {
"columns": [
{
@@ -10,66 +10,71 @@
},
{
"ordinal": 1,
"name": "info_hash: InfoHash",
"type_info": "Bytea"
},
{
"ordinal": 2,
"name": "upload_factor",
"type_info": "Int2"
},
{
"ordinal": 2,
"ordinal": 3,
"name": "download_factor",
"type_info": "Int2"
},
{
"ordinal": 3,
"ordinal": 4,
"name": "seeders",
"type_info": "Int8"
},
{
"ordinal": 4,
"ordinal": 5,
"name": "leechers",
"type_info": "Int8"
},
{
"ordinal": 5,
"ordinal": 6,
"name": "times_completed",
"type_info": "Int4"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "snatched",
"type_info": "Int8"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "edition_group_id",
"type_info": "Int4"
},
{
"ordinal": 8,
"ordinal": 9,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 9,
"ordinal": 10,
"name": "updated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"ordinal": 11,
"name": "created_by_id",
"type_info": "Int4"
},
{
"ordinal": 11,
"ordinal": 12,
"name": "deleted_at!: _",
"type_info": "Timestamptz"
},
{
"ordinal": 12,
"ordinal": 13,
"name": "deleted_by_id!: _",
"type_info": "Int4"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "extras!: _",
"type_info": {
"Custom": {
@@ -96,7 +101,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "languages!: _",
"type_info": {
"Custom": {
@@ -166,67 +171,67 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "release_name",
"type_info": "Text"
},
{
"ordinal": 16,
"ordinal": 17,
"name": "release_group",
"type_info": "Varchar"
},
{
"ordinal": 17,
"ordinal": 18,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 18,
"ordinal": 19,
"name": "file_amount_per_type",
"type_info": "Jsonb"
},
{
"ordinal": 19,
"ordinal": 20,
"name": "uploaded_as_anonymous",
"type_info": "Bool"
},
{
"ordinal": 20,
"ordinal": 21,
"name": "file_list",
"type_info": "Jsonb"
},
{
"ordinal": 21,
"ordinal": 22,
"name": "mediainfo",
"type_info": "Text"
},
{
"ordinal": 22,
"ordinal": 23,
"name": "trumpable",
"type_info": "Text"
},
{
"ordinal": 23,
"ordinal": 24,
"name": "staff_checked",
"type_info": "Bool"
},
{
"ordinal": 24,
"ordinal": 25,
"name": "container",
"type_info": "Varchar"
},
{
"ordinal": 25,
"ordinal": 26,
"name": "size",
"type_info": "Int8"
},
{
"ordinal": 26,
"ordinal": 27,
"name": "duration",
"type_info": "Int4"
},
{
"ordinal": 27,
"ordinal": 28,
"name": "audio_codec: _",
"type_info": {
"Custom": {
@@ -249,12 +254,12 @@
}
},
{
"ordinal": 28,
"ordinal": 29,
"name": "audio_bitrate",
"type_info": "Int4"
},
{
"ordinal": 29,
"ordinal": 30,
"name": "audio_bitrate_sampling: _",
"type_info": {
"Custom": {
@@ -284,7 +289,7 @@
}
},
{
"ordinal": 30,
"ordinal": 31,
"name": "audio_channels: _",
"type_info": {
"Custom": {
@@ -303,7 +308,7 @@
}
},
{
"ordinal": 31,
"ordinal": 32,
"name": "video_codec: _",
"type_info": {
"Custom": {
@@ -326,7 +331,7 @@
}
},
{
"ordinal": 32,
"ordinal": 33,
"name": "features!: _",
"type_info": {
"Custom": {
@@ -355,7 +360,7 @@
}
},
{
"ordinal": 33,
"ordinal": 34,
"name": "subtitle_languages!: _",
"type_info": {
"Custom": {
@@ -425,7 +430,7 @@
}
},
{
"ordinal": 34,
"ordinal": 35,
"name": "video_resolution!: _",
"type_info": {
"Custom": {
@@ -449,12 +454,12 @@
}
},
{
"ordinal": 35,
"ordinal": 36,
"name": "video_resolution_other_x",
"type_info": "Int4"
},
{
"ordinal": 36,
"ordinal": 37,
"name": "video_resolution_other_y",
"type_info": "Int4"
}
@@ -476,6 +481,7 @@
false,
false,
false,
false,
true,
true,
true,
@@ -504,5 +510,5 @@
true
]
},
"hash": "840f4fe4178c9746d4bb4258aed340b11fa64db9e1efe5e39cf4c927e0954c54"
"hash": "79b272dba5250f458e020b5b9ded8ba18a7f8af45f619ee7771ad88c4ab9a532"
}

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM peers\n WHERE (user_id, torrent_id, peer_id) IN (\n SELECT t.user_id, t.torrent_id, t.peer_id\n FROM (\n SELECT * FROM unnest(\n $1::int[],\n $2::int[],\n $3::bytea[]\n ) AS t(user_id, torrent_id, peer_id)\n ) AS t\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4Array",
"Int4Array",
"ByteaArray"
]
},
"nullable": []
},
"hash": "7da73662a96a68e239d011598ace3bc5b287a82c5b0c34ce9543842a1bed0ea4"
}

View File

@@ -0,0 +1,38 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n id,\n passkey as \"passkey: Passkey\",\n 0::INT AS \"num_seeding!\",\n 0::INT AS \"num_leeching!\"\n FROM users\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "passkey: Passkey",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "num_seeding!",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "num_leeching!",
"type_info": "Int4"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
null,
null
]
},
"hash": "bb66b1b13123112781db47d98cd02b28d11470b7c84d7aec56ec102fee20264d"
}

View File

@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE torrents\n SET\n seeders = seeders + updates.seeder_delta,\n leechers = leechers + updates.leecher_delta,\n times_completed = times_completed + updates.times_completed_delta\n FROM (\n SELECT * FROM unnest($1::int[], $2::bigint[], $3::bigint[], $4::bigint[]) AS\n t(torrent_id, seeder_delta, leecher_delta, times_completed_delta)\n ) AS updates\n WHERE torrents.id = updates.torrent_id\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4Array",
"Int8Array",
"Int8Array",
"Int8Array"
]
},
"nullable": []
},
"hash": "c45f235654a1b2aa8c849c5644443fe34ea7a4dd976fe6b4405e7b4a585a1325"
}

View File

@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n id,\n info_hash as \"info_hash: InfoHash\"\n FROM torrents\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "info_hash: InfoHash",
"type_info": "Bytea"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false
]
},
"hash": "d94c7cf9c02a4f060345d02ac4bd2434069fc46d43e6f3e7e3618737c2dcd547"
}

View File

@@ -0,0 +1,74 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n peers.ip AS \"ip_address: IpAddr\",\n peers.user_id AS \"user_id\",\n peers.torrent_id AS \"torrent_id\",\n peers.port AS \"port\",\n peers.seeder AS \"is_seeder: bool\",\n peers.active AS \"is_active: bool\",\n peers.updated_at AS \"updated_at: DateTime<Utc>\",\n peers.uploaded AS \"uploaded\",\n peers.downloaded AS \"downloaded\",\n peers.peer_id AS \"peer_id: PeerId\"\n FROM peers\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "ip_address: IpAddr",
"type_info": "Inet"
},
{
"ordinal": 1,
"name": "user_id",
"type_info": "Int4"
},
{
"ordinal": 2,
"name": "torrent_id",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "port",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "is_seeder: bool",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "is_active: bool",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "updated_at: DateTime<Utc>",
"type_info": "Timestamp"
},
{
"ordinal": 7,
"name": "uploaded",
"type_info": "Int8"
},
{
"ordinal": 8,
"name": "downloaded",
"type_info": "Int8"
},
{
"ordinal": 9,
"name": "peer_id: PeerId",
"type_info": "Bytea"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false,
false,
false,
true,
false,
false,
false
]
},
"hash": "f6d849721ff84614c129c14455d9a6adbe0ad29b7876963d5bd9015c0f73ba9d"
}

View File

@@ -1,6 +1,7 @@
use std::str::FromStr;
use actix_multipart::form::{bytes::Bytes, text::Text, MultipartForm};
use arcadia_shared::tracker::models::torrent::InfoHash;
use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -307,6 +308,7 @@ impl FromStr for Features {
#[derive(Debug, Serialize, FromRow, ToSchema)]
pub struct Torrent {
pub id: i32,
pub info_hash: InfoHash,
pub upload_factor: i16,
pub download_factor: i16,
pub seeders: i64,

View File

@@ -13,7 +13,8 @@ use arcadia_common::{
error::{Error, Result},
services::torrent_service::{get_announce_url, looks_like_url},
};
use bip_metainfo::{Info, InfoBuilder, InfoHash, Metainfo, MetainfoBuilder, PieceLength};
use arcadia_shared::tracker::models::torrent::InfoHash;
use bip_metainfo::{Info, InfoBuilder, Metainfo, MetainfoBuilder, PieceLength};
use serde_json::{json, Value};
use sqlx::PgPool;
use std::{borrow::Borrow, str::FromStr};
@@ -72,7 +73,7 @@ impl ConnectionPool {
.build(1, info, |_| {})
.map_err(|_| Error::TorrentFileInvalid)?;
let info_hash = InfoHash::from_bytes(&info_normalized);
let info_hash = bip_metainfo::InfoHash::from_bytes(&info_normalized);
// TODO: torrent metadata extraction should be done on the client side
let parent_folder = info.directory().map(|d| d.to_str().unwrap()).unwrap_or("");
@@ -206,7 +207,7 @@ impl ConnectionPool {
Torrent,
r#"
SELECT
id, upload_factor, download_factor, seeders, leechers,
id, info_hash as "info_hash: InfoHash", upload_factor, download_factor, seeders, leechers,
times_completed, snatched, edition_group_id, created_at, updated_at,
created_by_id,
deleted_at AS "deleted_at!: _",
@@ -270,7 +271,7 @@ impl ConnectionPool {
updated_at = NOW()
WHERE id = $1 AND deleted_at IS NULL
RETURNING
id, upload_factor, download_factor, seeders, leechers,
id, info_hash as "info_hash: InfoHash", upload_factor, download_factor, seeders, leechers,
times_completed, snatched, edition_group_id, created_at, updated_at,
created_by_id,
deleted_at AS "deleted_at!: _",

View File

@@ -16,3 +16,4 @@ ringmap = { version = "0.2.0", features = ["serde"] }
actix-web = "4"
log = "0.4"
parking_lot = "0.12.4"
utoipa = { version = "5.3.1", features = ["actix_extras"] }

View File

@@ -1,14 +1,19 @@
use anyhow::{bail, Context};
use chrono::{DateTime, Utc};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use sqlx::{Database, Decode, PgPool};
use sqlx::postgres::PgTypeInfo;
use sqlx::{Database, Decode, PgPool, Postgres, Type};
use std::net::IpAddr;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
use utoipa::ToSchema;
use crate::tracker::models::peer::{self, Peer};
use crate::tracker::models::peer_id::PeerId;
use crate::utils::hex_decode;
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, Hash, PartialEq, ToSchema)]
pub struct InfoHash(pub [u8; 20]);
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
@@ -22,6 +27,18 @@ pub struct Torrent {
pub peers: peer::Map,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct APIInsertTorrent {
pub id: u32,
pub info_hash: InfoHash,
pub is_deleted: bool,
pub seeders: u32,
pub leechers: u32,
pub times_completed: u32,
pub download_factor: u8,
pub upload_factor: u8,
}
#[derive(Debug)]
pub struct Map(pub IndexMap<u32, Torrent>);
@@ -144,6 +161,12 @@ impl DerefMut for Map {
}
}
impl Type<Postgres> for InfoHash {
fn type_info() -> PgTypeInfo {
<Vec<u8> as Type<Postgres>>::type_info()
}
}
impl<'r, DB: Database> Decode<'r, DB> for InfoHash
where
&'r [u8]: Decode<'r, DB>,
@@ -165,3 +188,23 @@ where
Ok(InfoHash(<[u8; 20]>::try_from(&value[0..20])?))
}
}
impl FromStr for InfoHash {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = s.as_bytes();
let mut out = [0u8; 20];
if bytes.len() != 40 {
bail!("`{s}` is not a valid infohash.");
}
for pos in 0..20 {
out[pos] = hex_decode([bytes[pos * 2], bytes[pos * 2 + 1]])
.context("`{s}` is not a valid infohash")?;
}
Ok(InfoHash(out))
}
}

View File

@@ -1,3 +1,5 @@
use anyhow::{bail, Result};
/// Encodes one byte into 2 ascii-encoded hex digits.
#[inline(always)]
pub fn hex_encode(char: u8) -> [u8; 2] {
@@ -16,3 +18,19 @@ pub fn hex_encode(char: u8) -> [u8; 2] {
},
]
}
/// Decodes two ascii-encoded hex digits into one byte.
#[inline(always)]
pub fn hex_decode(chars: [u8; 2]) -> Result<u8> {
Ok(match chars[0] {
digit @ b'0'..=b'9' => (digit - b'0') << 4,
lower @ b'a'..=b'f' => (lower - b'a' + 0xA) << 4,
upper @ b'A'..=b'F' => (upper - b'A' + 0xA) << 4,
_ => bail!("Invalid URL encoding."),
} + match chars[1] {
digit @ b'0'..=b'9' => digit - b'0',
lower @ b'a'..=b'f' => lower - b'a' + 0xA,
upper @ b'A'..=b'F' => upper - b'A' + 0xA,
_ => bail!("Invalid URL encoding."),
})
}

View File

@@ -1 +1,2 @@
pub mod announce;
pub mod torrents;

View File

@@ -0,0 +1 @@
pub mod upsert_torrent;

View File

@@ -0,0 +1,34 @@
use actix_web::{
web::{Data, Json},
HttpResponse,
};
use arcadia_shared::tracker::models::{
peer,
torrent::{APIInsertTorrent, Torrent},
};
use log::info;
use crate::Tracker;
pub async fn exec(arc: Data<Tracker>, torrent: Json<APIInsertTorrent>) -> HttpResponse {
info!("Inserting torrent with id {}.", torrent.id);
arc.torrents.lock().insert(
torrent.id,
Torrent {
is_deleted: torrent.is_deleted,
seeders: torrent.seeders,
leechers: torrent.leechers,
times_completed: torrent.times_completed,
download_factor: torrent.download_factor as i16,
upload_factor: torrent.upload_factor as i16,
peers: peer::Map::new(),
},
);
arc.infohash2id
.write()
.insert(torrent.info_hash, torrent.id);
HttpResponse::Ok().finish()
}

View File

@@ -3,6 +3,8 @@ use std::{collections::HashSet, str::FromStr};
#[derive(Debug, Envconfig, Clone)]
pub struct Env {
#[envconfig(from = "API_KEY")]
pub api_key: String,
#[envconfig(from = "ALLOWED_TORRENT_CLIENTS")]
pub allowed_torrent_clients: AllowedTorrentClientSet,
#[envconfig(from = "NUMWANT_DEFAULT")]

View File

@@ -1,35 +1,19 @@
// use actix_web::{dev::ServiceRequest, error::ErrorUnauthorized, web::Data};
// use actix_web::{Error, FromRequest, HttpRequest};
// use futures::future::{ready, Ready};
use actix_web::Error;
use actix_web::{dev::ServiceRequest, error::ErrorUnauthorized, web::Data};
// pub struct Passkey(pub String);
use crate::Tracker;
// impl FromRequest for Passkey {
// type Error = Error;
// type Future = Ready<Result<Self, Self::Error>>;
// fn from_request(req: &HttpRequest, _payload: &mut actix_web::dev::Payload) -> Self::Future {
// let passkey = req.path().into_inner();
// match passkey {
// Some(key) => ready(Ok(Passkey(key))),
// None => ready(Err(actix_web::error::ErrorUnauthorized(
// "authentication error: missing passkey",
// ))),
// }
// }
// }
// pub async fn authenticate_user(
// req: ServiceRequest,
// passkey: Passkey,
// ) -> std::result::Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {
// // if passkey.0 != arc.env.passkey {
// // Err((
// // ErrorUnauthorized("authentication error: invalid API key"),
// // req,
// // ))
// // } else {
// Ok(req)
// // }
// }
pub async fn authenticate_backend(
req: ServiceRequest,
_: (),
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
let api_key = &req
.app_data::<Data<Tracker>>()
.expect("app data set")
.env
.api_key;
match req.headers().get("x-api-key").and_then(|v| v.to_str().ok()) {
Some(k) if k == api_key => Ok(req),
_ => Err((ErrorUnauthorized("invalid or missing API key"), req)),
}
}

View File

@@ -1,14 +1,17 @@
use actix_web::web::{self, scope};
use actix_web::web::{self, put, resource, scope};
use crate::announce::handlers::announce::config as AnnouncesConfig;
// use actix_web_httpauth::middleware::HttpAuthentication;
// use crate::middleware::authenticate_user;
use crate::{
announce::handlers::{announce::config as AnnouncesConfig, torrents::upsert_torrent},
middleware::authenticate_backend,
};
use actix_web_httpauth::middleware::HttpAuthentication;
pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service(scope("{passkey}").configure(AnnouncesConfig));
// TODO: protect by only allowing requests from backend's ip
// cfg.service(
// .wrap(HttpAuthentication::with_fn(authenticate_user(req, passkey))),
// );
cfg.service(
web::scope("/api")
.wrap(HttpAuthentication::with_fn(authenticate_backend))
.service(resource("/torrents").route(put().to(upsert_torrent::exec))),
);
cfg.service(scope("{passkey}").configure(AnnouncesConfig));
}

View File

@@ -19,6 +19,7 @@ pub async fn create_test_app(
) -> impl Service<Request, Response = ServiceResponse, Error = Error> {
// Create a default env for testing
let env = Env {
api_key: "amazing_api_key".to_owned(),
allowed_torrent_clients: AllowedTorrentClientSet {
clients: vec![b"lt0F01-".to_vec(), b"qB".to_vec(), b"UTorrent".to_vec()]
.into_iter()