Skip to content

Commit

Permalink
Merge branches 'axios-tus' and 'prolong-sessions' into no-token-in-ui-2
Browse files Browse the repository at this point in the history
  • Loading branch information
SpecLad committed Aug 9, 2024
3 parents 79ec6a1 + 8a74cd9 + 446c4f0 commit 7302e8e
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 12 deletions.
2 changes: 1 addition & 1 deletion cvat-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "15.1.0",
"version": "15.1.1",
"type": "module",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "src/api.ts",
Expand Down
65 changes: 65 additions & 0 deletions cvat-core/src/axios-tus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (C) 2024 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

import Axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import * as tus from 'tus-js-client';

class AxiosHttpResponse implements tus.HttpResponse {
readonly #axiosResponse: AxiosResponse;

constructor(axiosResponse: AxiosResponse) {
this.#axiosResponse = axiosResponse;
}

getStatus(): number { return this.#axiosResponse.status; }
getHeader(header: string): string { return this.#axiosResponse.headers[header.toLowerCase()]; }
getBody(): string { return this.#axiosResponse.data; }
getUnderlyingObject(): any { return this.#axiosResponse; }
}

class AxiosHttpRequest implements tus.HttpRequest {
readonly #axiosConfig: AxiosRequestConfig;
readonly #abortController: AbortController;

constructor(method: string, url: string) {
this.#abortController = new AbortController();
this.#axiosConfig = {
method, url, headers: {}, signal: this.#abortController.signal,
};
}

getMethod(): string { return this.#axiosConfig.method; }
getURL(): string { return this.#axiosConfig.url; }

setHeader(header: string, value: string): void {
this.#axiosConfig.headers[header.toLowerCase()] = value;
}
getHeader(header: string): string | undefined {
return this.#axiosConfig.headers[header.toLowerCase()];
}

setProgressHandler(handler: (bytesSent: number) => void): void {
this.#axiosConfig.onUploadProgress = (progressEvent) => {
handler(progressEvent.loaded);
};
}

async send(body: any): Promise<tus.HttpResponse> {
const axiosResponse = await Axios({ ...this.#axiosConfig, data: body });
return new AxiosHttpResponse(axiosResponse);
}

async abort(): Promise<void> { this.#abortController.abort(); }

getUnderlyingObject(): any { return this.#axiosConfig; }
}

class AxiosHttpStack implements tus.HttpStack {
createRequest(method: string, url: string): tus.HttpRequest {
return new AxiosHttpRequest(method, url);
}
getName(): string { return 'AxiosHttpStack'; }
}

export const axiosTusHttpStack = new AxiosHttpStack();
12 changes: 2 additions & 10 deletions cvat-core/src/server-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as tus from 'tus-js-client';
import { ChunkQuality } from 'cvat-data';

import './axios-config';
import { axiosTusHttpStack } from './axios-tus';
import {
SerializedLabel, SerializedAnnotationFormats, ProjectsFilter,
SerializedProject, SerializedTask, TasksFilter, SerializedUser, SerializedOrganization,
Expand Down Expand Up @@ -117,7 +118,6 @@ function fetchAll(url, filter = {}): Promise<any> {
}

async function chunkUpload(file: File, uploadConfig): Promise<{ uploadSentSize: number; filename: string }> {
const params = enableOrganization();
const {
endpoint, chunkSize, totalSize, onUpdate, metadata, totalSentSize,
} = uploadConfig;
Expand All @@ -130,9 +130,7 @@ async function chunkUpload(file: File, uploadConfig): Promise<{ uploadSentSize:
filetype: file.type,
...metadata,
},
headers: {
Authorization: Axios.defaults.headers.common.Authorization,
},
httpStack: axiosTusHttpStack,
chunkSize,
retryDelays: [2000, 4000, 8000, 16000, 32000, 64000],
onShouldRetry(err: tus.DetailedError | Error): boolean {
Expand All @@ -151,12 +149,6 @@ async function chunkUpload(file: File, uploadConfig): Promise<{ uploadSentSize:
onError(error) {
reject(error);
},
onBeforeRequest(req) {
const xhr = req.getUnderlyingObject();
const { org } = params;
req.setHeader('X-Organization', org);
xhr.withCredentials = true;
},
onProgress(bytesUploaded) {
if (onUpdate && Number.isInteger(totalSentSize) && Number.isInteger(totalSize)) {
const currentUploadedSize = totalSentSize + bytesUploaded;
Expand Down
3 changes: 2 additions & 1 deletion cvat/apps/engine/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from unittest import mock
from textwrap import dedent
from typing import Optional, Callable, Dict, Any, Mapping
from urllib.parse import urljoin

import django_rq
from attr.converters import to_bool
Expand Down Expand Up @@ -315,7 +316,7 @@ def init_tus_upload(self, request):

return self._tus_response(
status=status.HTTP_201_CREATED,
extra_headers={'Location': '{}{}'.format(location, tus_file.file_id),
extra_headers={'Location': urljoin(location, tus_file.file_id),
'Upload-Filename': tus_file.filename})

def append_tus_chunk(self, request, file_id):
Expand Down
64 changes: 64 additions & 0 deletions cvat/apps/iam/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
#
# SPDX-License-Identifier: MIT

from datetime import timedelta
from typing import Callable

from django.utils.functional import SimpleLazyObject
from rest_framework.exceptions import ValidationError, NotFound
from django.conf import settings
from django.http import HttpRequest, HttpResponse


def get_organization(request):
Expand Down Expand Up @@ -57,3 +61,63 @@ def __call__(self, request):
request.iam_context = SimpleLazyObject(lambda: get_organization(request))

return self.get_response(request)

class SessionRefreshMiddleware:
"""
Implements behavior similar to SESSION_SAVE_EVERY_REQUEST=True, but instead of
saving the session on every request, saves it at most once per REFRESH_INTERVAL.
This is accomplished by setting a parallel cookie that expires whenever the session
needs to be prolonged.
This ensures that user sessions are automatically prolonged while in use,
but avoids making an extra DB query on every HTTP request.
Must be listed after SessionMiddleware in the MIDDLEWARE list.
"""

_REFRESH_INTERVAL = timedelta(days=1)
_COOKIE_NAME = "sessionfresh"

def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
self.get_response = get_response

def __call__(self, request: HttpRequest) -> HttpResponse:
response = self.get_response(request)

shared_cookie_args = {
"key": self._COOKIE_NAME,
"domain": getattr(settings, "SESSION_COOKIE_DOMAIN"),
"path": getattr(settings, "SESSION_COOKIE_PATH", "/"),
"samesite": getattr(settings, "SESSION_COOKIE_SAMESITE", "Lax"),
}

if request.session.is_empty():
if self._COOKIE_NAME in request.COOKIES:
response.delete_cookie(**shared_cookie_args)
return response

if self._COOKIE_NAME in request.COOKIES:
# Session is still fresh.
return response

if response.status_code >= 500:
# SessionMiddleware does not save the session for 5xx responses,
# so we should not set our cookie either.
return response

response.set_cookie(
**shared_cookie_args,
value="1",
max_age=min(
self._REFRESH_INTERVAL,
# Refresh no later than after half of the session lifetime.
timedelta(seconds=request.session.get_expiry_age() // 2),
),
httponly=True,
secure=getattr(settings, "SESSION_COOKIE_SECURE", False),
)

# Force SessionMiddleware to re-save the session.
request.session.modified = True

return response
1 change: 1 addition & 0 deletions cvat/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ def generate_secret_key():
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'cvat.apps.iam.middleware.SessionRefreshMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
Expand Down

0 comments on commit 7302e8e

Please sign in to comment.