159 lines
5.0 KiB
JavaScript
159 lines
5.0 KiB
JavaScript
// Copyright (C) CVAT.ai Corporation
|
||
//
|
||
// SPDX-License-Identifier: MIT
|
||
import http from 'k6/http';
|
||
import encoding from 'k6/encoding';
|
||
import { validateResponse } from '../../utils/validation.js';
|
||
import { BASE_URL } from '../../variables/constants.js';
|
||
|
||
const MAX_REQUEST_SIZE = 50 * 1024 * 1024; // 50 MB threshold
|
||
|
||
function tusUploadInit(token, taskId, filePath, fileSize) {
|
||
const res = http.post(`${BASE_URL}/tasks/${taskId}/data/`, null,
|
||
{
|
||
headers: {
|
||
Authorization: `Token ${token}`,
|
||
'Upload-Length': `${fileSize}`,
|
||
'Upload-Metadata': `filename ${encoding.b64encode(filePath)}`,
|
||
},
|
||
},
|
||
);
|
||
validateResponse(res, 201, 'Task data Upload-Start');
|
||
return res;
|
||
}
|
||
|
||
function tusUploadFinish(token, taskId, finishOpts, fileName) {
|
||
const url = `${BASE_URL}/tasks/${taskId}/data/?file=${fileName}`;
|
||
const res = http.post(url, JSON.stringify(finishOpts), {
|
||
headers: {
|
||
Authorization: `Token ${token}`,
|
||
'Upload-Finish': '',
|
||
'Content-Type': 'application/json',
|
||
},
|
||
});
|
||
validateResponse(res, 202, 'Task data Upload-Finish');
|
||
return res;
|
||
}
|
||
|
||
/**
|
||
* Uploads a local file to a CVAT task via TUS protocol. Uses a single batch.
|
||
* @param {string} token
|
||
* @param {string} taskId
|
||
* @param {string} fileName
|
||
*/
|
||
export function tusUploadFile(token, taskId, fileName, fileData) {
|
||
const fileSize = fileData.byteLength;
|
||
// 1. Upload-Init
|
||
let res = tusUploadInit(token, taskId, fileName, fileSize);
|
||
// 2. Create upload URL (CVAT sends Location header)
|
||
const uploadUrl = res.headers.Location;
|
||
if (!uploadUrl) {
|
||
throw new Error('CVAT did not return upload URL in Location header');
|
||
}
|
||
// 3. Send file chunks (we’ll send it in one go here)
|
||
res = http.patch(uploadUrl, fileData, {
|
||
headers: {
|
||
Authorization: `Token ${token}`,
|
||
'Upload-Offset': '0',
|
||
'Content-Type': 'application/offset+octet-stream',
|
||
},
|
||
});
|
||
validateResponse(res, 204, 'Task data Upload-Chunk');
|
||
tusUploadFinish(token, taskId, { image_quality: 70 }, fileName);
|
||
}
|
||
|
||
// Single large file with TUS (Upload-Length + PATCH)
|
||
function tusUploadSingleFile(token, taskId, file) {
|
||
const size = file.bytes.byteLength;
|
||
let res = tusUploadInit(token, taskId, file.name, size);
|
||
const uploadUrl = res.headers.Location;
|
||
let offset = 0;
|
||
const CHUNK_SIZE = 10 * 1024 * 1024;
|
||
while (offset < size) {
|
||
const end = Math.min(offset + CHUNK_SIZE, size);
|
||
const chunk = file.bytes.slice(offset, end);
|
||
|
||
res = http.patch(uploadUrl, chunk, {
|
||
headers: {
|
||
Authorization: `Token ${token}`,
|
||
'Tus-Resumable': '1.0.0',
|
||
'Upload-Offset': String(offset),
|
||
'Content-Type': 'application/offset+octet-stream',
|
||
},
|
||
});
|
||
validateResponse(res, 204, 'Upload Patch');
|
||
offset = Number(res.headers['Upload-Offset']);
|
||
}
|
||
}
|
||
|
||
function splitFilesByRequests(files) {
|
||
const bulk = [];
|
||
const separate = [];
|
||
let total = 0;
|
||
for (const f of files) {
|
||
if (f.bytes.byteLength > MAX_REQUEST_SIZE) {
|
||
separate.push(f);
|
||
} else {
|
||
bulk.push(f);
|
||
}
|
||
total += f.bytes.byteLength;
|
||
}
|
||
|
||
const groups = [];
|
||
let current = [];
|
||
let currentSize = 0;
|
||
for (const f of bulk) {
|
||
if (currentSize + f.bytes.byteLength > MAX_REQUEST_SIZE) {
|
||
groups.push({ files: current, size: currentSize });
|
||
current = [];
|
||
currentSize = 0;
|
||
}
|
||
current.push(f);
|
||
currentSize += f.bytes.byteLength;
|
||
}
|
||
if (current.length) groups.push({ files: current, size: currentSize });
|
||
return { groups, separate, total };
|
||
}
|
||
|
||
function tusUploadFiles(
|
||
token,
|
||
taskId,
|
||
files, // [{ name: "a.jpg", bytes: ArrayBuffer }, ...]
|
||
finishOpts, // { image_quality, sorting_method, ... }
|
||
) {
|
||
const url = `${BASE_URL}/tasks/${taskId}/data`;
|
||
const { groups, separate } = splitFilesByRequests(files);
|
||
// If sorting method is predefined -> pass upload_file_order
|
||
if (
|
||
finishOpts.sorting_method &&
|
||
String(finishOpts.sorting_method).toLowerCase() === 'predefined'
|
||
) {
|
||
finishOpts.upload_file_order = files.map((f) => f.name);
|
||
}
|
||
// ---- Bulk files via Upload-Multiple
|
||
for (const { files: group } of groups) {
|
||
const formData = {};
|
||
group.forEach((f, i) => {
|
||
formData[`client_files[${i}]`] = http.file(f.bytes, f.name);
|
||
});
|
||
formData.image_quality = finishOpts.image_quality;
|
||
|
||
const res = http.post(url, formData, {
|
||
headers: {
|
||
Authorization: `Token ${token}`,
|
||
'Upload-Multiple': '',
|
||
},
|
||
});
|
||
validateResponse(res, 200, 'Upload-Multiple');
|
||
}
|
||
|
||
// ---- Large files via TUS
|
||
for (const f of separate) {
|
||
tusUploadSingleFile(token, taskId, f);
|
||
}
|
||
|
||
tusUploadFinish(token, taskId, finishOpts);
|
||
}
|
||
|
||
export default { tusUploadFiles, tusUploadFile };
|