Skip to content

Commit bdd6953

Browse files
committed
Merge branch 'dev' into feature/auto-project-covers
2 parents 8e0843e + 9b784a0 commit bdd6953

26 files changed

Lines changed: 871 additions & 323 deletions

.github/workflows/dev-ci.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: 'Deploy dev server'
2+
3+
on:
4+
push:
5+
branches:
6+
- dev
7+
8+
jobs:
9+
deploy:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: run on server
13+
uses: garygrossgarten/github-action-ssh@release
14+
with:
15+
host: ${{ secrets.DEV_SERVER_HOST }}
16+
username: ${{ secrets.DEV_SERVER_USER }}
17+
password: ${{ secrets.DEV_SERVER_PASSWORD }}
18+
command: |
19+
cd /root/api &&
20+
git checkout dev &&
21+
git pull &&
22+
23+
rm -f .env &&
24+
touch .env &&
25+
26+
echo "DJANGO_SECRET_KEY=${{ secrets.DEV_DJANGO_SECRET_KEY }}" >> .env &&
27+
28+
echo "DATABASE_NAME=${{ secrets.DEV_DATABASE_NAME }}" >> .env &&
29+
echo "DATABASE_PASSWORD=${{ secrets.DEV_DATABASE_PASSWORD }}" >> .env &&
30+
echo "DATABASE_USER=${{ secrets.DEV_DATABASE_USER }}" >> .env &&
31+
echo "DATABASE_HOST=${{ secrets.DEV_DATABASE_HOST }}" >> .env &&
32+
echo "DATABASE_PORT=${{ secrets.DEV_DATABASE_PORT }}" >> .env &&
33+
34+
echo "EMAIL_USER=${{ secrets.EMAIL_USER }}" >> .env &&
35+
echo "EMAIL_PASSWORD=${{ secrets.EMAIL_PASSWORD }}" >> .env &&
36+
echo "EMAIL_HOST=${{ secrets.EMAIL_HOST }}" >> .env &&
37+
echo "EMAIL_PORT=${{ secrets.EMAIL_PORT }}" >> .env &&
38+
echo "SELECTEL_ACCOUNT_ID=${{ secrets.SELECTEL_ACCOUNT_ID }}" >> .env &&
39+
echo "SELECTEL_CONTAINER_NAME=${{ secrets.SELECTEL_CONTAINER_NAME }}" >> .env &&
40+
echo "SELECTEL_CONTAINER_PASSWORD=${{ secrets.SELECTEL_CONTAINER_PASSWORD }}" >> .env &&
41+
echo "SELECTEL_CONTAINER_USERNAME=${{ secrets.SELECTEL_CONTAINER_USERNAME }}" >> .env &&
42+
43+
echo "CLICKUP_API_TOKEN=${{ secrets.CLICKUP_API_TOKEN }}" >> .env &&
44+
echo "CLICKUP_SPACE_ID=${{ secrets.CLICKUP_SPACE_ID }}" >> .env &&
45+
46+
echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env &&
47+
48+
docker-compose -f docker-compose.dev-ci.yml up -d --build --force-recreate

docker-compose.dev-ci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
version: "3.4"
2+
services:
3+
server:
4+
ports:
5+
- "8000:8000"
6+
build:
7+
context: .
8+
dockerfile: Dockerfile
9+
env_file:
10+
- .env
11+
restart: always
12+
networks:
13+
template-network:
14+
15+
networks:
16+
template-network:
17+
18+
volumes:
19+
db-volume:

events/signals.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,9 @@ def autoposting_receiver(sender, instance, created, *args, **kwargs):
1616
if response["ok"]:
1717
instance.tg_message_id = response["result"]["message_id"]
1818
instance.save()
19-
# print(response) -
2019
else:
2120
link = f"{APP_URL}/{instance.pk}"
2221
text = f"***{instance.title}***\n{instance.short_text}\n\n{link}"
2322
response = edit_message(
2423
text, instance.tg_message_id, settings.TELEGRAM_CHANNEL
2524
)
26-
# print(response) -

files/admin.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django.contrib import admin
44
from django.forms import ModelForm, FileField
55

6-
from files.helpers import FileAPI
6+
from files.service import CDN, SelectelSwiftStorage
77
from files.models import UserFile
88

99

@@ -40,6 +40,9 @@ class UserFileAdmin(admin.ModelAdmin):
4040
)
4141

4242
date_hierarchy = "datetime_uploaded"
43+
ordering = ("-datetime_uploaded",)
44+
45+
cdn = CDN(storage=SelectelSwiftStorage())
4346

4447
@admin.display(empty_value="Empty filename")
4548
def filename(self, obj):
@@ -61,9 +64,8 @@ def get_fieldsets(self, request, obj=None):
6164
return fieldsets
6265

6366
def save_model(self, request, obj, form, change):
64-
file_api = FileAPI(request.FILES["file"], request.user)
65-
url, info = file_api.upload()
66-
obj.link = url
67+
info = self.cdn.upload(request.FILES["file"], request.user)
68+
obj.link = info.url
6769
obj.user = request.user
6870
obj.name = info.name
6971
obj.size = info.size
@@ -72,5 +74,10 @@ def save_model(self, request, obj, form, change):
7274
super().save_model(request, obj, form, change)
7375

7476
def delete_model(self, request, obj):
75-
FileAPI.delete(obj.link)
77+
self.cdn.delete(obj.link)
7678
obj.delete()
79+
80+
def delete_queryset(self, request, queryset):
81+
for obj in queryset:
82+
self.cdn.delete(obj.link)
83+
queryset.delete()

files/constants.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import mimetypes
2+
3+
SUPPORTED_IMAGES_TYPES = (
4+
mimetypes.types_map[".jpg"],
5+
mimetypes.types_map[".png"],
6+
)

files/helpers.py

Lines changed: 7 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,9 @@
1-
from typing import Union
1+
import webp
2+
from PIL import Image
23

3-
import requests
4-
import time
5-
import magic
6-
from django.core.files.uploadedfile import TemporaryUploadedFile, InMemoryUploadedFile
74

8-
from files.exceptions import SelectelUploadError
9-
from files.typings import UserFileInfo
10-
11-
from procollab.settings import (
12-
DEBUG,
13-
SELECTEL_ACCOUNT_ID,
14-
SELECTEL_CONTAINER_NAME,
15-
SELECTEL_CONTAINER_PASSWORD,
16-
SELECTEL_CONTAINER_USERNAME,
17-
)
18-
19-
20-
class FileAPI:
21-
# fixme: looks terrible
22-
def __init__(
23-
self, file: Union[TemporaryUploadedFile, InMemoryUploadedFile], user
24-
) -> None:
25-
self.file = file # it's TemporaryUploadedFile, and it will be
26-
# removed after first .close() call, so we must read this file only once
27-
self.user = user
28-
self.file_object = self.file.open(mode="rb")
29-
30-
@staticmethod
31-
def delete(url: str) -> int:
32-
"""Deletes file from selcdn"""
33-
token = FileAPI._get_selectel_swift_token()
34-
response = requests.delete(url, headers={"X-Auth-Token": token})
35-
return response.status_code
36-
37-
def upload(self) -> tuple[str, UserFileInfo]:
38-
url = self._upload_via_selectel_swift()
39-
info = self.get_file_info(self.file)
40-
self.file_object.close()
41-
return url, info
42-
43-
def get_file_info(
44-
self, file: Union[TemporaryUploadedFile, InMemoryUploadedFile]
45-
) -> UserFileInfo:
46-
name, ext = file.name.split(".")
47-
return UserFileInfo(
48-
size=file.size, name=name, extension=ext, mime_type=self.get_file_mime_type()
49-
)
50-
51-
def get_file_mime_type(self):
52-
if isinstance(self.file, InMemoryUploadedFile):
53-
return magic.from_buffer(self.file_object.read(), mime=True)
54-
else:
55-
return magic.from_file(self.file.temporary_file_path(), mime=True)
56-
57-
def _upload_via_selectel_swift(self) -> str:
58-
token = self._get_selectel_swift_token()
59-
url = self._generate_selectel_swift_file_url()
60-
61-
requests.put(
62-
url,
63-
headers={
64-
"X-Auth-Token": token,
65-
"Content-Type": self.file_object.content_type,
66-
},
67-
data=self.file_object.read(),
68-
)
69-
70-
return url
71-
72-
def _generate_selectel_swift_link(sefl):
73-
link = f"https://api.selcdn.ru/v1/SEL_{SELECTEL_ACCOUNT_ID}/{SELECTEL_CONTAINER_NAME}/"
74-
if DEBUG:
75-
link += "debug/"
76-
return link
77-
78-
@staticmethod
79-
def _get_selectel_swift_token():
80-
"""Returns auth token for selcdn"""
81-
data = {
82-
"auth": {
83-
"identity": {
84-
"methods": ["password"],
85-
"password": {
86-
"user": {
87-
"id": SELECTEL_CONTAINER_USERNAME,
88-
"password": SELECTEL_CONTAINER_PASSWORD,
89-
}
90-
},
91-
}
92-
}
93-
}
94-
response = requests.post("https://api.selcdn.ru/v3/auth/tokens", json=data)
95-
if response.status_code not in [200, 201]:
96-
raise SelectelUploadError("Couldn't generate a token for selcdn")
97-
return response.headers["x-subject-token"]
98-
99-
def _get_file_extension(self) -> str:
100-
if len(self.file.name.split(".")) > 1:
101-
return "." + self.file.name.split(".")[1]
102-
return ""
103-
104-
def _generate_selectel_swift_file_url(self) -> str:
105-
"""
106-
Generates url for selcdn
107-
Returns:
108-
url: str looks like /hashedEmail/hashedFilename_hashedTime.extension
109-
"""
110-
link = self._generate_selectel_swift_link()
111-
extension = self._get_file_extension()
112-
return (
113-
link
114-
+ f"{abs(hash(self.user.email))}/{abs(hash(self.file.name))}_{abs(hash(time.time()))}{extension}"
115-
)
5+
def convert_image_to_webp(image, quality: int = 70):
6+
config = webp.WebPConfig.new(preset=webp.WebPPreset.PHOTO, quality=quality)
7+
pil_image = Image.open(image.file)
8+
webp_image = webp.WebPPicture.from_pil(pil_image)
9+
return webp_image.encode(config)

files/models.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import reprlib
2+
13
from django.contrib.auth import get_user_model
24
from django.db import models
35

@@ -26,7 +28,8 @@ class UserFile(models.Model):
2628
size = models.PositiveBigIntegerField(null=False, blank=True, default=1)
2729

2830
def __str__(self):
29-
return f"UserFile by {self.user}, {self.link}"
31+
filename_with_extension = f"{self.name}.{self.extension}"
32+
return f"UserFile<{reprlib.repr(filename_with_extension)}>"
3033

3134
class Meta:
3235
verbose_name = "Файл"

0 commit comments

Comments
 (0)