Chunked uploads

This commit is contained in:
amcmanu3 2024-05-26 20:54:06 -04:00
parent 96b766cef7
commit c30d17cbf8
3 changed files with 178 additions and 3 deletions

View File

@ -44,6 +44,7 @@ from app.classes.web.routes.api.servers.server.files import (
ApiServersServerFilesIndexHandler,
ApiServersServerFilesCreateHandler,
ApiServersServerFilesZipHandler,
ApiServersServerFilesUploadHandler,
)
from app.classes.web.routes.api.servers.server.tasks.task.children import (
ApiServersServerTasksTaskChildrenHandler,
@ -243,6 +244,11 @@ def api_handlers(handler_args):
ApiServersServerFilesZipHandler,
handler_args,
),
(
r"/api/v2/servers/([a-z0-9-]+)/files/upload/?",
ApiServersServerFilesUploadHandler,
handler_args,
),
(
r"/api/v2/servers/([a-z0-9-]+)/tasks/?",
ApiServersServerTasksIndexHandler,

View File

@ -6,6 +6,7 @@ from jsonschema import validate
from jsonschema.exceptions import ValidationError
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.shared.helpers import Helpers
from app.classes.shared.main_controller import WebSocketManager, Controller
from app.classes.shared.file_helpers import FileHelpers
from app.classes.web.base_api_handler import BaseApiHandler
@ -577,3 +578,91 @@ class ApiServersServerFilesZipHandler(BaseApiHandler):
},
)
return self.finish_json(200, {"status": "ok"})
class ApiServersServerFilesUploadHandler(BaseApiHandler):
async def post(self, server_id: str):
for header, value in self.request.headers.items():
print(f"{header}: {value}")
fileHash = self.request.headers.get("fileHash", 0)
chunkHash = self.request.headers.get("chunk-hash", 0)
file_size = self.request.headers.get("fileSize", None)
self.file_id = self.request.headers.get("fileId")
self.chunked = self.request.headers.get("chunked", True)
self.filename = self.request.headers.get("filename", None)
try:
total_chunks = int(self.request.headers.get("total_chunks", None))
except TypeError:
return self.finish_json(
400, {"status": "error", "data": "INVALID CHUNK COUNT"}
)
self.chunk_index = self.request.headers.get("chunkId")
self.location = self.request.headers.get("location", None)
self.upload_dir = self.location
self.temp_dir = os.path.join(self.controller.project_root, "temp", self.file_id)
if self.chunked and not self.chunk_index:
return self.finish_json(
200, {"status": "ok", "data": {"file-id": self.file_id}}
)
# Create the upload and temp directories if they don't exist
os.makedirs(self.upload_dir, exist_ok=True)
os.makedirs(self.temp_dir, exist_ok=True)
# Read headers and query parameters
content_length = int(self.request.headers.get("Content-Length"))
if content_length <= 0:
return self.finish_json(
400, {"status": "error", "data": {"message": "Invalid content length"}}
)
if not self.filename or self.chunk_index is None or total_chunks is None:
return self.finish_json(
400,
{
"status": "error",
"data": {
"message": "Filename, chunk_index,"
" and total_chunks are required"
},
},
)
# File paths
file_path = os.path.join(self.upload_dir, self.filename)
chunk_path = os.path.join(
self.temp_dir, f"{self.filename}.part{self.chunk_index}"
)
# Save the chunk
with open(chunk_path, "wb") as f:
f.write(self.request.body)
# Check if all chunks are received
received_chunks = [
f
for f in os.listdir(self.temp_dir)
if f.startswith(f"{self.filename}.part")
]
if len(received_chunks) == total_chunks:
with open(file_path, "wb") as outfile:
for i in range(total_chunks):
chunk_file = os.path.join(self.temp_dir, f"{self.filename}.part{i}")
with open(chunk_file, "rb") as infile:
outfile.write(infile.read())
os.remove(chunk_file)
self.write(
json.dumps(
{"status": "completed", "message": "File uploaded successfully"}
)
)
else:
self.write(
json.dumps(
{
"status": "partial",
"message": f"Chunk {self.chunk_index} received",
}
)
)

View File

@ -67,7 +67,8 @@
translate('serverFiles', 'download', data['lang']) }}</a>
<a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteFile" href="#"
style="color: red">{{ translate('serverFiles', 'delete', data['lang']) }}</a>
<a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteDir" href="#" style="color: red">{{
<a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteDir" href="#"
style="color: red">{{
translate('serverFiles', 'delete', data['lang']) }}</a>
<a href="javascript:void(0)" class="closebtn" style="color: var(--info);"
onclick="document.getElementById('files-tree-nav').style.display = 'none';">{{
@ -156,7 +157,8 @@
right: 35px;
}
}
.tree-file:hover{
.tree-file:hover {
cursor: pointer;
}
</style>
@ -721,6 +723,84 @@
}
}
async function uploadFile(file, path, onProgress) {
const fileId = uuidv4();
const token = getCookie("_xsrf")
const fileInput = document.getElementById('fileInput');
if (!file) {
alert("Please select a file first.");
return;
}
const chunkSize = 1024 * 1024; // 1MB
const totalChunks = Math.ceil(file.size / chunkSize);
const uploadPromises = [];
let res = await fetch(`/api/v2/servers/${serverId}/files/upload/`, {
method: 'POST',
headers: {
'X-XSRFToken': token,
'chunked': true,
'total_chunks': totalChunks,
'location': path,
'filename': file.name,
'fileId': fileId,
},
body: null,
});
let responseData = await res.json();
let file_id = ""
if (responseData.status === "ok") {
file_id = responseData.data["file-id"]
}
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const uploadPromise = fetch(`/api/v2/servers/${serverId}/files/upload/`, {
method: 'POST',
body: chunk,
headers: {
'Content-Range': `bytes ${start}-${end - 1}/${file.size}`,
'Content-Length': chunk.size,
'chunked': true,
'total_chunks': totalChunks,
'filename': file.name,
'location': path,
'filename': file.name,
'fileId': fileId,
'chunkId': i,
},
}).then(response => response.json())
.then(data => {
if (data.status === "completed") {
alert("File uploaded successfully!");
} else if (data.status !== "partial") {
throw new Error(data.message);
}
});
uploadPromises.push(uploadPromise);
}
try {
await Promise.all(uploadPromises);
} catch (error) {
alert("Error uploading file: " + error.message);
}
}
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0,
v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
async function sendFile(file, path, serverId, left, i, onProgress) {
let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf")
@ -881,7 +961,7 @@
`;
$('#upload-progress-bar-parent').append(progressHtml);
await sendFile(files.files[i], path, serverId, nFiles - i - 1, i, (progress) => {
await uploadFile(files.files[i], path, (progress) => {
$(`#upload-progress-bar-${i + 1}`).attr('aria-valuenow', progress)
$(`#upload-progress-bar-${i + 1}`).css('width', progress + '%');
});