fix: memory leak while uploading files by disabling base64 encoding of chunks

This commit is contained in:
Elias Schneider
2024-04-04 20:55:45 +02:00
parent 0bfbaea49a
commit 7a15fbb465
4 changed files with 46 additions and 67 deletions

View File

@@ -27,17 +27,14 @@ export class FileController {
@UseGuards(CreateShareGuard, ShareOwnerGuard) @UseGuards(CreateShareGuard, ShareOwnerGuard)
async create( async create(
@Query() query: any, @Query() query: any,
@Body() body: string, @Body() body: string,
@Param("shareId") shareId: string, @Param("shareId") shareId: string,
) { ) {
const { id, name, chunkIndex, totalChunks } = query; const { id, name, chunkIndex, totalChunks } = query;
// Data can be empty if the file is empty // Data can be empty if the file is empty
const data = body.toString().split(",")[1] ?? "";
return await this.fileService.create( return await this.fileService.create(
data, body,
{ index: parseInt(chunkIndex), total: parseInt(totalChunks) }, { index: parseInt(chunkIndex), total: parseInt(totalChunks) },
{ id, name }, { id, name },
shareId, shareId,

View File

@@ -1,22 +1,19 @@
import { Button, Group } from "@mantine/core"; import { Button, Group } from "@mantine/core";
import { useModals } from "@mantine/modals";
import { cleanNotifications } from "@mantine/notifications"; import { cleanNotifications } from "@mantine/notifications";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { useRouter } from "next/router";
import pLimit from "p-limit"; import pLimit from "p-limit";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import Dropzone from "../../components/upload/Dropzone"; import Dropzone from "../../components/upload/Dropzone";
import FileList from "../../components/upload/FileList"; import FileList from "../../components/upload/FileList";
import showCompletedUploadModal from "../../components/upload/modals/showCompletedUploadModal";
import useConfig from "../../hooks/config.hook"; import useConfig from "../../hooks/config.hook";
import useTranslate from "../../hooks/useTranslate.hook"; import useTranslate from "../../hooks/useTranslate.hook";
import shareService from "../../services/share.service"; import shareService from "../../services/share.service";
import { FileListItem, FileMetaData, FileUpload } from "../../types/File.type"; import { FileListItem, FileMetaData, FileUpload } from "../../types/File.type";
import toast from "../../utils/toast.util"; import toast from "../../utils/toast.util";
import { useRouter } from "next/router";
const promiseLimit = pLimit(3); const promiseLimit = pLimit(3);
const chunkSize = 10 * 1024 * 1024; // 10MB
let errorToastShown = false; let errorToastShown = false;
const EditableUpload = ({ const EditableUpload = ({
@@ -33,6 +30,8 @@ const EditableUpload = ({
const router = useRouter(); const router = useRouter();
const config = useConfig(); const config = useConfig();
const chunkSize = useRef(parseInt(config.get("share.chunkSize")));
const [existingFiles, setExistingFiles] = const [existingFiles, setExistingFiles] =
useState<Array<FileMetaData & { deleted?: boolean }>>(savedFiles); useState<Array<FileMetaData & { deleted?: boolean }>>(savedFiles);
const [uploadingFiles, setUploadingFiles] = useState<FileUpload[]>([]); const [uploadingFiles, setUploadingFiles] = useState<FileUpload[]>([]);
@@ -66,7 +65,7 @@ const EditableUpload = ({
const fileUploadPromises = files.map(async (file, fileIndex) => const fileUploadPromises = files.map(async (file, fileIndex) =>
// Limit the number of concurrent uploads to 3 // Limit the number of concurrent uploads to 3
promiseLimit(async () => { promiseLimit(async () => {
let fileId: string; let fileId: string | undefined;
const setFileProgress = (progress: number) => { const setFileProgress = (progress: number) => {
setUploadingFiles((files) => setUploadingFiles((files) =>
@@ -81,23 +80,20 @@ const EditableUpload = ({
setFileProgress(1); setFileProgress(1);
let chunks = Math.ceil(file.size / chunkSize); let chunks = Math.ceil(file.size / chunkSize.current);
// If the file is 0 bytes, we still need to upload 1 chunk // If the file is 0 bytes, we still need to upload 1 chunk
if (chunks == 0) chunks++; if (chunks == 0) chunks++;
for (let chunkIndex = 0; chunkIndex < chunks; chunkIndex++) { for (let chunkIndex = 0; chunkIndex < chunks; chunkIndex++) {
const from = chunkIndex * chunkSize; const from = chunkIndex * chunkSize.current;
const to = from + chunkSize; const to = from + chunkSize.current;
const blob = file.slice(from, to); const blob = file.slice(from, to);
try { try {
await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async (event) =>
await shareService await shareService
.uploadFile( .uploadFile(
shareId, shareId,
event, blob,
{ {
id: fileId, id: fileId,
name: file.name, name: file.name,
@@ -107,11 +103,6 @@ const EditableUpload = ({
) )
.then((response) => { .then((response) => {
fileId = response.id; fileId = response.id;
resolve(response);
})
.catch(reject);
reader.readAsDataURL(blob);
}); });
setFileProgress(((chunkIndex + 1) / chunks) * 100); setFileProgress(((chunkIndex + 1) / chunks) * 100);

View File

@@ -3,7 +3,7 @@ import { useModals } from "@mantine/modals";
import { cleanNotifications } from "@mantine/notifications"; import { cleanNotifications } from "@mantine/notifications";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import pLimit from "p-limit"; import pLimit from "p-limit";
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import Meta from "../../components/Meta"; import Meta from "../../components/Meta";
import Dropzone from "../../components/upload/Dropzone"; import Dropzone from "../../components/upload/Dropzone";
@@ -19,7 +19,6 @@ import { CreateShare, Share } from "../../types/share.type";
import toast from "../../utils/toast.util"; import toast from "../../utils/toast.util";
const promiseLimit = pLimit(3); const promiseLimit = pLimit(3);
const chunkSize = 10 * 1024 * 1024; // 10MB
let errorToastShown = false; let errorToastShown = false;
let createdShare: Share; let createdShare: Share;
@@ -38,6 +37,8 @@ const Upload = ({
const [files, setFiles] = useState<FileUpload[]>([]); const [files, setFiles] = useState<FileUpload[]>([]);
const [isUploading, setisUploading] = useState(false); const [isUploading, setisUploading] = useState(false);
const chunkSize = useRef(parseInt(config.get("share.chunkSize")));
maxShareSize ??= parseInt(config.get("share.maxSize")); maxShareSize ??= parseInt(config.get("share.maxSize"));
const uploadFiles = async (share: CreateShare, files: FileUpload[]) => { const uploadFiles = async (share: CreateShare, files: FileUpload[]) => {
@@ -54,7 +55,7 @@ const Upload = ({
const fileUploadPromises = files.map(async (file, fileIndex) => const fileUploadPromises = files.map(async (file, fileIndex) =>
// Limit the number of concurrent uploads to 3 // Limit the number of concurrent uploads to 3
promiseLimit(async () => { promiseLimit(async () => {
let fileId: string; let fileId;
const setFileProgress = (progress: number) => { const setFileProgress = (progress: number) => {
setFiles((files) => setFiles((files) =>
@@ -69,23 +70,20 @@ const Upload = ({
setFileProgress(1); setFileProgress(1);
let chunks = Math.ceil(file.size / chunkSize); let chunks = Math.ceil(file.size / chunkSize.current);
// If the file is 0 bytes, we still need to upload 1 chunk // If the file is 0 bytes, we still need to upload 1 chunk
if (chunks == 0) chunks++; if (chunks == 0) chunks++;
for (let chunkIndex = 0; chunkIndex < chunks; chunkIndex++) { for (let chunkIndex = 0; chunkIndex < chunks; chunkIndex++) {
const from = chunkIndex * chunkSize; const from = chunkIndex * chunkSize.current;
const to = from + chunkSize; const to = from + chunkSize.current;
const blob = file.slice(from, to); const blob = file.slice(from, to);
try { try {
await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async (event) =>
await shareService await shareService
.uploadFile( .uploadFile(
createdShare.id, createdShare.id,
event, blob,
{ {
id: fileId, id: fileId,
name: file.name, name: file.name,
@@ -95,11 +93,6 @@ const Upload = ({
) )
.then((response) => { .then((response) => {
fileId = response.id; fileId = response.id;
resolve(response);
})
.catch(reject);
reader.readAsDataURL(blob);
}); });
setFileProgress(((chunkIndex + 1) / chunks) * 100); setFileProgress(((chunkIndex + 1) / chunks) * 100);

View File

@@ -77,7 +77,7 @@ const removeFile = async (shareId: string, fileId: string) => {
const uploadFile = async ( const uploadFile = async (
shareId: string, shareId: string,
readerEvent: ProgressEvent<FileReader>, chunk: Blob,
file: { file: {
id?: string; id?: string;
name: string; name: string;
@@ -85,10 +85,8 @@ const uploadFile = async (
chunkIndex: number, chunkIndex: number,
totalChunks: number, totalChunks: number,
): Promise<FileUploadResponse> => { ): Promise<FileUploadResponse> => {
const data = readerEvent.target!.result;
return ( return (
await api.post(`shares/${shareId}/files`, data, { await api.post(`shares/${shareId}/files`, chunk, {
headers: { "Content-Type": "application/octet-stream" }, headers: { "Content-Type": "application/octet-stream" },
params: { params: {
id: file.id, id: file.id,