Skip to content

Commit e0a60d8

Browse files
authored
Layer tus-js-client on top of Axios (#8281)
This ensures that requests coming from tus-js-client have the same defaults as the ones coming from the rest of the UI. In particular, this ensures that TUS requests include the `X-CSRFTOKEN` header. Currently, this doesn't matter much, because TUS requests are authenticated using the token. However, I'd like to get rid of token authentication in the UI, after which `X-CSRFTOKEN` will become important.
1 parent 8d0095f commit e0a60d8

File tree

3 files changed

+91
-11
lines changed

3 files changed

+91
-11
lines changed

cvat-core/src/axios-tus.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (C) 2024 CVAT.ai Corporation
2+
//
3+
// SPDX-License-Identifier: MIT
4+
5+
import Axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
6+
import * as tus from 'tus-js-client';
7+
8+
class AxiosHttpResponse implements tus.HttpResponse {
9+
readonly #axiosResponse: AxiosResponse;
10+
11+
constructor(axiosResponse: AxiosResponse) {
12+
this.#axiosResponse = axiosResponse;
13+
}
14+
15+
getStatus(): number {
16+
return this.#axiosResponse.status;
17+
}
18+
getHeader(header: string): string | undefined {
19+
return this.#axiosResponse.headers[header.toLowerCase()];
20+
}
21+
getBody(): string {
22+
return this.#axiosResponse.data;
23+
}
24+
getUnderlyingObject(): AxiosResponse {
25+
return this.#axiosResponse;
26+
}
27+
}
28+
29+
class AxiosHttpRequest implements tus.HttpRequest {
30+
readonly #axiosConfig: AxiosRequestConfig;
31+
readonly #abortController: AbortController;
32+
33+
constructor(method: string, url: string) {
34+
this.#abortController = new AbortController();
35+
this.#axiosConfig = {
36+
method,
37+
url,
38+
headers: {},
39+
signal: this.#abortController.signal,
40+
validateStatus: () => true,
41+
};
42+
}
43+
44+
getMethod(): string {
45+
return this.#axiosConfig.method;
46+
}
47+
getURL(): string {
48+
return this.#axiosConfig.url;
49+
}
50+
51+
setHeader(header: string, value: string): void {
52+
this.#axiosConfig.headers[header.toLowerCase()] = value;
53+
}
54+
getHeader(header: string): string | undefined {
55+
return this.#axiosConfig.headers[header.toLowerCase()];
56+
}
57+
58+
setProgressHandler(handler: (bytesSent: number) => void): void {
59+
this.#axiosConfig.onUploadProgress = (progressEvent) => {
60+
handler(progressEvent.loaded);
61+
};
62+
}
63+
64+
async send(body: any): Promise<tus.HttpResponse> {
65+
const axiosResponse = await Axios({ ...this.#axiosConfig, data: body });
66+
return new AxiosHttpResponse(axiosResponse);
67+
}
68+
69+
async abort(): Promise<void> {
70+
this.#abortController.abort();
71+
}
72+
73+
getUnderlyingObject(): AxiosRequestConfig {
74+
return this.#axiosConfig;
75+
}
76+
}
77+
78+
class AxiosHttpStack implements tus.HttpStack {
79+
createRequest(method: string, url: string): tus.HttpRequest {
80+
return new AxiosHttpRequest(method, url);
81+
}
82+
getName(): string {
83+
return 'AxiosHttpStack';
84+
}
85+
}
86+
87+
export const axiosTusHttpStack = new AxiosHttpStack();

cvat-core/src/server-proxy.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as tus from 'tus-js-client';
1010
import { ChunkQuality } from 'cvat-data';
1111

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

119120
async function chunkUpload(file: File, uploadConfig): Promise<{ uploadSentSize: number; filename: string }> {
120-
const params = enableOrganization();
121121
const {
122122
endpoint, chunkSize, totalSize, onUpdate, metadata, totalSentSize,
123123
} = uploadConfig;
@@ -130,9 +130,7 @@ async function chunkUpload(file: File, uploadConfig): Promise<{ uploadSentSize:
130130
filetype: file.type,
131131
...metadata,
132132
},
133-
headers: {
134-
Authorization: Axios.defaults.headers.common.Authorization,
135-
},
133+
httpStack: axiosTusHttpStack,
136134
chunkSize,
137135
retryDelays: [2000, 4000, 8000, 16000, 32000, 64000],
138136
onShouldRetry(err: tus.DetailedError | Error): boolean {
@@ -151,12 +149,6 @@ async function chunkUpload(file: File, uploadConfig): Promise<{ uploadSentSize:
151149
onError(error) {
152150
reject(error);
153151
},
154-
onBeforeRequest(req) {
155-
const xhr = req.getUnderlyingObject();
156-
const { org } = params;
157-
req.setHeader('X-Organization', org);
158-
xhr.withCredentials = true;
159-
},
160152
onProgress(bytesUploaded) {
161153
if (onUpdate && Number.isInteger(totalSentSize) && Number.isInteger(totalSize)) {
162154
const currentUploadedSize = totalSentSize + bytesUploaded;

cvat/apps/engine/mixins.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from unittest import mock
1515
from textwrap import dedent
1616
from typing import Optional, Callable, Dict, Any, Mapping
17+
from urllib.parse import urljoin
1718

1819
import django_rq
1920
from attr.converters import to_bool
@@ -315,7 +316,7 @@ def init_tus_upload(self, request):
315316

316317
return self._tus_response(
317318
status=status.HTTP_201_CREATED,
318-
extra_headers={'Location': '{}{}'.format(location, tus_file.file_id),
319+
extra_headers={'Location': urljoin(location, tus_file.file_id),
319320
'Upload-Filename': tus_file.filename})
320321

321322
def append_tus_chunk(self, request, file_id):

0 commit comments

Comments
 (0)