Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Share): Generate zip file on the fly #727

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
Warnings:

- You are about to drop the column `isZipReady` on the `Share` table. All the data in the column will be lost.

*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Share" (
"id" TEXT NOT NULL PRIMARY KEY,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"name" TEXT,
"uploadLocked" BOOLEAN NOT NULL DEFAULT false,
"views" INTEGER NOT NULL DEFAULT 0,
"expiration" DATETIME NOT NULL,
"description" TEXT,
"removedReason" TEXT,
"creatorId" TEXT,
"reverseShareId" TEXT,
"storageProvider" TEXT NOT NULL DEFAULT 'LOCAL',
CONSTRAINT "Share_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "Share_reverseShareId_fkey" FOREIGN KEY ("reverseShareId") REFERENCES "ReverseShare" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_Share" ("createdAt", "creatorId", "description", "expiration", "id", "name", "removedReason", "reverseShareId", "storageProvider", "uploadLocked", "views") SELECT "createdAt", "creatorId", "description", "expiration", "id", "name", "removedReason", "reverseShareId", "storageProvider", "uploadLocked", "views" FROM "Share";
DROP TABLE "Share";
ALTER TABLE "new_Share" RENAME TO "Share";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
1 change: 0 additions & 1 deletion backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ model Share {

name String?
uploadLocked Boolean @default(false)
isZipReady Boolean @default(false)
views Int @default(0)
expiration DateTime
description String?
Expand Down
2 changes: 1 addition & 1 deletion backend/src/file/file.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class FileController {
@Res({ passthrough: true }) res: Response,
@Param("shareId") shareId: string,
) {
const zipStream = this.fileService.getZip(shareId);
const zipStream = await this.fileService.getZip(shareId);

res.set({
"Content-Type": "application/zip",
Expand Down
26 changes: 22 additions & 4 deletions backend/src/file/file.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { Injectable } from "@nestjs/common";
import { LocalFileService } from "./local.service";
import { S3FileService } from "./s3.service";
import { ConfigService } from "src/config/config.service";
import { Readable } from "stream";
import { PassThrough, Readable } from "stream";
import { PrismaService } from "../prisma/prisma.service";
import * as archiver from "archiver";
import * as fs from "fs";
import { SHARE_DIRECTORY } from "src/constants";

@Injectable()
export class FileService {
Expand Down Expand Up @@ -59,9 +62,24 @@ export class FileService {
return storageService.deleteAllFiles(shareId);
}

getZip(shareId: string) {
const storageService = this.getStorageService();
return storageService.getZip(shareId) as Readable;
async getZip(shareId: string) {
const passThrough = new PassThrough();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put the logic into the local.service.ts because file.service.ts is just a wrapper of local.service.ts and s3.service.ts.

With that it would also be easier to add the zip functionality to S3 later.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, it seems that it's not storage related and therefore the file.service.ts is the better place. It's reading all the related files from any of the storage backends, and put it in a ZIP. Before, you had the getZip in the storage specific layer, as there was a zip file already in the storage.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say that the functionality there is storage related. Especially the call to fs.createReadStream would be handled differently with S3.

const path = `${SHARE_DIRECTORY}/${shareId}`;

const files = await this.prisma.file.findMany({ where: { shareId } });
const archive = archiver("zip", {
zlib: { level: this.configService.get("share.zipCompressionLevel") },
});

for (const file of files) {
archive.append(fs.createReadStream(`${path}/${file.id}`), {
name: file.name,
});
}

archive.pipe(passThrough);
archive.finalize();
return passThrough as Readable;
}

private async streamToUint8Array(stream: Readable): Promise<Uint8Array> {
Expand Down
3 changes: 0 additions & 3 deletions backend/src/share/dto/shareMetaData.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ export class ShareMetaDataDTO {
@Expose()
id: string;

@Expose()
isZipReady: boolean;

from(partial: Partial<ShareMetaDataDTO>) {
return plainToClass(ShareMetaDataDTO, partial, {
excludeExtraneousValues: true,
Expand Down
8 changes: 1 addition & 7 deletions backend/src/share/share.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,6 @@ export class ShareService {
"You need at least on file in your share to complete it.",
);

// Asynchronously create a zip of all files
if (share.files.length > 1)
this.createZip(id).then(() =>
this.prisma.share.update({ where: { id }, data: { isZipReady: true } }),
);

// Send email for each recipient
for (const recipient of share.recipients) {
await this.emailService.sendMailToShareRecipients(
Expand Down Expand Up @@ -200,7 +194,7 @@ export class ShareService {
async revertComplete(id: string) {
return this.prisma.share.update({
where: { id },
data: { uploadLocked: false, isZipReady: false },
data: { uploadLocked: false },
});
}

Expand Down
34 changes: 2 additions & 32 deletions frontend/src/components/share/DownloadAllButton.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { Button } from "@mantine/core";
import { useEffect, useState } from "react";
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import useTranslate from "../../hooks/useTranslate.hook";
import shareService from "../../services/share.service";
import toast from "../../utils/toast.util";

const DownloadAllButton = ({ shareId }: { shareId: string }) => {
const [isZipReady, setIsZipReady] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const t = useTranslate();

const downloadAll = async () => {
setIsLoading(true);
Expand All @@ -17,37 +13,11 @@ const DownloadAllButton = ({ shareId }: { shareId: string }) => {
.then(() => setIsLoading(false));
};

useEffect(() => {
shareService
.getMetaData(shareId)
.then((share) => setIsZipReady(share.isZipReady))
.catch(() => {});

const timer = setInterval(() => {
shareService
.getMetaData(shareId)
.then((share) => {
setIsZipReady(share.isZipReady);
if (share.isZipReady) clearInterval(timer);
})
.catch(() => clearInterval(timer));
}, 5000);
return () => {
clearInterval(timer);
};
}, []);

return (
<Button
variant="outline"
loading={isLoading}
onClick={() => {
if (!isZipReady) {
toast.error(t("share.notify.download-all-preparing"));
} else {
downloadAll();
}
}}
onClick={() => downloadAll()}
>
<FormattedMessage id="share.button.download-all" />
</Button>
Expand Down
1 change: 0 additions & 1 deletion frontend/src/i18n/translations/de-DE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,6 @@ export default {
"share.modal.password": "Passwort",
"share.modal.error.invalid-password": "Ungültiges Passwort",
"share.button.download-all": "Alles herunterladen",
"share.notify.download-all-preparing": "Die Freigabe wird vorbereitet. Bitte versuche es in ein paar Minuten erneut.",
"share.modal.file-link": "Dateilink",
"share.table.name": "Name",
"share.table.size": "Größe",
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/i18n/translations/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,6 @@ export default {
"share.modal.error.invalid-password": "Invalid password",

"share.button.download-all": "Download all",
"share.notify.download-all-preparing":
"The share is being prepared. Please try again in a few minutes.",

"share.modal.file-link": "File link",
"share.table.name": "Name",
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/share/[shareId]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ const Share = ({ shareId }: { shareId: string }) => {
<Title order={3}>{share?.name || share?.id}</Title>
<Text size="sm">{share?.description}</Text>
</Box>
{share?.files.length > 1 && <DownloadAllButton shareId={shareId} />}
<DownloadAllButton shareId={shareId} />
</Group>

<FileList
Expand Down
1 change: 0 additions & 1 deletion frontend/src/types/share.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export type CreateShare = {

export type ShareMetaData = {
id: string;
isZipReady: boolean;
};

export type MyShare = Omit<Share, "hasPassword"> & {
Expand Down
Loading