diff --git a/logs_collector/collector/admin.py b/logs_collector/collector/admin.py index e205677..c5dc23a 100644 --- a/logs_collector/collector/admin.py +++ b/logs_collector/collector/admin.py @@ -5,7 +5,7 @@ from django.utils.html import format_html from django.utils.translation import ngettext from .models import Platform, Archive, Ticket -from .utils import sizify +from .utils.helpers import sizify class PlatformAdmin(admin.ModelAdmin): diff --git a/logs_collector/collector/api/urls.py b/logs_collector/collector/api/urls.py index ab94f7b..1a34d27 100644 --- a/logs_collector/collector/api/urls.py +++ b/logs_collector/collector/api/urls.py @@ -18,4 +18,5 @@ router.register(r'tickets', views.TicketViewSet) urlpatterns = [ # CRUD: path('v1/', include(router.urls)), + path('v1/storage/', views.StorageInfo.as_view(), name='storage-info'), ] diff --git a/logs_collector/collector/api/views.py b/logs_collector/collector/api/views.py index 857b96b..00b5ca7 100644 --- a/logs_collector/collector/api/views.py +++ b/logs_collector/collector/api/views.py @@ -1,4 +1,5 @@ from django.core.exceptions import ValidationError, ObjectDoesNotExist +from django.conf import settings from rest_framework import status # from rest_framework.decorators import action @@ -10,6 +11,7 @@ from rest_framework.parsers import ( from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework import viewsets +from rest_framework import views from rest_framework import filters from django_filters.rest_framework import DjangoFilterBackend @@ -18,6 +20,7 @@ from drf_spectacular.utils import extend_schema from drf_spectacular.openapi import OpenApiParameter from collector.models import Archive, Ticket, Platform +from collector.utils.helpers import get_mount_fs_info from .filters import ArchiveFilter, TicketFilter from .permissions import IsGuestUpload @@ -122,3 +125,10 @@ class TicketViewSet(viewsets.ModelViewSet): def perform_create(self, serializer): serializer.save(user=self.request.user) + + +class StorageInfo(views.APIView): + """Info about storage total/used/free space""" + + def get(self, request): + return Response(get_mount_fs_info(settings.MEDIA_ROOT)) diff --git a/logs_collector/collector/context_processors.py b/logs_collector/collector/context_processors.py new file mode 100644 index 0000000..7160cf6 --- /dev/null +++ b/logs_collector/collector/context_processors.py @@ -0,0 +1,14 @@ +from django.conf import settings + +from .utils.helpers import get_mount_fs_info + + +def metadata(request): + return { + "version": settings.VERSION, + "environment": settings.ENVIRONMENT, + } + + +def storage_info(request): + return get_mount_fs_info(settings.MEDIA_ROOT) diff --git a/logs_collector/collector/migrations/0001_initial.py b/logs_collector/collector/migrations/0001_initial.py index fce79fc..bc56299 100644 --- a/logs_collector/collector/migrations/0001_initial.py +++ b/logs_collector/collector/migrations/0001_initial.py @@ -47,7 +47,7 @@ class Migration(migrations.Migration): name='Archive', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('file', models.FileField(upload_to=collector.utils.logs_dir_path)), + ('file', models.FileField(upload_to=collector.utils.helpers.logs_dir_path)), ('size', models.BigIntegerField(editable=False)), ('md5', models.CharField(editable=False, max_length=1024)), ('time_create', models.DateTimeField(auto_now_add=True)), diff --git a/logs_collector/collector/models.py b/logs_collector/collector/models.py index 2296cbd..063351a 100644 --- a/logs_collector/collector/models.py +++ b/logs_collector/collector/models.py @@ -6,7 +6,7 @@ from django.contrib.auth.models import User from django.db import models from django.urls import reverse -from .utils import logs_dir_path +from .utils.helpers import logs_dir_path class Archive(models.Model): diff --git a/logs_collector/collector/static/collector/js/helpers.js b/logs_collector/collector/static/collector/js/helpers.js new file mode 100644 index 0000000..f48ed67 --- /dev/null +++ b/logs_collector/collector/static/collector/js/helpers.js @@ -0,0 +1,69 @@ +// formatted byte size to human readable: +const sizify = (value) => { + let ext = '' + if (value < 512000) { + value = value / 1024.0 + ext = 'KB' + } else if (value < 4194304000) { + value = value / 1048576.0 + ext = 'MB' + } else { + value = value / 1073741824.0 + ext = 'GB' + }; + return `${Math.round(value * 10) / 10} ${ext}` +}; + +// fix update bootstrap tooltip func: +const updateBsTooltip = (instance) => { + let tt = bootstrap.Tooltip.getInstance(instance); + tt.dispose(); + bootstrap.Tooltip.getOrCreateInstance(instance); +}; + +// update storage info widget: +const updateStorageInfo = () => { + // set storage items vars: + let storageIcon = $("#storage_icon") + let storageProgressContainer = $("#storage_progress_container") + let storage_progress = $("#storage_progress") + // set API url: + const storageUrl = storage_progress.attr("storage-url") + $.ajax({ + type: "GET", + url: storageUrl, + headers: { + "Content-Type":"application/json" + }, + dataType: "json", + success: function (data, textStatus, jqXHR) { + // JSON answer: + let storage = data.storage + // set updated fields: + let storageInfoNewFields = [ + `Total: ${sizify(storage.total)}`, + '
', + `Used: ${sizify(storage.used)}`, + '
', + `Free: ${sizify(storage.free)}` + ].join('') + // progress bar update: + storage_progress.attr("style", `width:${storage.used_percent}%`) + // progress bar color update: + if (storage.used_percent > 90) { + storage_progress.attr("class", "progress-bar bg-danger"); + } else if (storage.used_percent > 80) { + storage_progress.attr("class", "progress-bar bg-warning"); + } else { + storage_progress.attr("class", "progress-bar bg-success"); + }; + // tooltips update: + storageIcon.attr("data-bs-title", `Storage used: ${storage.used_percent}%`) + storageProgressContainer.attr("data-bs-title", storageInfoNewFields) + updateBsTooltip(storageIcon) + updateBsTooltip(storageProgressContainer) + } + }); +}; + +export {sizify, updateBsTooltip, updateStorageInfo}; diff --git a/logs_collector/collector/static/collector/js/jq.ticket.detail.js b/logs_collector/collector/static/collector/js/jq.ticket.detail.js index 10575fd..2cdc11b 100644 --- a/logs_collector/collector/static/collector/js/jq.ticket.detail.js +++ b/logs_collector/collector/static/collector/js/jq.ticket.detail.js @@ -1,3 +1,6 @@ +import {updateStorageInfo} from "./helpers.js"; + + $(function () { console.log("JQ is ready to work"); @@ -24,8 +27,11 @@ $(function () { success: function (data, textStatus, jqXHR) { console.log(jqXHR.status); $(archiveListElement).hide(1500); + setTimeout(() => { + updateStorageInfo(); + }, 3000); }, - error: function (data, textStatus, jqXHR) { + error: function (jqXHR, textStatus, errorThrown) { console.log(jqXHR.status); } }); @@ -57,7 +63,7 @@ $(function () { success: function (data, textStatus, jqXHR) { console.log(jqXHR.status) }, - error: function (data, textStatus, jqXHR) { + error: function (jqXHR, textStatus, errorThrown) { console.log(data) console.log(jqXHR.status) } @@ -82,11 +88,14 @@ $(function () { console.log(jqXHR.status); if (delDiv.length) { delDiv.hide(1500); + setTimeout(() => { + updateStorageInfo(); + }, 3000); } else { window.location.href = redirectUrl; } }, - error: function (data, textStatus, jqXHR) { + error: function (jqXHR, textStatus, errorThrown) { console.log(jqXHR.status); } }); diff --git a/logs_collector/collector/static/collector/js/jq.upload.progress.js b/logs_collector/collector/static/collector/js/jq.upload.progress.js index c4e3a21..45a324a 100644 --- a/logs_collector/collector/static/collector/js/jq.upload.progress.js +++ b/logs_collector/collector/static/collector/js/jq.upload.progress.js @@ -1,3 +1,5 @@ +import {updateStorageInfo} from "./helpers.js"; + $(function () { const uploadForm = document.getElementById('upload_form'); const input_file = document.getElementById('id_file'); @@ -6,7 +8,7 @@ $(function () { $("#upload_form").submit(function(e){ e.preventDefault(); - $form = $(this) + // $form = $(this) let formData = new FormData(this); let upload_token = formData.get("token") const media_data = input_file.files[0]; @@ -51,6 +53,11 @@ $(function () { ].join('') uploadForm.reset() progress_bar.classList.add('not-visible') + try { + updateStorageInfo(); + } catch (error) { + console.log(error) + }; }, error: function(jqXHR, textStatus, errorThrown){ console.log(jqXHR); diff --git a/logs_collector/collector/templates/collector/archive_upload.html b/logs_collector/collector/templates/collector/archive_upload.html index ddad5f0..b97ce01 100644 --- a/logs_collector/collector/templates/collector/archive_upload.html +++ b/logs_collector/collector/templates/collector/archive_upload.html @@ -51,5 +51,5 @@ {% endblock main %} {% block jquery %} - + {% endblock jquery %} diff --git a/logs_collector/collector/templates/collector/storage.html b/logs_collector/collector/templates/collector/storage.html new file mode 100644 index 0000000..5f62143 --- /dev/null +++ b/logs_collector/collector/templates/collector/storage.html @@ -0,0 +1,39 @@ + diff --git a/logs_collector/collector/templates/collector/ticket.html b/logs_collector/collector/templates/collector/ticket.html index 1ba6158..366552a 100644 --- a/logs_collector/collector/templates/collector/ticket.html +++ b/logs_collector/collector/templates/collector/ticket.html @@ -56,5 +56,5 @@ {% endblock main %} {% block jquery %} - + {% endblock jquery %} diff --git a/logs_collector/collector/templates/collector/tickets.html b/logs_collector/collector/templates/collector/tickets.html index 5392c93..28cab7c 100644 --- a/logs_collector/collector/templates/collector/tickets.html +++ b/logs_collector/collector/templates/collector/tickets.html @@ -86,9 +86,6 @@ {% include 'collector/includes/pagination.html' %} {% endblock main %} -{% block bs %} - -{% endblock bs %} {% block jquery %} - + {% endblock jquery %} diff --git a/logs_collector/collector/utils.py b/logs_collector/collector/utils.py deleted file mode 100644 index 0c61f31..0000000 --- a/logs_collector/collector/utils.py +++ /dev/null @@ -1,43 +0,0 @@ -def logs_dir_path(instance, filename): - """ - file will be uploaded to - MEDIA_ROOT_FOR_SENSITIVE_FILES// - """ - return f'{instance.ticket.number}/{filename}' - - -def sizify(value: int) -> str: - """Simple kb/mb/gb size snippet for admin panel custom field: - - Args: - value (int): size of file from Filefield - - Returns: - str: format human readable size like 4.2 Gb - """ - if value < 512000: - value = value / 1024.0 - ext = 'Kb' - elif value < 4194304000: - value = value / 1048576.0 - ext = 'Mb' - else: - value = value / 1073741824.0 - ext = 'Gb' - return f'{round(value, 2)} {ext}' - - -class PageTitleViewMixin: - title = 'Collector' - - def get_title(self, *args, **kwargs): - """ - Return the class title attr by default, - but you can override this method to further customize - """ - return self.title - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['title'] = self.get_title() - return context diff --git a/logs_collector/collector/utils/__init__.py b/logs_collector/collector/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/logs_collector/collector/utils/helpers.py b/logs_collector/collector/utils/helpers.py new file mode 100644 index 0000000..bff3ce6 --- /dev/null +++ b/logs_collector/collector/utils/helpers.py @@ -0,0 +1,38 @@ +import shutil + + +def logs_dir_path(instance, filename): + """ + file will be uploaded to + MEDIA_ROOT/view/ + """ + return f'{instance.ticket.number}/{filename}' + + +def sizify(value: int) -> str: + """Simple kb/mb/gb size snippet for admin panel custom field: + + Args: + value (int): size of file from Filefield + + Returns: + str: format human readable size like 4.2 Gb + """ + if value < 512000: + value = value / 1024.0 + ext = 'KB' + elif value < 4194304000: + value = value / 1048576.0 + ext = 'MB' + else: + value = value / 1073741824.0 + ext = 'GB' + return f'{round(value, 1)} {ext}' + + +def get_mount_fs_info(path): + mount_info = shutil.disk_usage(path)._asdict() + mount_info['used_percent'] = round( + mount_info['used'] / mount_info['total'] * 100 + ) + return {'storage': mount_info} diff --git a/logs_collector/collector/utils/mixins.py b/logs_collector/collector/utils/mixins.py new file mode 100644 index 0000000..674c37f --- /dev/null +++ b/logs_collector/collector/utils/mixins.py @@ -0,0 +1,23 @@ +class ExtraContextMixin: + """The class adds additional context + to all child view classes that inherit from it. + Overrides the get_context_data method for CBV + """ + + title = 'Collector' + + def get_title(self, *args, **kwargs): + """ + Return the class title attr by default, + but you can override this method to further customize + """ + return self.title + + def get_context_data(self, **kwargs): + context = {} + try: + context = super().get_context_data(**kwargs) + except Exception: + pass + context['title'] = self.get_title() + return context diff --git a/logs_collector/collector/views.py b/logs_collector/collector/views.py index 5377061..db7dd8d 100644 --- a/logs_collector/collector/views.py +++ b/logs_collector/collector/views.py @@ -3,25 +3,22 @@ from django.http import FileResponse from django.views import generic from django.views.generic.detail import SingleObjectMixin from django.db.models import Q -from django.shortcuts import render from two_factor.views import OTPRequiredMixin from .forms import TicketForm, ArchiveForm from .models import Archive, Ticket -from .utils import PageTitleViewMixin +from .utils.mixins import ExtraContextMixin -class ArchiveUploadView(PageTitleViewMixin, generic.View): +class ArchiveUploadView(ExtraContextMixin, generic.TemplateView): form_class = ArchiveForm() - template = 'collector/archive_upload.html', + template_name = 'collector/archive_upload.html' - def get(self, request): - return render( - request, - self.template, - context={'form': self.form_class} - ) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['form'] = self.form_class + return context def get_title(self): return f'{self.title} - upload' @@ -41,7 +38,7 @@ class ArchiveHandlerView( return FileResponse(self.object.file) -class CreateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.CreateView): +class CreateTicket(LoginRequiredMixin, ExtraContextMixin, generic.CreateView): model = Ticket form_class = TicketForm template_name = 'collector/ticket_create.html' @@ -54,7 +51,7 @@ class CreateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.CreateView): return super().form_valid(form) -class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView): +class UpdateTicket(LoginRequiredMixin, ExtraContextMixin, generic.UpdateView): model = Ticket form_class = TicketForm template_name = 'collector/ticket_create.html' @@ -69,7 +66,7 @@ class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView): return super().form_valid(form) -class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView): +class ListAllTickets(LoginRequiredMixin, ExtraContextMixin, generic.ListView): model = Ticket template_name = 'collector/tickets.html' context_object_name = 'tickets' @@ -98,7 +95,7 @@ class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView): return super().get_queryset() -class ListPlatformTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView): # noqa:E501 +class ListPlatformTickets(LoginRequiredMixin, ExtraContextMixin, generic.ListView): # noqa:E501 model = Ticket template_name = 'collector/tickets.html' context_object_name = 'tickets' @@ -114,7 +111,7 @@ class ListPlatformTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListVi ) -class DetailTicket(LoginRequiredMixin, PageTitleViewMixin, generic.DetailView): +class DetailTicket(LoginRequiredMixin, ExtraContextMixin, generic.DetailView): model = Ticket template_name = 'collector/ticket.html' context_object_name = 'ticket' diff --git a/logs_collector/logs_collector/__init__.py b/logs_collector/logs_collector/__init__.py index e69de29..a95ca20 100644 --- a/logs_collector/logs_collector/__init__.py +++ b/logs_collector/logs_collector/__init__.py @@ -0,0 +1,29 @@ +""" +An application for uploading archives with log files +for their subsequent download and check issues +that have arisen with software products. +The purpose of creating this application is +the ability to securely exchange and store log files containing sensitive data. +I have not found an application that would allow an unauthorized client +to upload data without providing him with authorization credentials. +You can use other applications for this, +such as Google cloud, Yandex cloud, DropBox etc, but in this case, +you do not have a tool that would allow you to automatically restrict uploads +later until you explicitly deny access to the shared link. +This app allows you to upload files using a unique token +associated with a support ticket. +This token has a limit on the number of file upload attempts. +Also, if the ticket is resolved, then the token is invalid. +""" + + +# █▀▄▀█ █▀▀ ▀█▀ ▄▀█ ▀ +# █░▀░█ ██▄ ░█░ █▀█ ▄ +# ------------------- +__author__ = "MOIS3Y" +__credits__ = ["Stepan Zhukovsky"] +__license__ = "GPL v3.0" +__version__ = "0.1.0" +__maintainer__ = "Stepan Zhukovsky" +__email__ = "stepan@zhukovsky.me" +__status__ = "Development" diff --git a/logs_collector/logs_collector/settings.py b/logs_collector/logs_collector/settings.py index 9c72247..ba90fee 100644 --- a/logs_collector/logs_collector/settings.py +++ b/logs_collector/logs_collector/settings.py @@ -2,6 +2,7 @@ import environ from pathlib import Path from datetime import timedelta +from . import __version__ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -9,6 +10,8 @@ BASE_DIR = Path(__file__).resolve().parent.parent # Set default environ variables: env = environ.Env( # set casting default value + VERSION=(str, __version__), + ENVIRONMENT=(str, 'development'), DEBUG=(bool, False), SECRET_KEY=(str, 'j9QGbvM9Z4otb47'), SQLITE_URL=(str, f'sqlite:///{BASE_DIR / "data/db.sqlite3"}'), @@ -20,6 +23,9 @@ env = environ.Env( # Read .env file if exist: environ.Env.read_env(BASE_DIR / '.env') +VERSION = env('VERSION') +ENVIRONMENT = env('ENVIRONMENT') + # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = env('SECRET_KEY') @@ -78,10 +84,14 @@ TEMPLATES = [ 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ + # default: 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + # collector: + 'collector.context_processors.metadata', + 'collector.context_processors.storage_info', ], }, }, @@ -174,6 +184,7 @@ REST_FRAMEWORK = { 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', # 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', # noqa:E501 # 'PAGE_SIZE': 3, + 'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata', } if DEBUG: diff --git a/logs_collector/collector/static/collector/js/bs.tooltip.js b/logs_collector/static/js/bs.tooltip.js similarity index 100% rename from logs_collector/collector/static/collector/js/bs.tooltip.js rename to logs_collector/static/js/bs.tooltip.js diff --git a/logs_collector/templates/base.html b/logs_collector/templates/base.html index 0b60a93..bf2f9de 100644 --- a/logs_collector/templates/base.html +++ b/logs_collector/templates/base.html @@ -40,9 +40,12 @@ {% block collector_content %}{% endblock collector_content %} {% block account_content %}{% endblock account_content %} - + + + + {% block collector_scripts %}{% endblock collector_scripts %} {% block account_scripts %}{% endblock account_scripts %} diff --git a/logs_collector/templates/includes/brand.html b/logs_collector/templates/includes/brand.html new file mode 100644 index 0000000..0f5cbaf --- /dev/null +++ b/logs_collector/templates/includes/brand.html @@ -0,0 +1,28 @@ + + Logs Collector + + + diff --git a/logs_collector/templates/includes/extra_menu.html b/logs_collector/templates/includes/extra_menu.html new file mode 100644 index 0000000..323a81f --- /dev/null +++ b/logs_collector/templates/includes/extra_menu.html @@ -0,0 +1,61 @@ +{% if request.user.is_authenticated %} + +{% else %} + +{% endif %} diff --git a/logs_collector/templates/includes/menu.html b/logs_collector/templates/includes/menu.html new file mode 100644 index 0000000..d3385e1 --- /dev/null +++ b/logs_collector/templates/includes/menu.html @@ -0,0 +1,47 @@ +{% load collector_extras %} +{% get_platforms as platforms %} + \ No newline at end of file diff --git a/logs_collector/templates/includes/navigation.html b/logs_collector/templates/includes/navigation.html index e2899d4..df97515 100644 --- a/logs_collector/templates/includes/navigation.html +++ b/logs_collector/templates/includes/navigation.html @@ -1,157 +1,22 @@ -{% load collector_extras %} -{% get_platforms as platforms %}