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 };
|