diff --git a/openra/apps.py b/openra/apps.py index 44895f4..5dc2c60 100644 --- a/openra/apps.py +++ b/openra/apps.py @@ -20,5 +20,6 @@ def ready(self): ".management.commands.import_latest_engines", ".facades", '.services.engine_file_repository', - '.services.map_file_repository' + '.services.map_file_repository', + '.services.screenshot_repository', ]) diff --git a/openra/classes/exceptions.py b/openra/classes/exceptions.py index f1f7268..e9f58cb 100644 --- a/openra/classes/exceptions.py +++ b/openra/classes/exceptions.py @@ -24,3 +24,6 @@ def get_full_details(self): def print_full_details(self): print(self.get_full_details()) + + def __str__(self): + return self.get_full_details() diff --git a/openra/classes/file_location.py b/openra/classes/file_location.py index e493c91..da5bada 100644 --- a/openra/classes/file_location.py +++ b/openra/classes/file_location.py @@ -1,3 +1,5 @@ +from __future__ import annotations +import io from django.conf import os from fs.base import FS, copy from fs.tempfs import TempFS @@ -17,7 +19,7 @@ def __init__(self, fs: FS, path: str, file: str): self.file = file def get_fs_path(self): - return os.path.join(self.path + self.file) + return os.path.join(self.path, self.file) def get_os_dir(self): try: @@ -37,6 +39,30 @@ def get_os_path(self): except Exception as exception: raise ExceptionFileLocationGetOSPath(exception, self.fs, self.path, self.file) + def ensure_file_exists(self): + if not self.fs.exists(self.get_fs_path()): + if self.path: + self.fs.makedirs(self.path) + self.fs.create(self.get_fs_path()) + + def copy_to_file_location(self, location: FileLocation): + try: + location.ensure_file_exists() + copy.copy_file( + self.fs, + os.path.join( + self.path, + self.file + ), + location.fs, + location.get_fs_path() + ) + + return location + + except Exception as exception: + raise ExceptionFileLocationCopyToFileLocation(exception, self.fs, self.path, self.file, location) + def copy_to_tempfs(self, filename: str): try: temp_fs = TempFS() @@ -58,6 +84,23 @@ def copy_to_tempfs(self, filename: str): except Exception as exception: raise ExceptionFileLocationCopyToTempFS(exception, self.fs, self.path, self.file, filename) + def get_file_clone(self): + try: + + file = io.BytesIO() + + self.fs.download( + self.get_fs_path(), + file + ) + + file.seek(0) + + return file + + except Exception as exception: + raise ExceptionFileLocationGetFileClone(exception, self.fs, self.path, self.file) + class ExceptionFileLocationGetOSDir(ExceptionBase): def __init__(self, exception, fs: FS, path: str, file: str): @@ -75,6 +118,19 @@ def __init__(self, exception, fs: FS, path: str, file: str): self.message = "An exception occured while trying to get the os path" +class ExceptionFileLocationCopyToFileLocation(ExceptionBase): + def __init__(self, exception, fs: FS, path: str, file: str, target: FileLocation): + super().__init__() + self.message = "An exception occured while trying to copy a file to a TempFS" + self.detail.append('from fs type: ' + str(type(fs))) + self.detail.append('from path: ' + path) + self.detail.append('from file: ' + file) + self.detail.append('to fs type: ' + str(type(target.fs))) + self.detail.append('to path: ' + target.path) + self.detail.append('to file: ' + target.file) + self.detail.append('message: ' + str(exception)) + + class ExceptionFileLocationCopyToTempFS(ExceptionBase): def __init__(self, exception, fs: FS, path: str, file: str, target: str): super().__init__() @@ -84,3 +140,13 @@ def __init__(self, exception, fs: FS, path: str, file: str, target: str): self.detail.append('file: ' + file) self.detail.append('target: ' + target) self.detail.append('message: ' + str(exception)) + + +class ExceptionFileLocationGetFileClone(ExceptionBase): + def __init__(self, exception, fs: FS, path: str, file: str): + super().__init__() + self.message = "An exception occured while trying to clone a file" + self.detail.append('fs type: ' + str(type(fs))) + self.detail.append('path: ' + path) + self.detail.append('file: ' + file) + self.detail.append('message: ' + str(exception)) diff --git a/openra/classes/screenshot_resource.py b/openra/classes/screenshot_resource.py new file mode 100644 index 0000000..e0efc5d --- /dev/null +++ b/openra/classes/screenshot_resource.py @@ -0,0 +1,23 @@ + +from openra.classes.exceptions import ExceptionBase + + +class ScreenshotResource: + + type: str + id: int + + def __init__(self, type: str, id: int): + if type not in ['maps']: + raise ExceptionScreenshotResourceTypeInvalid(type, id) + + self.type = type + self.id = id + + +class ExceptionScreenshotResourceTypeInvalid(ExceptionBase): + def __init__(self, type: str, id: int): + super().__init__() + self.message = "Invalid resource type for screenshot resource" + self.detail.append('Type : ' + type) + self.detail.append('Id: ' + str(id)) diff --git a/openra/containers.py b/openra/containers.py index 78a6ec6..0f1cbf7 100644 --- a/openra/containers.py +++ b/openra/containers.py @@ -11,6 +11,8 @@ from openra.services.log import Log from openra.services.map_file_repository import MapFileRepository from openra.services.map_search import MapSearch +from openra.services.screenshot_repository import ScreenshotRepository +from openra.services.uploaded_file_importer import UploadedFileImporter from openra.services.utility import Utility @@ -41,6 +43,14 @@ class Container(containers.DeclarativeContainer): ) ) + uploaded_file_importer = providers.Singleton( + UploadedFileImporter + ) + + screenshot_repository = providers.Singleton( + ScreenshotRepository + ) + engine_file_repository = providers.Singleton( EngineFileRepository ) diff --git a/openra/services/screenshot_repository.py b/openra/services/screenshot_repository.py new file mode 100644 index 0000000..cd07b69 --- /dev/null +++ b/openra/services/screenshot_repository.py @@ -0,0 +1,93 @@ +import io +from dependency_injector.wiring import Provide, inject +from django.conf import os +from django.contrib.auth.models import User +from django.core.files.uploadedfile import UploadedFile +from django.utils import timezone +from fs.base import FS, copy +from openra.classes.exceptions import ExceptionBase +from openra.classes.file_location import FileLocation +from openra.classes.screenshot_resource import ScreenshotResource +from openra.models import Screenshots +from PIL import Image + +from openra.services.uploaded_file_importer import UploadedFileImporter + + +class ScreenshotRepository: + + _data_fs: FS + _uploaded_file_importer: UploadedFileImporter + + @inject + def __init__( + self, + data_fs: FS = Provide['data_fs'], + uploaded_file_importer: UploadedFileImporter = Provide['uploaded_file_importer'] + ): + self._data_fs = data_fs + self._uploaded_file_importer = uploaded_file_importer + + def create_from_uploaded_file(self, uploaded_file: UploadedFile, user: User, resource: ScreenshotResource, map_preview: bool): + + if uploaded_file.content_type not in ['image/jpeg', 'image/png', 'image/gif']: + raise ExceptionInvalidMimeType(uploaded_file.name, uploaded_file.content_type) + + extension = uploaded_file.content_type.split('/')[1] + + uploaded = self._uploaded_file_importer.import_file( + uploaded_file, + uploaded_file.name + ) + + image = Image.open( + uploaded.get_file_clone() + ) + + image.thumbnail(( + 250, + 250 + )) + + thumbnail = io.BytesIO() + + image.save(thumbnail, extension) + + thumbnail.seek(0) + + model = Screenshots( + user=user, + ex_id=resource.id, + ex_name=resource.type, + posted=timezone.now(), + map_preview=map_preview, + ) + + model.save() + + directory = os.path.join('screenshots', str(model.id)) + + uploaded.copy_to_file_location( + FileLocation( + self._data_fs, + directory, + str(resource.id) + '.' + extension + ) + ) + + preview_path = os.path.join('screenshots', str(model.id), str(resource.id) + '-mini.' + extension) + + self._data_fs.writefile( + preview_path, + thumbnail + ) + + return model + + +class ExceptionInvalidMimeType(ExceptionBase): + def __init__(self, filename: str, mimetype): + super().__init__() + self.message = "Mimetype invalid for a screenshot" + self.detail.append('Filename : ' + filename) + self.detail.append('Mimetype: ' + str(mimetype)) diff --git a/openra/services/uploaded_file_importer.py b/openra/services/uploaded_file_importer.py new file mode 100644 index 0000000..9c37d74 --- /dev/null +++ b/openra/services/uploaded_file_importer.py @@ -0,0 +1,38 @@ +from django.core.files.uploadedfile import File +from fs.tempfs import TempFS + +from openra.classes.exceptions import ExceptionBase +from openra.classes.file_location import FileLocation + + +class UploadedFileImporter: + + def import_file(self, request_file: File, filename: str): + + try: + temp_fs = self._create_temp_fs() + + for chunk in request_file.chunks(): + temp_fs.appendbytes( + filename, + chunk + ) + + return FileLocation( + temp_fs, + '', + filename + ) + except Exception as exception: + raise ExceptionUploadedFileImporter(exception, filename) + + def _create_temp_fs(self): + return TempFS() + + +class ExceptionUploadedFileImporter(ExceptionBase): + def __init__(self, exception, filename: str): + super().__init__() + self.message = "Uploaded file importer caught an exception while attempting to upload this file" + self.detail.append('filename: ' + filename) + self.detail.append('message: ' + str(exception)) diff --git a/openra/templates/addScreenshotForm.html b/openra/templates/addScreenshotForm.html index 007662e..25fa8fb 100644 --- a/openra/templates/addScreenshotForm.html +++ b/openra/templates/addScreenshotForm.html @@ -1,6 +1,6 @@ {% if request.user.is_authenticated %}
-
{% csrf_token %} + {% csrf_token %}

Upload screenshot of your map

@@ -22,4 +22,4 @@

Upload screenshot of your map

-{% endif %} \ No newline at end of file +{% endif %} diff --git a/openra/tests/test_classes_file_location.py b/openra/tests/test_classes_file_location.py index 43d3f31..936cbda 100644 --- a/openra/tests/test_classes_file_location.py +++ b/openra/tests/test_classes_file_location.py @@ -5,7 +5,7 @@ from unittest import TestCase -from openra.classes.file_location import ExceptionFileLocationCopyToTempFS, ExceptionFileLocationGetOSDir, ExceptionFileLocationGetOSPath, FileLocation +from openra.classes.file_location import ExceptionFileLocationCopyToTempFS, ExceptionFileLocationGetFileClone, ExceptionFileLocationGetOSDir, ExceptionFileLocationGetOSPath, FileLocation class TestFileLocation(TestCase): @@ -85,6 +85,33 @@ def test_get_os_path_throws_exception_if_no_os_path(self): file.get_os_path ) + def test_copy_to_file_location_copies_a_file(self): + fs = MemoryFS() + + fs.makedir('location') + fs.writetext('location/test_file', 'file_content') + + file = FileLocation( + fs, + 'location/', + 'test_file' + ) + + fs2 = MemoryFS() + + file2 = FileLocation( + fs2, + 'location2/', + 'test_file2' + ) + + file.copy_to_file_location(file2) + + self.assertEquals( + 'file_content', + file2.fs.readtext(file2.get_fs_path()) + ) + def test_copy_to_tempfs_copies_a_file(self): fs = MemoryFS() @@ -125,3 +152,38 @@ def test_copy_to_tempfs_throws_exception_if_unable_to_copy(self): file.copy_to_tempfs, 'new_name' ) + + def test_get_file_clone(self): + fs = MemoryFS() + + fs.makedir('location') + fs.writetext('location/test_file', 'file_content') + + file = FileLocation( + fs, + 'location/', + 'test_file' + ) + + clone = file.get_file_clone() + + self.assertEquals( + b'file_content', + clone.read() + ) + + def test_get_file_clone_throws_exception_if_unable_to_clone(self): + fs = MemoryFS() + + fs.makedir('location') + + file = FileLocation( + fs, + 'location/', + 'test_file' + ) + + self.assertRaises( + ExceptionFileLocationGetFileClone, + file.get_file_clone + ) diff --git a/openra/tests/test_service_screenshot_repository.py b/openra/tests/test_service_screenshot_repository.py new file mode 100644 index 0000000..d778d87 --- /dev/null +++ b/openra/tests/test_service_screenshot_repository.py @@ -0,0 +1,76 @@ +from unittest import TestCase +from django.conf import os +from django.core.files.uploadedfile import SimpleUploadedFile +from fs.tempfs import TempFS +from openra.classes.screenshot_resource import ScreenshotResource +from openra.services.screenshot_repository import ExceptionInvalidMimeType, ScreenshotRepository + +from openra.tests.factories import MapsFactory, UserFactory + + +class TestServiceScreenshotRepository(TestCase): + + def __get_uploaded_image_file(self): + with open('openra/static/images/soviet-logo-fallback.png', 'rb') as file: + data = file.read() + return SimpleUploadedFile( + 'image.jpg', + data, + content_type='image/png' + ) + + def test_it_can_import_an_uploaded_file(self): + user = UserFactory() + map = MapsFactory() + + fs = TempFS() + + resource = ScreenshotResource('maps', map.id) + + file = self.__get_uploaded_image_file() + + repo = ScreenshotRepository(data_fs=fs) + + screenshot = repo.create_from_uploaded_file( + file, + user, + resource, + False + ) + + self.assertEquals( + resource.id, + screenshot.ex_id + ) + + self.assertEquals( + resource.type, + screenshot.ex_name + ) + + self.assertTrue( + fs.exists(os.path.join('screenshots', str(screenshot.id), str(map.id) + '.png')) + ) + + self.assertTrue( + fs.exists(os.path.join('screenshots', str(screenshot.id), str(map.id) + '-mini.png')) + ) + + def test_it_will_throw_an_invalid_mime_type_exception_when_not_an_image(self): + user = UserFactory() + map = MapsFactory() + + resource = ScreenshotResource('maps', map.id) + + file = SimpleUploadedFile('image.jpg', b'content') + + repo = ScreenshotRepository() + + self.assertRaises( + ExceptionInvalidMimeType, + repo.create_from_uploaded_file, + file, + user, + resource, + False + ) diff --git a/openra/tests/test_service_uploaded_file_importer.py b/openra/tests/test_service_uploaded_file_importer.py new file mode 100644 index 0000000..426f0ff --- /dev/null +++ b/openra/tests/test_service_uploaded_file_importer.py @@ -0,0 +1,36 @@ +from unittest import TestCase +from django.core.files.uploadedfile import SimpleUploadedFile + +from openra.services.uploaded_file_importer import ExceptionUploadedFileImporter, UploadedFileImporter + + +class TestUploadedFileImporter(TestCase): + + def test_it_can_import_an_uploaded_file(self): + file = SimpleUploadedFile('image.jpg', b'content') + + importer = UploadedFileImporter() + + location = importer.import_file(file, 'imported.jpg') + + self.assertEquals( + 'content', + location.fs.readtext( + location.get_fs_path() + ) + ) + + self.assertEquals( + 'imported.jpg', + location.get_fs_path() + ) + + def test_it_can_throw_an_exception_if_the_file_doesnt_exist(self): + importer = UploadedFileImporter() + + self.assertRaises( + ExceptionUploadedFileImporter, + importer.import_file, + 'test_url', + 'file_name' + ) diff --git a/openra/urls.py b/openra/urls.py index ccfc054..bb4427d 100644 --- a/openra/urls.py +++ b/openra/urls.py @@ -49,6 +49,7 @@ url(r'^maps/(?P\d+)/report', views.map_report, name='map_report'), url(r'^maps/(?P\d+)/update-map-info', views.map_update_map_info, name='map_update_map_info'), + url(r'^maps/(?P\d+)/upload-screenshot', views.map_upload_screenshot, name='map_upload_screenshot'), url(r'^upload/map/?$', views.uploadMap, name='uploadMap'), url(r'^upload/map/(?P\d+)/?$', views.uploadMap, name='uploadMap'), diff --git a/openra/views.py b/openra/views.py index 5af6a09..e659248 100644 --- a/openra/views.py +++ b/openra/views.py @@ -25,9 +25,11 @@ from allauth.socialaccount.models import SocialAccount from openra import content, handlers, misc from openra.auth import ExceptionLoginFailed, set_session_to_remember_auth, try_login +from openra.classes.screenshot_resource import ScreenshotResource from openra.models import Maps, Lints, Screenshots, Reports, Rating, Comments, UnsubscribeComments from openra.services.map_search import MapSearch from openra.classes.pagination import Pagination +from openra.services.screenshot_repository import ScreenshotRepository # TODO: Fix the code and reenable some of these warnings # pylint: disable=invalid-name @@ -344,13 +346,34 @@ def map_update_map_info(request, map_id): return HttpResponseRedirect('/maps/' + map_id) -def displayMap(request, arg): - if request.method == 'POST': - if request.FILES.get('screenshot', False) is not False: +@inject +def map_upload_screenshot(request, map_id, + screenshot_repository: ScreenshotRepository = Provide['screenshot_repository'] + + ): + if not request.user.is_authenticated(): + return HttpResponseRedirect('/login/') - handlers.addScreenshot(request, arg, 'map') + if request.FILES.get('screenshot', False): - elif request.POST.get('comment', "") != "": + target_map = Maps.objects.get(id=map_id) + if request.user.is_superuser or request.user.id == target_map.user_id: + screenshot_repository.create_from_uploaded_file( + request.FILES.get('screenshot', False), + request.user, + ScreenshotResource( + 'maps', + target_map.id, + ), + request.POST.get('map_preview', None) == 'on' + ) + + return HttpResponseRedirect('/maps/' + map_id) + + +def displayMap(request, arg): + if request.method == 'POST': + if request.POST.get('comment', "") != "": account_age = misc.user_account_age(request.user) if account_age < 24: template = loader.get_template('index.html') diff --git a/requirements.txt b/requirements.txt index 8644720..6d15f80 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ decorator==4.0.9 -defusedxml==0.4.1 +defusedxml==0.7.1 Django==1.9.4 django-allauth==0.24.1 django-cors-headers==1.1.0 @@ -28,3 +28,4 @@ PyGithub==1.56 urllib3==1.26.13 freezegun==1.2.2 autopep8==2.0.1 +pillow==9.5.0