Skip to content

Commit cdc0c1c

Browse files
yakserVeryBigSad
andcommitted
Start async file uploading
Co-authored-by: VeryBigSad <VeryBigSad@users.noreply.github.com>
1 parent 92b13bc commit cdc0c1c

14 files changed

Lines changed: 594 additions & 2 deletions

File tree

.env.example

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,9 @@ DATABASE_NAME=
1212
DATABASE_USER=
1313
DATABASE_PASSWORD=
1414
DATABASE_HOST=
15-
DATABASE_PORT=
15+
DATABASE_PORT=
16+
17+
SELECTEL_ACCOUNT_ID=
18+
SELECTEL_CONTAINER_NAME=
19+
SELECTEL_CONTAINER_PASSWORD=
20+
SELECTEL_CONTAINER_USERNAME=

files/__init__.py

Whitespace-only changes.

files/admin.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from django.contrib import admin
2+
3+
from files.models import UserFile
4+
5+
6+
@admin.register(UserFile)
7+
class UserFileAdmin(admin.ModelAdmin):
8+
list_display = (
9+
"id",
10+
"link",
11+
"datetime_uploaded",
12+
)
13+
list_display_links = (
14+
"id",
15+
"link",
16+
)

files/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class FilesConfig(AppConfig):
5+
default_auto_field = "django.db.models.BigAutoField"
6+
name = "files"

files/migrations/0001_initial.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Generated by Django 4.1.2 on 2022-11-11 22:37
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
initial = True
11+
12+
dependencies = [
13+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name="UserFile",
19+
fields=[
20+
(
21+
"id",
22+
models.BigAutoField(
23+
auto_created=True,
24+
primary_key=True,
25+
serialize=False,
26+
verbose_name="ID",
27+
),
28+
),
29+
("link", models.URLField()),
30+
("datetime_uploaded", models.DateTimeField(auto_now_add=True)),
31+
(
32+
"user",
33+
models.ForeignKey(
34+
null=True,
35+
on_delete=django.db.models.deletion.SET_NULL,
36+
to=settings.AUTH_USER_MODEL,
37+
),
38+
),
39+
],
40+
),
41+
]

files/migrations/__init__.py

Whitespace-only changes.

files/models.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from django.db import models
2+
from django.contrib.auth import get_user_model
3+
4+
User = get_user_model()
5+
6+
7+
class UserFile(models.Model):
8+
"""
9+
UserFile model
10+
11+
Attributes:
12+
user: User who uploaded the file
13+
link: Link to the file on the CDN
14+
datetime_uploaded: Datetime when the file was uploaded
15+
"""
16+
17+
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
18+
link = models.URLField(null=False)
19+
datetime_uploaded = models.DateTimeField(auto_now_add=True)
20+
21+
def __str__(self):
22+
return f"UserFile<{self.id}> - {self.link}"

files/tests.py

Whitespace-only changes.

files/urls.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from django.urls import path
2+
3+
4+
from files.views import FileUploadView
5+
6+
app_name = "industries"
7+
8+
urlpatterns = [
9+
path("", FileUploadView.as_view()),
10+
]

files/views.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import asyncio
2+
import json
3+
from datetime import time
4+
from asgiref.sync import sync_to_async
5+
from aiohttp import ClientSession
6+
from django.db import transaction
7+
from django.utils.decorators import classonlymethod
8+
from rest_framework import permissions, status
9+
from rest_framework.response import Response
10+
from rest_framework.views import APIView
11+
12+
from files.models import UserFile
13+
from procollab.settings import (
14+
DEBUG,
15+
SELECTEL_ACCOUNT_ID,
16+
SELECTEL_CONTAINER_NAME,
17+
SELECTEL_CONTAINER_PASSWORD,
18+
SELECTEL_CONTAINER_USERNAME,
19+
)
20+
21+
22+
class FileUploadView(APIView):
23+
permission_classes = [permissions.AllowAny]
24+
25+
@classonlymethod
26+
def as_view(cls, **initkwargs):
27+
view = super().as_view(**initkwargs)
28+
view._is_coroutine = asyncio.coroutines._is_coroutine
29+
return view
30+
31+
@transaction.atomic
32+
async def post(self, request):
33+
file = request.FILES["file"]
34+
if DEBUG is True:
35+
return Response(
36+
{"message": "Files doesn't save in development mode, sorry <3"},
37+
status=status.HTTP_406_NOT_ACCEPTABLE,
38+
)
39+
40+
link = f"https://api.selcdn.ru/v1/SEL_{SELECTEL_ACCOUNT_ID}/{SELECTEL_CONTAINER_NAME}/"
41+
42+
user = request.user
43+
44+
# creates UserFile object in the database
45+
self._save_to_db(user, link)
46+
token = await self._get_token()
47+
async with ClientSession(headers={"X-Auth-Token": token}) as server:
48+
if len(file.split(".")) > 1:
49+
extension = file.filename.split(".")[1]
50+
else:
51+
extension = ""
52+
53+
# looks like /hashed_email/hashed_filename_hashed_time.extension
54+
url = (
55+
link + f"/{SELECTEL_CONTAINER_NAME}/{abs(hash(user.email))}/"
56+
f"{abs(hash(file.filename))}_{abs(hash(time.time()))}.{extension}"
57+
)
58+
59+
async with server.put(
60+
url,
61+
data=file.open(mode="rb").read(),
62+
) as response:
63+
if response.status != 201:
64+
return await Response(
65+
"Failed to upload file", status_code=status.HTTP_409_CONFLICT
66+
)
67+
return await Response({"url": url}, status=status.HTTP_201_CREATED)
68+
69+
@sync_to_async
70+
def _save_to_db(self, user, link):
71+
"""creates userfile object for file uploads"""
72+
return UserFile.objects.create(user=user, link=link)
73+
74+
async def _get_token(self):
75+
"""returns auth token for sentry"""
76+
async with ClientSession() as server:
77+
data = {
78+
"auth": {
79+
"identity": {
80+
"methods": ["password"],
81+
"password": {
82+
"user": {
83+
"id": SELECTEL_CONTAINER_USERNAME,
84+
"password": SELECTEL_CONTAINER_PASSWORD,
85+
}
86+
},
87+
}
88+
}
89+
}
90+
async with server.post(
91+
"https://api.selcdn.ru/v3/auth/tokens",
92+
data=json.dumps(data),
93+
) as response:
94+
if response.status != 201:
95+
return Response(
96+
"Failed to get token", status_code=status.HTTP_409_CONFLICT
97+
)
98+
return response.json()

0 commit comments

Comments
 (0)