Modify: archive upload js now it check token status, HEALTHCHEK call health api endpoint

This commit is contained in:
Stepan Zhukovsky 2023-09-13 19:22:37 +09:00
parent a1679b69c3
commit a7a85b1816
8 changed files with 244 additions and 82 deletions

View File

@ -29,7 +29,8 @@ ARG VERSION=0.1.0 \
USER_NAME=collector \
USER_GROUP=collector \
APP_UID=1000 \
APP_GID=1000
APP_GID=1000 \
HEALTHCHECK_PATH=api/v1/check/health/
# copy app dependences
COPY --from=base /usr/local/lib/python3.10/site-packages/ /usr/local/lib/python3.10/site-packages/
@ -61,7 +62,7 @@ LABEL maintainer="s.zhukovskii@ispsystem.com"
LABEL me.zhukovsky.logs-collector.version=v${VERSION}
# call the health check endpoint of app
HEALTHCHECK CMD curl --fail http://localhost:${WEB_PORT} || exit 1
HEALTHCHECK CMD curl --fail http://localhost:${WEB_PORT}/${HEALTHCHECK_PATH} || exit 1
# run app
ENTRYPOINT [ "sh", "entrypoint.sh" ]

View File

@ -11,3 +11,15 @@ class IsGuestUpload(permissions.BasePermission):
return True
return request.user.is_authenticated
class IsGuestCheckUrls(permissions.BasePermission):
"""
Special permission class for the ability to upload attachments
to an unauthorized user using a ticket token
"""
def has_permission(self, request, view):
if request.method in ('HEAD', 'OPTIONS', 'GET',):
return True
return request.user.is_authenticated

View File

@ -67,3 +67,25 @@ class StorageInfoSerializer(serializers.Serializer):
free = serializers.IntegerField(read_only=True)
used_percent = serializers.IntegerField(read_only=True)
status = serializers.CharField(read_only=True)
class TokenStateRootSerializer(serializers.Serializer):
info = serializers.CharField(read_only=True, default="manual message")
class TokenStateSerializer(serializers.ModelSerializer):
token = serializers.UUIDField(read_only=True)
attempts = serializers.IntegerField(read_only=True)
resolved = serializers.BooleanField(read_only=True)
class Meta:
model = Ticket
fields = [
'token',
'attempts',
'resolved'
]
class AppHealthInfoSerializer(serializers.Serializer):
status = serializers.CharField(read_only=True, default="ok")

View File

@ -15,8 +15,31 @@ router.register(r'archives', views.ArchiveViewSet)
router.register(r'platforms', views.PlatformViewSet)
router.register(r'tickets', views.TicketViewSet)
check_urlpatterns = [
path(
'health/',
views.AppHealthInfo.as_view(),
name='app-info'
),
path(
'storage/',
views.StorageInfo.as_view(),
name='storage-info'
),
path(
'token/',
views.TokenStateRoot.as_view(),
name='token-root'
),
path(
'token/<str:token>',
views.TokenStateInfo.as_view(),
name='token-info'
),
]
urlpatterns = [
# CRUD:
path('v1/', include(router.urls)),
path('v1/storage/', views.StorageInfo.as_view(), name='storage-info'),
path('v1/check/', include(check_urlpatterns)),
]

View File

@ -10,9 +10,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 rest_framework import filters, generics, views, viewsets
from django_filters.rest_framework import DjangoFilterBackend
@ -23,13 +21,16 @@ 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
from .permissions import IsGuestUpload, IsGuestCheckUrls
from .serializers import (
PublicArchiveUploadSerializer,
ArchiveSerializer,
PlatformSerializer,
TicketSerializer,
StorageInfoSerializer,
TokenStateSerializer,
AppHealthInfoSerializer,
TokenStateRootSerializer,
)
@ -172,3 +173,40 @@ class StorageInfo(views.APIView):
)
def get(self, request):
return Response(get_mount_fs_info(settings.DATA_DIR))
class TokenStateRoot(views.APIView):
""" Show the message of a specific upload token URL"""
permission_classes = (IsGuestCheckUrls,)
@extend_schema(
responses=TokenStateRootSerializer,
summary='Show info message how get token status'
)
def get(self, request):
message = "to find out the status of the token, place it in the URL"
return Response({"detail": message}, status=status.HTTP_303_SEE_OTHER)
@extend_schema_view(
get=extend_schema(
summary='Show the status of a specific upload token'
)
)
class TokenStateInfo(generics.RetrieveAPIView):
""" Show the status of a specific upload token"""
queryset = Ticket.objects.order_by('-time_create')
lookup_field = 'token'
serializer_class = TokenStateSerializer
permission_classes = (IsGuestCheckUrls,)
class AppHealthInfo(views.APIView):
permission_classes = (IsGuestCheckUrls,)
@extend_schema(
responses=AppHealthInfoSerializer,
summary='Show app status'
)
def get(self, request):
return Response({'status': 'ok'}, status=status.HTTP_200_OK)

View File

@ -66,4 +66,18 @@ const updateStorageInfo = () => {
});
};
export {sizify, updateBsTooltip, updateStorageInfo};
const genAlertMessage = (
alertMessage='Success message',
alertType='success',
extraClass=''
) => {
let alertMessageHTML = [
`<div class="alert alert-${alertType} alert-dismissible ${extraClass}" role="alert">`,
` <div>${alertMessage}</div>`,
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
'</div>'
].join('')
return alertMessageHTML
}
export {sizify, updateBsTooltip, updateStorageInfo, genAlertMessage};

View File

@ -1,88 +1,139 @@
import {updateStorageInfo} from "./helpers.js";
import {updateStorageInfo, genAlertMessage} from "./helpers.js";
$(function () {
// set global variables:
const uploadForm = document.getElementById('upload_form');
const input_file = document.getElementById('id_file');
const progress_bar = document.getElementById('progress');
const alert_container = document.getElementById('alert');
const inputFile = document.getElementById('id_file');
const progressBar = document.getElementById('progress');
const alertContainer = document.getElementById('alert');
// get upload form:
$("#upload_form").submit(function(e){
e.preventDefault();
// $form = $(this)
// collect request data:
let formData = new FormData(this);
let upload_token = formData.get("token")
const media_data = input_file.files[0];
if(media_data != null){
progress_bar.classList.remove("not-visible");
}
$.ajax({
type: 'POST',
url: progress_bar.getAttribute("upload-url"),
data: formData,
dataType: 'json',
xhr:function(){
const xhr = new window.XMLHttpRequest();
xhr.timeout = 3600000; // increase request timeout to 1 hour
xhr.upload.addEventListener('progress', e=>{
if(e.lengthComputable){
const percentProgress = (e.loaded/e.total)*100;
console.log(percentProgress);
progress_bar.innerHTML = `
<div
class="progress-bar progress-bar-striped progress-bar-animated"
style="width: ${percentProgress}%"
>
</div>`
let uploadToken = formData.get("token")
// generate the URL for token validation:
let tokenStatusUrl = [
progressBar.getAttribute('token-status-url'),
uploadToken
].join('')
// init upload file func:
const uploadFile = () => {
// toggle visible progress bar:
const mediaData = inputFile.files[0];
if(mediaData != null){
progressBar.classList.remove("not-visible");
}
// upload file (chunk) xrh request:
$.ajax({
type: 'POST',
url: progressBar.getAttribute("upload-url"),
data: formData,
dataType: 'json',
xhr:function(){
const xhr = new window.XMLHttpRequest();
xhr.timeout = 3600000; // increase request timeout to 1 hour
xhr.upload.addEventListener('progress', e=>{
if(e.lengthComputable){
const percentProgress = (e.loaded/e.total)*100;
console.log(percentProgress);
progressBar.innerHTML = `
<div
class="progress-bar progress-bar-striped progress-bar-animated"
style="width: ${percentProgress}%"
>
</div>`
}
});
return xhr
},
// set auth method:
beforeSend: function(xhr) {
if (uploadToken) {
xhr.setRequestHeader("Upload-Token", uploadToken);
}
});
return xhr
},
beforeSend: function(xhr) {
if (upload_token) {
xhr.setRequestHeader("Upload-Token", upload_token);
},
success: function(data, textStatus, jqXHR){
console.log(jqXHR.status);
alertContainer.innerHTML = genAlertMessage(
'The file has been successfully uploaded to the server. Thank you!',
'success',
'col-lg-6'
)
uploadForm.reset()
progressBar.classList.add('not-visible')
try {
updateStorageInfo();
} catch (error) {
console.log(error)
};
},
error: function(jqXHR, textStatus, errorThrown){
console.log(jqXHR);
let errorMessage = "Unexpected error. Try again please"
if (jqXHR.status === 423 || jqXHR.status === 403) {
errorMessage = `Error ${jqXHR.status}: ${jqXHR.responseJSON.error}`
}
if (jqXHR.status === 401) {
errorMessage = 'The token field cannot be empty'
}
alertContainer.innerHTML = genAlertMessage(
errorMessage,
'danger',
'col-lg-6'
)
progressBar.classList.add('not-visible')
},
cache: false,
contentType: false,
processData: false,
});
}
// check token status and upload file if token valid:
$.ajax({
type: 'GET',
url: tokenStatusUrl,
dataType: "json",
success: function (data, textStatus, jqXHR) {
if (data.attempts === 0) {
alertContainer.innerHTML = genAlertMessage(
`Token: ${uploadToken} expired`,
'danger',
'col-lg-6'
);
}
},
success: function(data, textStatus, jqXHR){
console.log(jqXHR.status);
let type = "success";
alert_container.innerHTML = [
`<div class="alert alert-${type} alert-dismissible col-lg-6" role="alert">`,
` <div>The file has been successfully uploaded to the server. Thank you!</div>`,
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
'</div>'
].join('')
uploadForm.reset()
progress_bar.classList.add('not-visible')
try {
updateStorageInfo();
} catch (error) {
console.log(error)
else if (data.resolved === true) {
alertContainer.innerHTML = genAlertMessage(
`Ticket bound with token: ${uploadToken} <br> already resolved`,
'danger',
'col-lg-6'
);
} else {
alertContainer.innerHTML = genAlertMessage(
`Token: ${uploadToken} is valid. <br> Starting to upload...`,
'success',
'col-lg-6'
);
uploadFile();
};
},
error: function(jqXHR, textStatus, errorThrown){
console.log(jqXHR);
let type = "danger";
let error_message = "Unexpected error. Try again please"
if (jqXHR.status === 423) {
error_message = `Error ${jqXHR.status}: ${jqXHR.responseJSON.error}`
error: function(jqXHR){
console.log(jqXHR)
console.log(jqXHR.responseJSON.detail)
if (jqXHR.responseJSON.detail) {
alertContainer.innerHTML = genAlertMessage(
`Token: ${uploadToken} is not valid`,
'danger',
'col-lg-6'
)
} else {
alertContainer.innerHTML = genAlertMessage(
`Unexpected error. Try again please`,
'danger',
'col-lg-6'
)
}
if (jqXHR.status === 403) {
error_message = `Error ${jqXHR.status}: ${jqXHR.responseJSON.error}`
}
if (jqXHR.status === 401) {
error_message = 'The token field cannot be empty'
}
alert_container.innerHTML = [
`<div class="alert alert-${type} alert-dismissible col-lg-6" role="alert">`,
` <div>${error_message}</div>`,
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
'</div>'
].join('')
progress_bar.classList.add('not-visible')
},
cache: false,
contentType: false,
processData: false,
});
});
});

View File

@ -35,7 +35,8 @@
<div
id="progress"
upload-url="{% url 'collector_api:archive-list' %}"
class="progress"
token-status-url="{% url 'collector_api:token-root' %}"
class="progress not-visible"
role="progressbar"
aria-label="Example 20px high"
aria-valuenow="25"