Uploader un fichier en multipart avec Kotlin et Javascript

Catégories

JavaScript Kotlin

Qu'est-ce que le multipart ?

La particularité du multipart est que nous pouvons uploader des fichiers de très gros volumes, jusqu'au Terabyte.

Le fonctionnement du multipart est un peu particulier. En général, lors de l'upload d'un fichier standard, le fichier entier est envoyé sous forme d'un objet unique. objet unique. Avec le multipart, on découpe le fichier en plusieurs morceaux pour ensuite le reconstituer avec toutes les parties.

Création du fichier multipart

1.Le fichier est découpé en plusieurs parties, appelées chunks. Dans ce cas, les segments sont de 5 Mo chacun.

const chunkSize = 1024 * 1024 * 5; // 5 MB chunk size
const totalChunks = Math.ceil(file.size / chunkSize);

const createFileChunks = (file, chunkSize) => {
    const chunks: Blob[] = [];
    let offset = 0;

    while (offset < file.size) {
        chunks.push(file.slice(offset, offset + chunkSize));
        offset += chunkSize;
    }

    return chunks;
}
  1. Le multipart est créé en envoyant le nom du fichier à uploader ainsi que le nombre total de chunks.
const initResponst = async () => {
    const response = await fetch("/folders/upload/init", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            filename: file.name,
            total_chunks: totalChunks,
        }),
    });

    if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
    }

    return await response.json();
}
  1. La création du fichier multipart est envoyée au serveur S3, en veillant à retourner l'id de l'upload pour continuer le processus.
val uploads = mutableMapOf<String, MutableList<CompletedPart>>()

suspend fun initiateMultipartUpload(client: S3Client, key: String): String? {
    val multipartRes = client.createMultipartUpload {
        checksumAlgorithm = ChecksumAlgorithm.Sha256
        bucket = S3Config.bucketName
        this.key = key
    }

    uploads[multipartRes.uploadId!!] = mutableListOf()

    return multipartRes.uploadId
}

Uploader la totalité des parties du fichier sous forme de chunk

  1. Le formData est créé et envoyé à l'API..
for (let index = 0; index < chunks.length; index++) {
    const chunk = chunks[index];
    const formData = new FormData();
    formData.append('uploadId', uploadId.toString());
    formData.append('chunkNumber', (index + 1).toString());
    formData.append('totalChunks', totalChunks.toString());
    formData.append('file', chunk, file.name);

    await fetch("/folders/upload", {
        method: "POST",
        headers: {
            "Content-Type": "multipart/form-data",
        },
        body: formData,
    });
}
  1. Récupèrer le formData pour ensuite uploader le chunk envoyé.

Grâce à la méthode receiveMultipart() de Ktor, permet facilement la récupération du formData pour l'envoyer au serveur s3.

post {
    val multipart = call.receiveMultipart()
    var uploadId: String? = null
    var chunkNumber: Int? = null
    var totalChunks: Int? = null
    var originalFileName: String? = null
    var fileBytes: ByteArray? = null

    multipart.forEachPart { part ->
        when (part) {
            is PartData.FormItem -> {
                when (part.name) {
                    "uploadId" -> uploadId = part.value
                    "chunkNumber" -> chunkNumber = part.value.toIntOrNull()
                    "totalChunks" -> totalChunks = part.value.toIntOrNull()
                }
            }

            is PartData.FileItem -> {
                originalFileName = part.originalFileName
                fileBytes = part.streamProvider().readBytes()
            }

            else -> {}
        }
        part.dispose()
    }

    if (uploadId != null && chunkNumber != null && totalChunks != null && originalFileName != null && fileBytes != null) {
            try {
                S3Config.makeClient()?.let {
                    S3SystemService.uploadMultipart(it, originalFileName, uploadId, chunkNumber!!, fileBytes, totalChunks!!)
                }
                call.respond(HttpStatusCode.OK, "Chunk $chunkNumber uploaded successfully")
            } catch (e: S3Exception) {
                call.respond(HttpStatusCode.BadRequest, "Error uploading chunk ${e.message}")
            }
    } else {
        call.respond(HttpStatusCode.BadRequest, "Invalid upload data")
    }
}

Ensuite, le chunk est uploadé sur le serveur S3 et stocké pour reconstituer le fichier.

suspend fun uploadMultipart(client: S3Client, key: String, uploadId: String?, chunkNumber: Int, fileBytes: ByteArray?, totalChunks: Int): String? {
    try {
        val part = client.uploadPart(UploadPartRequest {
            bucket = S3Config.bucketName
            this.key = key
            this.uploadId = uploadId
            partNumber = chunkNumber
            body = ByteStream.fromBytes(fileBytes!!)
        }).let {
            CompletedPart {
                checksumSha256 = it.checksumSha256
                partNumber = chunkNumber
                eTag = it.eTag
            }
        }

        uploads[uploadId!!]?.add(part)

    } catch (e: S3Exception) {
       println("Error uploading file: ${e.message}")
    }

    return uploadId
}

Reconstitution du fichier

  1. Indiquer la fin de l'upload du fichier pour le reconstituer.
await fetch("/folders/upload/complete", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
    },
    body: JSON.stringify({
        upload_id: uploadId.toString(),
        filename: file.name,
    }),
});
  1. On reconstruit le fichier sur le serveur S3.

Comme on peut le voir, l'uploadId a suivi tout le processus d'upload multipart. Grâce à cet identifiant et au nom du fichier, S3 retrouve les morceaux et reconstitue le fichier.

suspend fun completeMultipartUpload(client: S3Client, remotePath: String, uplId: String?) {
    client.completeMultipartUpload(CompleteMultipartUploadRequest {
        bucket = S3Config.bucketName
        this.key = remotePath
        this.uploadId = uplId
        multipartUpload = CompletedMultipartUpload {
            parts = uploads[uplId!!]
            bucket = S3Config.bucketName
            key = remotePath
            uploadId = uplId
        }
    }).also { uploads.remove(uplId) }
}

0 Commentaire