Merge branch 'files' into 'master'

Files

See merge request mathwave/sprint!7
This commit is contained in:
Egor Matveev 2021-11-13 15:32:58 +00:00
commit a7f22b19d0
24 changed files with 277 additions and 102 deletions

0
FileStorage/__init__.py Normal file
View File

21
FileStorage/root.py Normal file
View File

@ -0,0 +1,21 @@
import os
from os import mkdir
from os.path import exists
from aiohttp import web
from FileStorage.routes import setup_routes
def runserver():
app = web.Application()
setup_routes(app)
if not exists("data"):
mkdir("data")
if not exists("data/meta.txt"):
with open("data/meta.txt", "w") as fs:
fs.write("0")
web.run_app(app, host=os.getenv("FS_HOST", "0.0.0.0"), port=5555)
if __name__ == "__main__":
runserver()

9
FileStorage/routes.py Normal file
View File

@ -0,0 +1,9 @@
from aiohttp import web
from FileStorage.views import get_file, upload_file, delete_file
def setup_routes(app: web.Application):
app.router.add_get("/get_file", get_file)
app.router.add_post("/upload_file", upload_file)
app.router.add_post("/delete_file", delete_file)

26
FileStorage/sync.py Normal file
View File

@ -0,0 +1,26 @@
import threading
import aiofiles
def synchronized_method(method):
outer_lock = threading.Lock()
lock_name = "__" + method.__name__ + "_lock" + "__"
def sync_method(self, *args, **kws):
with outer_lock:
if not hasattr(self, lock_name):
setattr(self, lock_name, threading.Lock())
lock = getattr(self, lock_name)
with lock:
return method(self, *args, **kws)
return sync_method
@synchronized_method
async def write_meta(request):
async with aiofiles.open("data/meta.txt", "r") as fs:
num = int(await fs.read()) + 1
async with aiofiles.open("data/meta.txt", "w") as fs:
await fs.write(str(num))
return num

View File

@ -0,0 +1,3 @@
from .get_file import get_file
from .upload_file import upload_file
from .delete_file import delete_file

View File

@ -0,0 +1,8 @@
from os import remove
from aiohttp import web
async def delete_file(request):
remove("data/" + request.rel_url.query['id'])
return web.json_response({"success": True})

View File

@ -0,0 +1,10 @@
import aiofiles
from aiohttp import web
async def get_file(request):
response = web.StreamResponse()
await response.prepare(request)
async with aiofiles.open("data/" + request.rel_url.query['id'], "rb") as fs:
await response.write_eof(await fs.read())
return response

View File

@ -0,0 +1,11 @@
from aiohttp import web
from FileStorage.sync import write_meta
import aiofiles
async def upload_file(request):
file_id = await write_meta(request)
async with aiofiles.open("data/" + str(file_id), "wb") as fs:
await fs.write(await request.content.read())
return web.json_response({"id": file_id})

View File

@ -0,0 +1,9 @@
from django.core.management.base import BaseCommand
from FileStorage.root import runserver
class Command(BaseCommand):
help = 'starts FileStorage'
def handle(self, *args, **options):
runserver()

View File

@ -0,0 +1,31 @@
# Generated by Django 3.2.4 on 2021-11-10 20:17
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('Main', '0074_auto_20211106_1215'),
]
operations = [
migrations.CreateModel(
name='SolutionFile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('path', models.TextField()),
('fs_id', models.IntegerField()),
('solution', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.solution')),
],
),
migrations.DeleteModel(
name='Language',
),
migrations.AddField(
model_name='extrafile',
name='fs_id',
field=models.IntegerField(null=True),
),
]

View File

@ -8,3 +8,4 @@ from Main.models.settask import SetTask
from Main.models.solution import Solution from Main.models.solution import Solution
from Main.models.extrafile import ExtraFile from Main.models.extrafile import ExtraFile
from Main.models.progress import Progress from Main.models.progress import Progress
from Main.models.solution_file import SolutionFile

View File

@ -1,23 +1,17 @@
from os import remove
from os.path import join, exists
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import models from django.db import models
from Sprint.settings import DATA_ROOT from .mixins import FileStorageMixin
class ExtraFile(models.Model): class ExtraFile(FileStorageMixin, models.Model):
task = models.ForeignKey("Task", on_delete=models.CASCADE) task = models.ForeignKey("Task", on_delete=models.CASCADE)
filename = models.TextField() filename = models.TextField()
is_test = models.BooleanField(null=True) is_test = models.BooleanField(null=True)
is_sample = models.BooleanField(null=True) is_sample = models.BooleanField(null=True)
readable = models.BooleanField(null=True) readable = models.BooleanField(null=True)
test_number = models.IntegerField(null=True) test_number = models.IntegerField(null=True)
fs_id = models.IntegerField(null=True)
@property
def path(self):
return join(DATA_ROOT, "extra_files", str(self.id))
@property @property
def can_be_sample(self): def can_be_sample(self):
@ -29,13 +23,8 @@ class ExtraFile(models.Model):
) )
) )
@property
def text(self):
return open(self.path, "r").read()
def delete(self, using=None, keep_parents=False): def delete(self, using=None, keep_parents=False):
if exists(self.path): self.remove_from_fs()
remove(self.path)
if self.is_test and self.filename.endswith('.a'): if self.is_test and self.filename.endswith('.a'):
try: try:
ef = ExtraFile.objects.get(task=self.task, filename=self.filename.rstrip('.a'), is_test=True) ef = ExtraFile.objects.get(task=self.task, filename=self.filename.rstrip('.a'), is_test=True)
@ -47,4 +36,4 @@ class ExtraFile(models.Model):
@property @property
def answer(self): def answer(self):
return ExtraFile.objects.get(task=self.task, is_test=True, filename=self.filename + '.a') return ExtraFile.objects.get(task=self.task, is_test=True, filename=self.filename + '.a')

14
Main/models/mixins.py Normal file
View File

@ -0,0 +1,14 @@
from SprintLib.utils import get_bytes, write_bytes, delete_file
class FileStorageMixin:
@property
def text(self):
return get_bytes(self.fs_id).decode("utf-8")
def write(self, bytes):
self.fs_id = write_bytes(bytes)
self.save()
def remove_from_fs(self):
delete_file(self.fs_id)

View File

@ -1,13 +1,12 @@
from os import mkdir, walk
from os.path import join, exists from os.path import join, exists
from shutil import rmtree from shutil import rmtree
from subprocess import call from subprocess import call
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from Main.models.solution_file import SolutionFile
from Main.models.task import Task from Main.models.task import Task
from Sprint.settings import CONSTS, SOLUTIONS_ROOT, SOLUTIONS_ROOT_EXTERNAL from Sprint.settings import CONSTS, SOLUTIONS_ROOT, SOLUTIONS_ROOT_EXTERNAL
from SprintLib.language import languages from SprintLib.language import languages
@ -32,47 +31,41 @@ class Solution(models.Model):
@property @property
def files(self): def files(self):
data = [] data = []
for path, _, files in walk(self.directory): for file in SolutionFile.objects.filter(solution=self):
if path.startswith(self.testing_directory): try:
text = file.text
except:
continue continue
for file in files: entity = {
try: 'filename': file.path,
entity = { 'text': text
'filename': file, }
'text': open(join(path, file), 'r').read() end = file.path.split('.')[-1]
} language = None
end = file.split('.')[-1] for l in languages:
language = None if l.file_type == end:
for l in languages: language = l
if l.file_type == end: break
language = l if language is None:
break highlight = 'nohighlight'
if language is None: else:
highlight = 'nohighlight' highlight = 'language-' + language.highlight
else: entity['highlight'] = highlight
highlight = 'language-' + language.highlight data.append(entity)
entity['highlight'] = highlight
data.append(entity)
except:
continue
data.sort(key=lambda x: x['filename']) data.sort(key=lambda x: x['filename'])
return data return data
def create_dirs(self):
mkdir(self.directory)
mkdir(self.testing_directory)
@property @property
def directory(self): def directory(self):
return join(SOLUTIONS_ROOT, str(self.id)) return "solutions/" + str(self.id)
@property @property
def testing_directory(self): def testing_directory(self):
return join(self.directory, 'test_dir') return self.directory
@property @property
def volume_directory(self): def volume_directory(self):
return join(SOLUTIONS_ROOT_EXTERNAL, str(self.id), 'test_dir') return "/sprint-data/worker/" + str(self.id)
def exec_command(self, command, working_directory='app', timeout=None): def exec_command(self, command, working_directory='app', timeout=None):
return call(f'docker exec -i solution_{self.id} sh -c "cd {working_directory} && {command}"', shell=True, timeout=timeout) return call(f'docker exec -i solution_{self.id} sh -c "cd {working_directory} && {command}"', shell=True, timeout=timeout)

View File

@ -0,0 +1,9 @@
from django.db import models
from Main.models.mixins import FileStorageMixin
class SolutionFile(FileStorageMixin, models.Model):
path = models.TextField()
fs_id = models.IntegerField()
solution = models.ForeignKey('Solution', on_delete=models.CASCADE)

View File

@ -45,11 +45,9 @@ class TaskSettingsView(BaseView):
filename=filename, filename=filename,
is_test=is_test is_test=is_test
) )
with open(ef.path, 'wb') as fs: ef.write(self.request.FILES['file'].read())
for chunk in self.request.FILES['file'].chunks():
fs.write(chunk)
try: try:
open(ef.path, 'r').read() var = ef.text
ef.readable = True ef.readable = True
except UnicodeDecodeError: except UnicodeDecodeError:
ef.readable = False ef.readable = False
@ -73,8 +71,7 @@ class TaskSettingsView(BaseView):
ef, created = ExtraFile.objects.get_or_create(filename=name, task=self.entities.task) ef, created = ExtraFile.objects.get_or_create(filename=name, task=self.entities.task)
if not created: if not created:
return f'/admin/task?task_id={self.entities.task.id}&error_message=Файл с таким именем уже существует' return f'/admin/task?task_id={self.entities.task.id}&error_message=Файл с таким именем уже существует'
with open(ef.path, 'w') as fs: ef.write(b"")
fs.write('')
ef.is_test = is_test ef.is_test = is_test
ef.readable = True ef.readable = True
ef.save() ef.save()
@ -88,8 +85,8 @@ class TaskSettingsView(BaseView):
def post_save_test(self): def post_save_test(self):
ef = ExtraFile.objects.get(id=self.request.POST['test_id']) ef = ExtraFile.objects.get(id=self.request.POST['test_id'])
with open(ef.path, 'w') as fs: ef.remove_from_fs()
fs.write(self.request.POST['text']) ef.write(self.request.POST['text'].encode('utf-8'))
ef.is_sample = 'is_sample' in self.request.POST.keys() ef.is_sample = 'is_sample' in self.request.POST.keys()
ef.save() ef.save()
return f'/admin/task?task_id={self.entities.task.id}' return f'/admin/task?task_id={self.entities.task.id}'

View File

@ -1,11 +1,11 @@
import io
from zipfile import ZipFile from zipfile import ZipFile
from os.path import join
from Main.models import Solution, Progress from Main.models import Solution, Progress, SolutionFile
from SprintLib.BaseView import BaseView from SprintLib.BaseView import BaseView
from SprintLib.language import languages from SprintLib.language import languages
from SprintLib.queue import send_testing from SprintLib.queue import send_testing
from SprintLib.testers import * from SprintLib.utils import write_bytes
class TaskView(BaseView): class TaskView(BaseView):
@ -13,40 +13,54 @@ class TaskView(BaseView):
view_file = "task.html" view_file = "task.html"
def get(self): def get(self):
self.context['languages'] = sorted(languages, key=lambda x: x.name) self.context["languages"] = sorted(languages, key=lambda x: x.name)
progress, _ = Progress.objects.get_or_create(user=self.request.user, task=self.entities.task) progress, _ = Progress.objects.get_or_create(
self.context['progress'] = progress user=self.request.user, task=self.entities.task
)
self.context["progress"] = progress
def pre_handle(self): def pre_handle(self):
if self.request.method == 'GET': if self.request.method == "GET":
return return
self.solution = Solution.objects.create( self.solution = Solution.objects.create(
task=self.entities.task, task=self.entities.task,
user=self.request.user, user=self.request.user,
language_id=int(self.request.POST["language"]) language_id=int(self.request.POST["language"]),
) )
self.solution.create_dirs()
def post_0(self): def post_0(self):
# отправка решения через текст # отправка решения через текст
filename = 'solution.' + self.solution.language.file_type fs_id = write_bytes(self.request.POST["code"].encode("utf-8"))
file_path = join(self.solution.directory, filename) SolutionFile.objects.create(
with open(file_path, 'w') as fs: path="solution." + self.solution.language.file_type,
fs.write(self.request.POST['code']) solution=self.solution,
fs_id=fs_id,
)
send_testing(self.solution.id) send_testing(self.solution.id)
return "task?task_id=" + str(self.entities.task.id) return "task?task_id=" + str(self.entities.task.id)
def post_1(self): def post_1(self):
# отправка решения через файл # отправка решения через файл
if 'file' not in self.request.FILES: if "file" not in self.request.FILES:
return "task?task_id=" + str(self.entities.task.id) return "task?task_id=" + str(self.entities.task.id)
filename = self.request.FILES['file'].name filename = self.request.FILES["file"].name
file_path = join(self.solution.directory, filename) if filename.endswith(".zip"):
with open(file_path, 'wb') as fs: archive = ZipFile(io.BytesIO(self.request.FILES['file'].read()))
for chunk in self.request.FILES['file'].chunks(): for file in archive.infolist():
fs.write(chunk) if file.is_dir():
if filename.endswith('.zip'): continue
with ZipFile(file_path) as obj: fs_id = write_bytes(archive.read(file.filename))
obj.extractall(self.solution.directory) SolutionFile.objects.create(
path=file.filename,
solution=self.solution,
fs_id=fs_id,
)
else:
fs_id = write_bytes(self.request.FILES['file'].read())
SolutionFile.objects.create(
path=filename,
solution=self.solution,
fs_id=fs_id,
)
send_testing(self.solution.id) send_testing(self.solution.id)
return "task?task_id=" + str(self.entities.task.id) return "task?task_id=" + str(self.entities.task.id)

View File

@ -146,6 +146,9 @@ SOLUTIONS_ROOT = os.path.join(DATA_ROOT, "solutions")
RABBIT_HOST = HOST RABBIT_HOST = HOST
RABBIT_PORT = 5672 RABBIT_PORT = 5672
FS_HOST = "http://" + HOST
FS_PORT = 5555
STATICFILES_DIRS = [ STATICFILES_DIRS = [
os.path.join(BASE_DIR, "Main/static"), os.path.join(BASE_DIR, "Main/static"),
] ]

View File

@ -1,13 +1,12 @@
from os import listdir, mkdir from os import listdir, mkdir
from os.path import join, exists from os.path import join, exists
from shutil import copyfile, rmtree
from subprocess import call, TimeoutExpired from subprocess import call, TimeoutExpired
from Main.management.commands.bot import bot from Main.management.commands.bot import bot
from Main.models import ExtraFile from Main.models import ExtraFile, SolutionFile
from Main.models.progress import Progress from Main.models.progress import Progress
from Sprint.settings import CONSTS from Sprint.settings import CONSTS
from SprintLib.utils import copy_content from SprintLib.utils import get_bytes
class TestException(Exception): class TestException(Exception):
@ -56,19 +55,26 @@ class BaseTester:
self.solution = solution self.solution = solution
def execute(self): def execute(self):
if not exists(self.solution.testing_directory): if not exists("solutions"):
mkdir(self.solution.testing_directory) mkdir("solutions")
copy_content( mkdir("solutions/" + str(self.solution.id))
self.solution.directory, self.solution.testing_directory, ("test_dir",) for file in SolutionFile.objects.filter(solution=self.solution):
) dirs = file.path.split('/')
for i in range(len(dirs) - 1):
name = join(str("solutions/" + self.solution.id), '/'.join(dirs[:i + 1]))
if not exists(name):
mkdir(name)
with open(join("solutions/" + str(self.solution.id), file.path), 'wb') as fs:
fs.write(get_bytes(file.fs_id))
self.solution.result = CONSTS["testing_status"] self.solution.result = CONSTS["testing_status"]
self.solution.save() self.solution.save()
docker_command = f"docker run --name solution_{self.solution.id} --volume={self.solution.volume_directory}:/{self.working_directory} -t -d {self.solution.language.image}" docker_command = f"docker run --name solution_{self.solution.id} --volume=/sprint-data/solutions/{self.solution.id}:/{self.working_directory} -t -d {self.solution.language.image}"
print(docker_command) print(docker_command)
call(docker_command, shell=True) call(docker_command, shell=True)
print("Container created") print("Container created")
for file in ExtraFile.objects.filter(task=self.solution.task): for file in ExtraFile.objects.filter(task=self.solution.task):
copyfile(file.path, join(self.solution.testing_directory, file.filename)) with open(join("solutions/" + str(self.solution.id), file.filename), 'wb') as fs:
fs.write(get_bytes(file.fs_id))
print("Files copied") print("Files copied")
try: try:
self.before_test() self.before_test()
@ -98,7 +104,6 @@ class BaseTester:
print(str(e)) print(str(e))
self.solution.save() self.solution.save()
call(f"docker rm --force solution_{self.solution.id}", shell=True) call(f"docker rm --force solution_{self.solution.id}", shell=True)
rmtree(self.solution.testing_directory)
self.solution.user.userinfo.refresh_from_db() self.solution.user.userinfo.refresh_from_db()
if self.solution.user.userinfo.notification_solution_result: if self.solution.user.userinfo.notification_solution_result:
bot.send_message( bot.send_message(

View File

@ -1,4 +1,4 @@
from os import listdir from os import listdir, getcwd
from SprintLib.testers.BaseTester import BaseTester, TestException from SprintLib.testers.BaseTester import BaseTester, TestException
@ -7,11 +7,13 @@ class Python3Tester(BaseTester):
file = None file = None
def before_test(self): def before_test(self):
print(getcwd())
for file in listdir(self.solution.testing_directory): for file in listdir(self.solution.testing_directory):
if file.endswith(".py"): if file.endswith(".py"):
self.file = file self.file = file
break break
if self.file is None: if self.file is None:
print('no file')
raise TestException("TE") raise TestException("TE")
@property @property

View File

@ -1,12 +1,21 @@
from os import listdir from requests import get, post
from os.path import isfile, join
from shutil import copyfile, copytree from Sprint import settings
def copy_content(from_dir, to_dir, exc=()): def write_bytes(data):
for file in listdir(from_dir): url = settings.FS_HOST + ":" + str(settings.FS_PORT) + "/upload_file"
if file in exc: print(url)
continue return post(url, data=data).json()['id']
full_path = join(from_dir, file)
func = copyfile if isfile(full_path) else copytree
func(full_path, join(to_dir, file)) def get_bytes(num):
url = settings.FS_HOST + ":" + str(settings.FS_PORT) + "/get_file?id=" + str(num)
print(url)
return get(url).content
def delete_file(num):
url = settings.FS_HOST + ":" + str(settings.FS_PORT) + "/delete_file?id=" + str(num)
print(url)
post(url)

View File

@ -27,11 +27,20 @@ services:
ports: ports:
- "${PORT}:${PORT}" - "${PORT}:${PORT}"
volumes: volumes:
- /sprint-data/data:/usr/src/app/data
- /sprint-data/media:/usr/src/app/media - /sprint-data/media:/usr/src/app/media
depends_on: depends_on:
- postgres - postgres
- rabbitmq - rabbitmq
- storage
storage:
restart: always
image: mathwave/sprint-repo:sprint
command: python manage.py storage
ports:
- "5555:5555"
volumes:
- /sprint-data/data:/usr/src/app/data
bot: bot:
image: mathwave/sprint-repo:sprint image: mathwave/sprint-repo:sprint
@ -63,6 +72,7 @@ services:
- web - web
- rabbitmq - rabbitmq
- postgres - postgres
- storage
volumes: volumes:
- /sprint-data/data:/usr/src/app/data - /sprint-data/solutions:/usr/src/app/solutions
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock

View File

@ -1,3 +1,5 @@
aiofiles==0.7.0
aiohttp==3.8.0
amqp==5.0.6 amqp==5.0.6
asgiref==3.3.4 asgiref==3.3.4
billiard==3.6.4.0 billiard==3.6.4.0

View File

@ -1,3 +1,2 @@
python manage.py migrate python manage.py migrate
python manage.py update_languages
python manage.py runserver 0.0.0.0:$PORT --noreload python manage.py runserver 0.0.0.0:$PORT --noreload