vpn integration

This commit is contained in:
2026-04-11 22:07:59 +02:00
parent c4d27e9842
commit 00ac38d126
47 changed files with 945 additions and 749 deletions
+9 -4
View File
@@ -2,13 +2,18 @@ from django.apps import AppConfig
class TorrentConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'torrent'
default_auto_field = "django.db.models.BigAutoField"
name = "torrent"
def ready(self):
from django.db.models.signals import post_save, pre_delete, m2m_changed
from .signals import on_post_save_torrent, on_pre_delete_torrent, on_shared_user_changed
from django.db.models.signals import m2m_changed, post_save, pre_delete
from .models import Torrent
from .signals import (
on_post_save_torrent,
on_pre_delete_torrent,
on_shared_user_changed,
)
post_save.connect(on_post_save_torrent, sender=Torrent)
pre_delete.connect(on_pre_delete_torrent, sender=Torrent)
+36 -32
View File
@@ -1,11 +1,8 @@
from django.db.models import Q
from django.db.models.functions import Coalesce
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from typing import Optional, Union
import asyncio
from django.db.models import Q
from user.models import User
from .models import Torrent
@@ -14,17 +11,19 @@ class TorrentEventConsumer(AsyncJsonWebsocketConsumer):
super().__init__(*args, **kwargs)
self.channel_groups = set()
self.user: Optional[User] = None
self.follow_user: Optional[User] = None
self.user: User | None = None
self.follow_user: User | None = None
async def connect(self):
self.user = self.scope['user']
self.user = self.scope["user"]
if not self.user.is_authenticated:
await self.close()
return
self.follow_user = self.user
await self.channel_layer.group_add(f"user_{self.follow_user.id}", self.channel_name)
await self.channel_layer.group_add(
f"user_{self.follow_user.id}", self.channel_name
)
# user_id = int(self.scope['url_route']["kwargs"]["user_id"])
# if user_id == self.user.id:
@@ -53,47 +52,52 @@ class TorrentEventConsumer(AsyncJsonWebsocketConsumer):
print("call websocket not supported", content)
async def change_follow_user(self, user_id):
await self.channel_layer.group_discard(f"user_{self.follow_user.id}", self.channel_name)
await self.channel_layer.group_discard(
f"user_{self.follow_user.id}", self.channel_name
)
if user_id == self.user.id:
self.follow_user = self.user
await self.channel_layer.group_add(f"user_{self.follow_user.id}", self.channel_name)
await self.channel_layer.group_add(
f"user_{self.follow_user.id}", self.channel_name
)
return self.follow_user
elif await self.user.friends.filter(id=user_id).aexists():
self.follow_user = await User.objects.filter(id=user_id).aget()
await self.channel_layer.group_add(f"user_{self.follow_user.id}", self.channel_name)
await self.channel_layer.group_add(
f"user_{self.follow_user.id}", self.channel_name
)
return self.follow_user
else:
return None
async def transmission_data_updated(self, datas):
torrent_stats = datas["data"]
qs = (Torrent.objects
.filter(Q(user_id=self.follow_user.id) | Q(shared_users=self.follow_user.id))
.values_list("id", flat=True).distinct())
qs = (
Torrent.objects.filter(
Q(user_id=self.follow_user.id) | Q(shared_users=self.follow_user.id)
)
.values_list("id", flat=True)
.distinct()
)
torrent_ids = [i async for i in qs]
for hash_string, data in torrent_stats.items():
if hash_string in torrent_ids:
await self.send_json({
"context": "transmission_data_updated",
"data": data
})
await self.send_json(
{"context": "transmission_data_updated", "data": data}
)
async def add_torrent(self, data):
await self.send_json({
"context": "add_torrent",
"torrent_id": data["data"]
})
await self.send_json({"context": "add_torrent", "torrent_id": data["data"]})
async def remove_torrent(self, data):
await self.send_json({
"context": "remove_torrent",
"torrent_id": data["data"]
})
await self.send_json({"context": "remove_torrent", "torrent_id": data["data"]})
async def update_torrent(self, data):
await self.send_json({
"context": "update_torrent",
"torrent_id": data["data"]["torrent_id"],
"updated_fields": data["data"]["updated_fields"]
})
await self.send_json(
{
"context": "update_torrent",
"torrent_id": data["data"]["torrent_id"],
"updated_fields": data["data"]["updated_fields"],
}
)
@@ -1,17 +1,16 @@
from django.conf import settings
from django.utils import timezone
from django.core.management.base import BaseCommand
from django.db import close_old_connections
import time
import signal
import sys
import time
import traceback
from datetime import timedelta
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db import close_old_connections
from django.utils import timezone
from app.utils import send_sync_channel_message
from torrent.models import Torrent
from torrent.utils import transmission_handler
from app.utils import send_sync_channel_message
def update_transmission_data():
@@ -24,10 +23,11 @@ def update_transmission_data():
updated_torrents.append(torrent)
if updated_torrents:
Torrent.objects.bulk_update(updated_torrents, ["transmission_data"])
send_sync_channel_message("torrent", "transmission_data_updated", {
torrent.id: torrent.transmission_data
for torrent in updated_torrents
})
send_sync_channel_message(
"torrent",
"transmission_data_updated",
{torrent.id: torrent.transmission_data for torrent in updated_torrents},
)
def clean_old_torrents():
@@ -37,24 +37,16 @@ def clean_old_torrents():
print(f"delete torrent {torrent.name}")
torrent.delete()
def update_peer_port():
transmission_handler.update_vpn_port()
class Command(BaseCommand):
task_schedule = {
"update_transmission_data": {
"func": update_transmission_data,
"schedule": 5.0
},
"clean_old_torrents": {
"func": clean_old_torrents,
"schedule": 5.0
},
"update_peer_port": {
"func": update_peer_port,
"schedule": 10.0
}
"update_transmission_data": {"func": update_transmission_data, "schedule": 5.0},
"clean_old_torrents": {"func": clean_old_torrents, "schedule": 5.0},
"update_peer_port": {"func": update_peer_port, "schedule": 10.0},
}
histories = {}
run = True
@@ -66,7 +58,10 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS("start"))
while self.run:
for name, task in self.task_schedule.items():
if name not in self.histories or time.time() - self.histories[name] > task["schedule"]:
if (
name not in self.histories
or time.time() - self.histories[name] > task["schedule"]
):
self.call_func(name)
time.sleep(1)
+31 -16
View File
@@ -1,40 +1,55 @@
# Generated by Django 5.1.6 on 2025-03-04 23:41
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
dependencies = []
operations = [
migrations.CreateModel(
name='File',
name="File",
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('rel_name', models.TextField()),
('size', models.BigIntegerField()),
(
"id",
models.UUIDField(
default=uuid.uuid4, primary_key=True, serialize=False
),
),
("rel_name", models.TextField()),
("size", models.BigIntegerField()),
],
),
migrations.CreateModel(
name='SharedUser',
name="SharedUser",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateTimeField(auto_now_add=True)),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("date", models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='Torrent',
name="Torrent",
fields=[
('id', models.CharField(max_length=40, primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('date_added', models.DateTimeField(auto_now_add=True)),
('size', models.PositiveBigIntegerField()),
('transmission_data', models.JSONField(default=dict)),
(
"id",
models.CharField(max_length=40, primary_key=True, serialize=False),
),
("name", models.CharField(max_length=255)),
("date_added", models.DateTimeField(auto_now_add=True)),
("size", models.PositiveBigIntegerField()),
("transmission_data", models.JSONField(default=dict)),
],
),
]
+35 -19
View File
@@ -6,42 +6,58 @@ from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('torrent', '0001_initial'),
("torrent", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='shareduser',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
model_name="shareduser",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
migrations.AddField(
model_name='torrent',
name='shared_users',
field=models.ManyToManyField(blank=True, related_name='torrents_shares', through='torrent.SharedUser', to=settings.AUTH_USER_MODEL),
model_name="torrent",
name="shared_users",
field=models.ManyToManyField(
blank=True,
related_name="torrents_shares",
through="torrent.SharedUser",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name='torrent',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='torrents', to=settings.AUTH_USER_MODEL),
model_name="torrent",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="torrents",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name='shareduser',
name='torrent',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='torrent.torrent'),
model_name="shareduser",
name="torrent",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="torrent.torrent"
),
),
migrations.AddField(
model_name='file',
name='torrent',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='torrent.torrent'),
model_name="file",
name="torrent",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="files",
to="torrent.torrent",
),
),
migrations.AlterUniqueTogether(
name='shareduser',
unique_together={('user', 'torrent')},
name="shareduser",
unique_together={("user", "torrent")},
),
]
@@ -4,15 +4,14 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('torrent', '0002_initial'),
("torrent", "0002_initial"),
]
operations = [
migrations.AddField(
model_name='torrent',
name='date_modified',
model_name="torrent",
name="date_modified",
field=models.DateTimeField(auto_now=True),
),
]
@@ -4,15 +4,14 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('torrent', '0003_torrent_date_modified'),
("torrent", "0003_torrent_date_modified"),
]
operations = [
migrations.RenameField(
model_name='shareduser',
old_name='date',
new_name='date_created',
model_name="shareduser",
old_name="date",
new_name="date_created",
),
]
@@ -4,15 +4,14 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('torrent', '0004_rename_date_shareduser_date_created'),
("torrent", "0004_rename_date_shareduser_date_created"),
]
operations = [
migrations.RenameField(
model_name='torrent',
old_name='date_added',
new_name='date_created',
model_name="torrent",
old_name="date_added",
new_name="date_created",
),
]
+16 -17
View File
@@ -1,12 +1,11 @@
from django.db import models
from django.conf import settings
import mimetypes
import uuid
from functools import cached_property
from pathlib import Path
from urllib.parse import quote
import mimetypes
import uuid
import shlex
from django.conf import settings
from django.db import models
class Torrent(models.Model):
@@ -14,8 +13,12 @@ class Torrent(models.Model):
name = models.CharField(max_length=255)
date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
user = models.ForeignKey("user.User", on_delete=models.CASCADE, related_name="torrents")
shared_users = models.ManyToManyField("user.User", related_name="torrents_shares", blank=True, through="SharedUser")
user = models.ForeignKey(
"user.User", on_delete=models.CASCADE, related_name="torrents"
)
shared_users = models.ManyToManyField(
"user.User", related_name="torrents_shares", blank=True, through="SharedUser"
)
size = models.PositiveBigIntegerField()
transmission_data = models.JSONField(default=dict)
@@ -35,10 +38,7 @@ class Torrent(models.Model):
@cached_property
def related_users(self):
return [
self.user_id,
*self.shared_users.values_list("id", flat=True)
]
return [self.user_id, *self.shared_users.values_list("id", flat=True)]
class SharedUser(models.Model):
@@ -50,7 +50,6 @@ class SharedUser(models.Model):
unique_together = ("user", "torrent")
class File(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
torrent = models.ForeignKey("Torrent", models.CASCADE, related_name="files")
@@ -86,7 +85,7 @@ class File(models.Model):
def is_video(self):
if self.mime_types.startswith("video/"):
return True
video_extensions = ['.mp4', '.flv', '.webm', '.avi', '.mkv', '.mov', '.wmv']
video_extensions = [".mp4", ".flv", ".webm", ".avi", ".mkv", ".mov", ".wmv"]
return self.pathname.suffix.lower() in video_extensions
@property
@@ -95,13 +94,13 @@ class File(models.Model):
encoded_parts = []
for part in self.pathname.parts:
# Ignorer un slash initial si présent
if part == '/' or part == '\\':
if part == "/" or part == "\\":
continue
encoded_parts.append(quote(part))
# Construction du chemin final avec le préfixe Nginx
if settings.NGINX_ACCEL_BASE.endswith('/'):
base = settings.NGINX_ACCEL_BASE.rstrip('/')
if settings.NGINX_ACCEL_BASE.endswith("/"):
base = settings.NGINX_ACCEL_BASE.rstrip("/")
else:
base = settings.NGINX_ACCEL_BASE
+3 -4
View File
@@ -1,15 +1,14 @@
from django.urls import reverse
from django.utils.text import slugify
from rest_framework import serializers
from user.serializers import UserSerializer
from .models import Torrent, File
from .models import File, Torrent
class TorrentSerializer(serializers.ModelSerializer):
count_files = serializers.IntegerField(read_only=True, source="len_files")
download_url = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Torrent
fields = "__all__"
@@ -32,4 +31,4 @@ class FileSerializer(serializers.ModelSerializer):
return reverse("torrent:download_file", kwargs={"file_id": obj.id})
def get_flux_url(self, obj: File):
return f'{reverse("torrent:flux_file", kwargs={"file_id": obj.id})}#{slugify(obj.filename)}'
return f"{reverse('torrent:flux_file', kwargs={'file_id': obj.id})}#{slugify(obj.filename)}"
+32 -11
View File
@@ -1,11 +1,14 @@
from app.utils import send_sync_channel_message
from .models import Torrent
from .utils import transmission_handler
from .models import Torrent, SharedUser
def on_post_save_torrent(instance: Torrent, created, **kwargs):
if created:
send_sync_channel_message(f"user_{instance.user_id}", "add_torrent", instance.id)
send_sync_channel_message(
f"user_{instance.user_id}", "add_torrent", instance.id
)
def on_pre_delete_torrent(instance: Torrent, **kwargs):
@@ -25,20 +28,38 @@ def on_shared_user_changed(sender, instance: Torrent, action, pk_set, **kwargs):
for user_id in pk_set:
send_sync_channel_message(f"user_{user_id}", "add_torrent", instance.id)
for user_id in instance.related_users:
send_sync_channel_message(f"user_{user_id}", "update_torrent", {
"torrent_id": instance.id,
"updated_fields": {"shared_users": list(instance.shared_users.all().values_list("id", flat=True))}
})
send_sync_channel_message(
f"user_{user_id}",
"update_torrent",
{
"torrent_id": instance.id,
"updated_fields": {
"shared_users": list(
instance.shared_users.all().values_list("id", flat=True)
)
},
},
)
case "pre_remove":
pass
case "post_remove":
for user_id in pk_set:
send_sync_channel_message(f"user_{user_id}", "remove_torrent", instance.id)
send_sync_channel_message(
f"user_{user_id}", "remove_torrent", instance.id
)
for user_id in instance.related_users:
send_sync_channel_message(f"user_{user_id}", "update_torrent", {
"torrent_id": instance.id,
"updated_fields": {"shared_users": list(instance.shared_users.all().values_list("id", flat=True))}
})
send_sync_channel_message(
f"user_{user_id}",
"update_torrent",
{
"torrent_id": instance.id,
"updated_fields": {
"shared_users": list(
instance.shared_users.all().values_list("id", flat=True)
)
},
},
)
case "pre_clear":
pass
case "post_clear":
+8 -10
View File
@@ -1,12 +1,10 @@
from django.db import close_old_connections
from celery import shared_task
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
from .models import Torrent, File
from .utils import transmission_handler
from app.utils import send_sync_channel_message
from .models import Torrent
from .utils import transmission_handler
@shared_task
def update_transmission_data():
@@ -19,8 +17,8 @@ def update_transmission_data():
updated_torrents.append(torrent)
if updated_torrents:
Torrent.objects.bulk_update(updated_torrents, ["transmission_data"])
send_sync_channel_message("torrent", "transmission_data_updated", {
torrent.id: torrent.transmission_data
for torrent in updated_torrents
})
send_sync_channel_message(
"torrent",
"transmission_data_updated",
{torrent.id: torrent.transmission_data for torrent in updated_torrents},
)
+81 -111
View File
@@ -1,39 +1,34 @@
from unittest.mock import MagicMock, patch
from django.conf import settings
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from django.conf import settings
from unittest.mock import patch, MagicMock
from rest_framework.test import APIClient, APITestCase
from .models import Torrent, SharedUser, File
from user.models import User
from .views import TorrentViewSet, FileViewSet
from .utils import Transmission, torrent_proceed, torrent_share
from .models import File, SharedUser, Torrent
from .utils import Transmission, torrent_proceed
class TorrentModelTestCase(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpassword'
username="testuser", email="test@example.com", password="testpassword"
)
self.torrent = Torrent.objects.create(
id='abc123',
name='Test Torrent',
id="abc123",
name="Test Torrent",
user=self.user,
size=1000,
transmission_data={}
transmission_data={},
)
self.shared_user = User.objects.create_user(
username='shareduser',
email='shared@example.com',
password='sharedpassword'
username="shareduser", email="shared@example.com", password="sharedpassword"
)
self.file = File.objects.create(
torrent=self.torrent,
rel_name='test_file.txt',
size=100
torrent=self.torrent, rel_name="test_file.txt", size=100
)
def test_len_files(self):
@@ -41,15 +36,11 @@ class TorrentModelTestCase(TestCase):
self.assertEqual(self.torrent.len_files, 1)
# Add another file and test again
File.objects.create(
torrent=self.torrent,
rel_name='another_file.txt',
size=200
)
File.objects.create(torrent=self.torrent, rel_name="another_file.txt", size=200)
# Clear cached_property
if hasattr(self.torrent, '_len_files'):
delattr(self.torrent, '_len_files')
if hasattr(self.torrent, "_len_files"):
delattr(self.torrent, "_len_files")
self.assertEqual(self.torrent.len_files, 2)
@@ -62,8 +53,8 @@ class TorrentModelTestCase(TestCase):
self.torrent.shared_users.add(self.shared_user)
# Clear cached_property
if hasattr(self.torrent, '_related_users'):
delattr(self.torrent, '_related_users')
if hasattr(self.torrent, "_related_users"):
delattr(self.torrent, "_related_users")
# Should include both users now
self.assertIn(self.user.id, self.torrent.related_users)
@@ -73,30 +64,26 @@ class TorrentModelTestCase(TestCase):
class FileModelTestCase(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpassword'
username="testuser", email="test@example.com", password="testpassword"
)
self.torrent = Torrent.objects.create(
id='abc123',
name='Test Torrent',
id="abc123",
name="Test Torrent",
user=self.user,
size=1000,
transmission_data={}
transmission_data={},
)
self.file = File.objects.create(
torrent=self.torrent,
rel_name='test/path/file.mp4',
size=100
torrent=self.torrent, rel_name="test/path/file.mp4", size=100
)
def test_pathname(self):
"""Test the pathname property returns the correct path"""
self.assertEqual(str(self.file.pathname), 'test/path/file.mp4')
self.assertEqual(str(self.file.pathname), "test/path/file.mp4")
def test_filename(self):
"""Test the filename property returns the correct filename"""
self.assertEqual(self.file.filename, 'file.mp4')
self.assertEqual(self.file.filename, "file.mp4")
def test_abs_pathname(self):
"""Test the abs_pathname property returns the correct absolute path"""
@@ -109,9 +96,7 @@ class FileModelTestCase(TestCase):
# Test non-video file
non_video_file = File.objects.create(
torrent=self.torrent,
rel_name='test/path/document.pdf',
size=50
torrent=self.torrent, rel_name="test/path/document.pdf", size=50
)
self.assertFalse(non_video_file.is_video)
@@ -119,29 +104,22 @@ class FileModelTestCase(TestCase):
class SharedUserModelTestCase(TestCase):
def setUp(self):
self.owner = User.objects.create_user(
username='owner',
email='owner@example.com',
password='ownerpassword'
username="owner", email="owner@example.com", password="ownerpassword"
)
self.shared_user = User.objects.create_user(
username='shareduser',
email='shared@example.com',
password='sharedpassword'
username="shareduser", email="shared@example.com", password="sharedpassword"
)
self.torrent = Torrent.objects.create(
id='abc123',
name='Test Torrent',
id="abc123",
name="Test Torrent",
user=self.owner,
size=1000,
transmission_data={}
transmission_data={},
)
def test_shared_user_creation(self):
"""Test creating a shared user relationship"""
shared = SharedUser.objects.create(
user=self.shared_user,
torrent=self.torrent
)
shared = SharedUser.objects.create(user=self.shared_user, torrent=self.torrent)
self.assertEqual(shared.user, self.shared_user)
self.assertEqual(shared.torrent, self.torrent)
@@ -152,109 +130,99 @@ class SharedUserModelTestCase(TestCase):
class TorrentViewSetTestCase(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpassword'
username="testuser", email="test@example.com", password="testpassword"
)
self.client = APIClient()
self.client.force_authenticate(user=self.user)
self.torrent = Torrent.objects.create(
id='abc123',
name='Test Torrent',
id="abc123",
name="Test Torrent",
user=self.user,
size=1000,
transmission_data={}
transmission_data={},
)
self.file = File.objects.create(
torrent=self.torrent,
rel_name='test_file.txt',
size=100
torrent=self.torrent, rel_name="test_file.txt", size=100
)
def test_list_torrents(self):
"""Test listing torrents"""
url = reverse('torrent-list')
url = reverse("torrent-list")
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['id'], self.torrent.id)
self.assertEqual(response.data[0]["id"], self.torrent.id)
def test_retrieve_torrent(self):
"""Test retrieving a specific torrent"""
url = reverse('torrent-detail', args=[self.torrent.id])
url = reverse("torrent-detail", args=[self.torrent.id])
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['id'], self.torrent.id)
self.assertEqual(response.data['name'], 'Test Torrent')
self.assertEqual(response.data["id"], self.torrent.id)
self.assertEqual(response.data["name"], "Test Torrent")
@patch('torrent.views.torrent_share')
@patch("torrent.views.torrent_share")
def test_share_torrent(self, mock_torrent_share):
"""Test sharing a torrent with another user"""
mock_torrent_share.return_value = True
shared_user = User.objects.create_user(
username='shareduser',
email='shared@example.com',
password='sharedpassword'
username="shareduser", email="shared@example.com", password="sharedpassword"
)
url = reverse('torrent-share', args=[self.torrent.id])
response = self.client.post(url, {'user_id': shared_user.id})
url = reverse("torrent-share", args=[self.torrent.id])
response = self.client.post(url, {"user_id": shared_user.id})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data['success'])
self.assertTrue(response.data["success"])
mock_torrent_share.assert_called_once()
class FileViewSetTestCase(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpassword'
username="testuser", email="test@example.com", password="testpassword"
)
self.client = APIClient()
self.client.force_authenticate(user=self.user)
self.torrent = Torrent.objects.create(
id='abc123',
name='Test Torrent',
id="abc123",
name="Test Torrent",
user=self.user,
size=1000,
transmission_data={}
transmission_data={},
)
self.file = File.objects.create(
torrent=self.torrent,
rel_name='test_file.txt',
size=100
torrent=self.torrent, rel_name="test_file.txt", size=100
)
def test_list_files(self):
"""Test listing files"""
url = reverse('file-list')
url = reverse("file-list")
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_retrieve_file(self):
"""Test retrieving a specific file"""
url = reverse('file-detail', args=[self.file.id])
url = reverse("file-detail", args=[self.file.id])
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['id'], str(self.file.id))
self.assertEqual(response.data['rel_name'], 'test_file.txt')
self.assertEqual(response.data["id"], str(self.file.id))
self.assertEqual(response.data["rel_name"], "test_file.txt")
class TransmissionUtilsTestCase(TestCase):
@patch('torrent.utils.Client')
@patch("torrent.utils.Client")
def test_transmission_init(self, mock_client):
"""Test Transmission class initialization"""
transmission = Transmission()
mock_client.assert_called_once_with(**settings.TRANSMISSION)
@patch('torrent.utils.Client')
@patch("torrent.utils.Client")
def test_add_torrent(self, mock_client):
"""Test adding a torrent"""
mock_instance = mock_client.return_value
@@ -267,62 +235,64 @@ class TransmissionUtilsTestCase(TestCase):
mock_instance.add_torrent.assert_called_once_with(file_obj)
self.assertEqual(result, mock_instance.add_torrent.return_value)
@patch('torrent.utils.Client')
@patch("torrent.utils.Client")
def test_get_data(self, mock_client):
"""Test getting torrent data"""
mock_instance = mock_client.return_value
mock_torrent = MagicMock()
mock_torrent.progress = 50
mock_torrent.fields = {'name': 'Test', 'size': 1000}
mock_torrent.fields = {"name": "Test", "size": 1000}
mock_instance.get_torrent.return_value = mock_torrent
transmission = Transmission()
result = transmission.get_data('hash123')
result = transmission.get_data("hash123")
mock_instance.get_torrent.assert_called_once_with('hash123', transmission.trpc_args)
self.assertEqual(result['progress'], 50)
self.assertEqual(result['name'], 'Test')
self.assertEqual(result['size'], 1000)
mock_instance.get_torrent.assert_called_once_with(
"hash123", transmission.trpc_args
)
self.assertEqual(result["progress"], 50)
self.assertEqual(result["name"], "Test")
self.assertEqual(result["size"], 1000)
class TorrentProceedTestCase(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpassword',
max_size=10000
username="testuser",
email="test@example.com",
password="testpassword",
max_size=10000,
)
@patch('torrent.utils.transmission_handler')
@patch("torrent.utils.transmission_handler")
def test_torrent_proceed_size_exceed(self, mock_transmission):
"""Test torrent_proceed when user size is exceeded"""
# Set user's used size to exceed max_size
self.user.max_size = 100
Torrent.objects.create(
id='abc123',
name='Test Torrent',
id="abc123",
name="Test Torrent",
user=self.user,
size=200, # Exceeds max_size
transmission_data={}
transmission_data={},
)
file_obj = MagicMock()
result = torrent_proceed(self.user, file_obj)
self.assertEqual(result['status'], 'error')
self.assertEqual(result['message'], 'Size exceed')
self.assertEqual(result["status"], "error")
self.assertEqual(result["message"], "Size exceed")
mock_transmission.add_torrent.assert_not_called()
@patch('torrent.utils.transmission_handler')
@patch("torrent.utils.transmission_handler")
def test_torrent_proceed_transmission_error(self, mock_transmission):
"""Test torrent_proceed when transmission raises an error"""
from transmission_rpc.error import TransmissionError
mock_transmission.add_torrent.side_effect = TransmissionError('Test error')
mock_transmission.add_torrent.side_effect = TransmissionError("Test error")
file_obj = MagicMock()
result = torrent_proceed(self.user, file_obj)
self.assertEqual(result['status'], 'error')
self.assertEqual(result['message'], 'Transmission Error')
self.assertEqual(result["status"], "error")
self.assertEqual(result["message"], "Transmission Error")
+4 -2
View File
@@ -1,12 +1,14 @@
from django.urls import path
from .views import HomeView, download_file, download_torrent, pping, flux_file
from .views import HomeView, download_file, download_torrent, flux_file, pping
app_name = "torrent"
urlpatterns = [
path("", HomeView.as_view(), name="home"),
path("pping/", pping, name="pping"),
path("download_file/<uuid:file_id>", download_file, name="download_file"),
path("download_torrent/<str:torrent_id>", download_torrent, name="download_torrent"),
path(
"download_torrent/<str:torrent_id>", download_torrent, name="download_torrent"
),
path("flux_file/<uuid:file_id>", flux_file, name="flux_file"),
]
+63 -37
View File
@@ -1,24 +1,38 @@
import os
from django.conf import settings
import traceback
import base64
import io
import os
import traceback
from django.conf import settings
from transmission_rpc import Client
from transmission_rpc.error import TransmissionError
# from app.utils import send_sync_channel_message
from .models import Torrent, File
from user.models import User
# from app.utils import send_sync_channel_message
from .models import File, Torrent
class Transmission:
trpc_args = [
"id", "percentDone", "uploadRatio", "rateUpload", "rateDownload", "hashString", "status", "sizeWhenDone",
"leftUntilDone", "name", "eta", "totalSize", "uploadedEver", "peersGettingFromUs", "peersSendingToUs",
"tracker", "trackerStats", "activityDate"
"id",
"percentDone",
"uploadRatio",
"rateUpload",
"rateDownload",
"hashString",
"status",
"sizeWhenDone",
"leftUntilDone",
"name",
"eta",
"totalSize",
"uploadedEver",
"peersGettingFromUs",
"peersSendingToUs",
"tracker",
"trackerStats",
"activityDate",
]
def __init__(self):
@@ -31,7 +45,12 @@ class Transmission:
if os.path.exists(port_file):
try:
with open(port_file) as f:
vpn_port = int(f.read().strip())
content = f.read().strip()
if (
not content
): # Si le fichier est vide, on attend la prochaine itération
return
vpn_port = int(content)
# Récupère le port actuel configuré dans Transmission
current_settings = self.client.get_session()
@@ -55,15 +74,15 @@ class Transmission:
def get_data(self, hash_string):
data = self.client.get_torrent(hash_string, self.trpc_args)
return {
"progress": data.progress,
"status_str": data.status,
**data.fields
}
return {"progress": data.progress, "status_str": data.status, **data.fields}
def get_all_data(self, hash_strings=None):
return {
data.hashString: {"progress": data.progress, "status_str": data.status, **data.fields}
data.hashString: {
"progress": data.progress,
"status_str": data.status,
**data.fields,
}
for data in self.client.get_torrents(hash_strings, self.trpc_args)
}
@@ -84,7 +103,7 @@ class Transmission:
port_file = "/tmp/gluetun/forwarded_port"
vpn_port = None
if os.path.exists(port_file):
with open(port_file, "r") as f:
with open(port_file) as f:
vpn_port = f.read().strip()
# 2. Test de connectivité du port (via l'API Transmission)
@@ -106,11 +125,7 @@ transmission_handler = Transmission()
def torrent_proceed(user, file, file_mode="file_object"):
r = {
"torrent": None,
"status": "error",
"message": "Unexpected error"
}
r = {"torrent": None, "status": "error", "message": "Unexpected error"}
user: User
if user.size_used > user.max_size:
@@ -149,16 +164,18 @@ def torrent_proceed(user, file, file_mode="file_object"):
name=data["name"],
user=user,
size=data["totalSize"],
transmission_data=data
transmission_data=data,
)
File.objects.bulk_create(
[
File(
torrent=torrent,
rel_name=file.name,
size=file.size,
)
for file in transmission_handler.get_files(torrent.id)
]
)
File.objects.bulk_create([
File(
torrent=torrent,
rel_name=file.name,
size=file.size,
)
for file in transmission_handler.get_files(torrent.id)
])
r["torrent"] = torrent
r["status"] = "success"
@@ -167,13 +184,22 @@ def torrent_proceed(user, file, file_mode="file_object"):
def torrent_share(torrent, current_user, target_user_id):
from .models import Torrent, SharedUser
from .models import SharedUser
torrent: Torrent
if (torrent.user_id != target_user_id and
any([torrent.user == current_user, torrent.shared_users.filter(id=current_user.id)]) and
not SharedUser.objects.filter(torrent_id=torrent.id, user_id=target_user_id).exists()):
if (
torrent.user_id != target_user_id
and any(
[
torrent.user == current_user,
torrent.shared_users.filter(id=current_user.id),
]
)
and not SharedUser.objects.filter(
torrent_id=torrent.id, user_id=target_user_id
).exists()
):
torrent.shared_users.add(target_user_id)
return True
return False
return False
+43 -37
View File
@@ -1,21 +1,20 @@
import anyio
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Count, OuterRef, Q, Sum
from django.db.models.functions import Coalesce
from django.http import Http404, HttpResponse, StreamingHttpResponse
from django.shortcuts import redirect
from django.urls import reverse
from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q, Count, OuterRef, Sum
from django.db.models.functions import Coalesce
from django.http import HttpResponse, Http404, StreamingHttpResponse
from rest_framework.viewsets import GenericViewSet
from rest_framework import mixins
from rest_framework.response import Response
from rest_framework.decorators import action
import anyio
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from app.utils import StreamingZipFileResponse
from user.models import User
from .models import Torrent, File, SharedUser
from .serializers import TorrentSerializer, FileSerializer
from .models import File, SharedUser, Torrent
from .serializers import FileSerializer, TorrentSerializer
from .utils import torrent_proceed, torrent_share
@@ -35,7 +34,7 @@ async def download_file(request, file_id):
| Q(torrent__user__friends=user)
| Q(torrent__shared_users__friends=user),
torrent__transmission_data__progress__gte=100,
pk=file_id
pk=file_id,
).distinct()
try:
@@ -44,10 +43,12 @@ async def download_file(request, file_id):
raise Http404()
else:
if int(request.GET.get("dl_hotfix", 0)) == 1:
async def read_file():
async with await anyio.open_file(file.abs_pathname, "rb") as f:
while chunk := await f.read(128 * 1024):
yield chunk
response = StreamingHttpResponse(read_file())
response["Content-Length"] = file.size
response["Content-Type"] = "application/octet-stream"
@@ -86,7 +87,7 @@ async def secured_flux_file(request, file_id):
| Q(torrent__user__friends=user)
| Q(torrent__shared_users__friends=user),
torrent__transmission_data__progress__gte=100,
pk=file_id
pk=file_id,
).distinct()
try:
@@ -105,39 +106,42 @@ async def secured_flux_file(request, file_id):
async def download_torrent(request, torrent_id):
# py version
user = await request.auser()
qs = Torrent.objects.filter(
Q(user=user)
| Q(shared_users=user)
| Q(user__friends=user)
| Q(shared_users__friends=user),
transmission_data__progress__gte=100,
pk=torrent_id
).annotate(count_files=Count("files")).distinct()
qs = (
Torrent.objects.filter(
Q(user=user)
| Q(shared_users=user)
| Q(user__friends=user)
| Q(shared_users__friends=user),
transmission_data__progress__gte=100,
pk=torrent_id,
)
.annotate(count_files=Count("files"))
.distinct()
)
torrent = await qs.aget()
if await torrent.alen_files == 1:
file = await torrent.files.afirst()
return redirect(reverse("torrent:download_file", kwargs={
"file_id": file.pk
}))
return redirect(reverse("torrent:download_file", kwargs={"file_id": file.pk}))
response = StreamingZipFileResponse(
filename=f"{torrent.name}.zip",
file_list=[
(file.abs_pathname, file.rel_name)
async for file in torrent.files.all()
(file.abs_pathname, file.rel_name) async for file in torrent.files.all()
],
is_async=True
is_async=True,
)
return response
class TorrentViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
class TorrentViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet,
):
queryset = Torrent.objects.all().annotate(count_files=Count("files"))
serializer_class = TorrentSerializer
@@ -158,7 +162,9 @@ class TorrentViewSet(mixins.CreateModelMixin,
else:
user_id = self.request.user.id
sub = SharedUser.objects.filter(torrent_id=OuterRef("pk"), user_id=user_id).values("date_created")
sub = SharedUser.objects.filter(
torrent_id=OuterRef("pk"), user_id=user_id
).values("date_created")
qs = qs.annotate(last_date=Coalesce(sub, "date_created")).order_by("-last_date")
search = self.request.query_params.get("search", None)
@@ -188,7 +194,9 @@ class TorrentViewSet(mixins.CreateModelMixin,
def share(self, request, pk):
user_id = self.request.data.get("user_id")
torrent = self.get_object()
is_share_success = torrent_share(torrent=torrent, current_user=self.request.user, target_user_id=user_id)
is_share_success = torrent_share(
torrent=torrent, current_user=self.request.user, target_user_id=user_id
)
return Response({"success": is_share_success})
@action(methods=["get"], detail=False)
@@ -196,9 +204,7 @@ class TorrentViewSet(mixins.CreateModelMixin,
Torrent.objects.filter(user=self.request.user).aggregate(total_size=Sum("size"))
class FileViewSet(mixins.RetrieveModelMixin,
mixins.ListModelMixin,
GenericViewSet):
class FileViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, GenericViewSet):
queryset = File.objects.all()
serializer_class = FileSerializer
filterset_fields = ["torrent"]