Compare commits

..

26 Commits

Author SHA1 Message Date
Elias Schneider
bfb47ba6e8 release: 0.3.6 2022-12-13 18:45:52 +01:00
Elias Schneider
c1d87a1c29 test: improve tests for new feature 2022-12-13 18:44:17 +01:00
Elias Schneider
4c7e161217 chore: create prisma migration 2022-12-13 18:39:13 +01:00
Elias Schneider
844c47e129 fix: rerange accordion items 2022-12-13 09:57:48 +01:00
Elias Schneider
9b0c08d0cd fix: remove dot in email link 2022-12-13 09:06:18 +01:00
Elias Schneider
37fda220e9 Merge branch 'main' of https://github.com/stonith404/pingvin-share 2022-12-12 22:38:40 +01:00
Elias Schneider
3b7f5ddc52 Create close_inactive_issues.yml 2022-12-12 14:34:36 +01:00
Elias Schneider
8728fa5207 feat: add description field to share 2022-12-12 11:54:13 +01:00
Elias Schneider
c265129dcc Create SECURITY.md 2022-12-12 11:11:28 +01:00
Elias Schneider
78dd4a7e2a chore: add issue templates 2022-12-12 11:00:10 +01:00
Elias Schneider
3cad4dd487 docs: add synology nas installation by Marius 2022-12-11 12:38:58 +01:00
Elias Schneider
d1d3462056 release: 0.3.5 2022-12-11 12:23:46 +01:00
Elias Schneider
5b01108777 fix: zip doesn't contain file extension 2022-12-11 12:22:01 +01:00
Elias Schneider
3d1d4d0fc7 fix: only create zip if more than one file is in the share 2022-12-11 12:19:42 +01:00
Elias Schneider
7c0d62a429 Update FUNDING.yml 2022-12-10 23:26:57 +01:00
Elias Schneider
d010a8a2d3 feat: upload 3 files at same time 2022-12-10 23:16:10 +01:00
Elias Schneider
9798e26872 fix: setup wizard table doesn't take full width 2022-12-10 18:45:53 +01:00
Elias Schneider
0c10dc674f Merge pull request #37 from Neyxo/improvement-30-docker-image
Improved docker image (size & speed)
2022-12-10 18:33:45 +01:00
Elias Schneider
084e911eed fix: remove unnecessary port expose 2022-12-10 18:32:14 +01:00
Elias Schneider
797f8938ca fix: use node slim to fix arm builds 2022-12-10 18:31:39 +01:00
Elias Schneider
05cbb7b27e fix: jobs never get executed 2022-12-10 17:16:49 +01:00
Elias Schneider
905bab9c86 release: 0.3.4 2022-12-10 15:46:53 +01:00
Jean-Michel Carrel
8e38c5fed7 Improved docker image (size & speed) 2022-12-10 15:34:01 +01:00
Elias Schneider
7e877ce9f4 fix: show alternative to copy button if site is not using https 2022-12-10 13:16:23 +01:00
Elias Schneider
b1bfb09dfd fix: tables on mobile 2022-12-09 14:37:09 +01:00
Elias Schneider
c8a4521677 fix: sign up page available when registration is disabled 2022-12-09 12:05:43 +01:00
39 changed files with 547 additions and 197 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
# These are supported funding model platforms
github: stonith404

45
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: "🐛 Bug Report"
description: "Submit a bug report to help us improve"
title: "🐛 Bug Report: "
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out our bug report form 🙏
- type: textarea
id: steps-to-reproduce
validations:
required: true
attributes:
label: "👟 Reproduction steps"
description: "How do you trigger this bug? Please walk us through it step by step."
placeholder: "When I ..."
- type: textarea
id: expected-behavior
validations:
required: true
attributes:
label: "👍 Expected behavior"
description: "What did you think would happen?"
placeholder: "It should ..."
- type: textarea
id: actual-behavior
validations:
required: true
attributes:
label: "👎 Actual Behavior"
description: "What did actually happen? Add screenshots, if applicable."
placeholder: "It actually ..."
- type: input
id: operating-system
attributes:
label: "🌐 Browser"
description: "Which browser do you use?"
placeholder: "Firefox"
validations:
required: true
- type: markdown
attributes:
value: |
Before submitting, please check if the issues hasn't been raised before.

29
.github/ISSUE_TEMPLATE/feature.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: 🚀 Feature
description: "Submit a proposal for a new feature"
title: "🚀 Feature: "
labels: [feature]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out our feature request form 🙏
- type: textarea
id: feature-description
validations:
required: true
attributes:
label: "🔖 Feature description"
description: "A clear and concise description of what the feature is."
placeholder: "You should add ..."
- type: textarea
id: pitch
validations:
required: true
attributes:
label: "🎤 Pitch"
description: "Please explain why this feature should be implemented and how it would be used. Add examples, if applicable."
placeholder: "In my use-case, ..."
- type: markdown
attributes:
value: |
Before submitting, please check if the issues hasn't been raised before.

View File

@@ -0,0 +1,22 @@
name: Close inactive issues
on:
schedule:
- cron: "00 00 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v4
with:
days-before-issue-stale: 30
days-before-issue-close: 14
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,3 +1,42 @@
### [0.3.6](https://github.com/stonith404/pingvin-share/compare/v0.3.5...v0.3.6) (2022-12-13)
### Features
* add description field to share ([8728fa5](https://github.com/stonith404/pingvin-share/commit/8728fa5207524e9aee26d68eafe1b6fff367d749))
### Bug Fixes
* remove dot in email link ([9b0c08d](https://github.com/stonith404/pingvin-share/commit/9b0c08d0cdeeeef217ccba57f593fea9d8858371))
* rerange accordion items ([844c47e](https://github.com/stonith404/pingvin-share/commit/844c47e1290fb0f7dedb41a18be59ed5ab83dabc))
### [0.3.5](https://github.com/stonith404/pingvin-share/compare/v0.3.4...v0.3.5) (2022-12-11)
### Features
* upload 3 files at same time ([d010a8a](https://github.com/stonith404/pingvin-share/commit/d010a8a2d366708b1bb5088e9c1e9f9378d3e023))
### Bug Fixes
* jobs never get executed ([05cbb7b](https://github.com/stonith404/pingvin-share/commit/05cbb7b27ef98a3a80dd9edc318f1dcc9a8bd442))
* only create zip if more than one file is in the share ([3d1d4d0](https://github.com/stonith404/pingvin-share/commit/3d1d4d0fc7c0351724387c3721280c334ae94d98))
* remove unnecessary port expose ([084e911](https://github.com/stonith404/pingvin-share/commit/084e911eed95eb22fea0bf185803ba32c3eda1a9))
* setup wizard table doesn't take full width ([9798e26](https://github.com/stonith404/pingvin-share/commit/9798e26872064edc1049138cf73479b1354a43ed))
* use node slim to fix arm builds ([797f893](https://github.com/stonith404/pingvin-share/commit/797f8938cac9cc3bb788f632d97eba5c49fe98a5))
* zip doesn't contain file extension ([5b01108](https://github.com/stonith404/pingvin-share/commit/5b0110877745f1fcde4952737a93c07ef4a2a92d))
### [0.3.4](https://github.com/stonith404/pingvin-share/compare/v0.3.3...v0.3.4) (2022-12-10)
### Bug Fixes
* show alternative to copy button if site is not using https ([7e877ce](https://github.com/stonith404/pingvin-share/commit/7e877ce9f4b82d61c9b238e17def9f4c29e7aeb8))
* sign up page available when registration is disabled ([c8a4521](https://github.com/stonith404/pingvin-share/commit/c8a4521677280d6aba89d293a1fe0c38adf9f92c))
* tables on mobile ([b1bfb09](https://github.com/stonith404/pingvin-share/commit/b1bfb09dfd5c90cc18847470a9ce1ce8397c1476))
### [0.3.3](https://github.com/stonith404/pingvin-share/compare/v0.3.2...v0.3.3) (2022-12-08) ### [0.3.3](https://github.com/stonith404/pingvin-share/compare/v0.3.2...v0.3.3) (2022-12-08)

View File

@@ -1,27 +1,44 @@
FROM node:18-slim AS frontend-builder # Using node slim because prisma ORM needs libc for ARM builds
# Stage 1: on frontend dependency change
FROM node:18-slim AS frontend-dependencies
WORKDIR /opt/app WORKDIR /opt/app
COPY frontend/package.json frontend/package-lock.json ./ COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci RUN npm ci
# Stage 2: on frontend change
FROM node:18-slim AS frontend-builder
WORKDIR /opt/app
COPY ./frontend . COPY ./frontend .
COPY --from=frontend-dependencies /opt/app/node_modules ./node_modules
RUN npm run build RUN npm run build
FROM node:18-slim AS backend-builder # Stage 3: on backend dependency change
RUN apt-get update && apt-get install -y openssl FROM node:18-slim AS backend-dependencies
WORKDIR /opt/app WORKDIR /opt/app
COPY backend/package.json backend/package-lock.json ./ COPY backend/package.json backend/package-lock.json ./
RUN npm ci RUN npm ci
COPY ./backend .
RUN npx prisma generate
RUN npm run build
# Stage 4:on backend change
FROM node:18-slim AS backend-builder
RUN apt-get update && apt-get install -y openssl
WORKDIR /opt/app
COPY ./backend .
COPY --from=backend-dependencies /opt/app/node_modules ./node_modules
RUN npx prisma generate
RUN npm run build && npm prune --production
# Stage 5: Final image
FROM node:18-slim AS runner FROM node:18-slim AS runner
ENV NODE_ENV=production ENV NODE_ENV=production
RUN apt-get update && apt-get install -y openssl RUN apt-get update && apt-get install -y openssl
WORKDIR /opt/app/frontend WORKDIR /opt/app/frontend
COPY --from=frontend-builder /opt/app/next.config.js .
COPY --from=frontend-builder /opt/app/public ./public COPY --from=frontend-builder /opt/app/public ./public
COPY --from=frontend-builder /opt/app/.next ./.next # Automatically leverage output traces to reduce image size
COPY --from=frontend-builder /opt/app/node_modules ./node_modules # https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=frontend-builder /opt/app/.next/standalone ./
COPY --from=frontend-builder /opt/app/.next/static ./.next/static
WORKDIR /opt/app/backend WORKDIR /opt/app/backend
COPY --from=backend-builder /opt/app/node_modules ./node_modules COPY --from=backend-builder /opt/app/node_modules ./node_modules
@@ -31,4 +48,4 @@ COPY --from=backend-builder /opt/app/package.json ./
WORKDIR /opt/app WORKDIR /opt/app
EXPOSE 3000 EXPOSE 3000
CMD cd frontend && node_modules/.bin/next start & cd backend && npm run prod CMD node frontend/server.js & cd backend && npm run prod

View File

@@ -23,11 +23,17 @@ Pingvin Share is self-hosted file sharing platform and an alternative for WeTran
> Pleas note that Pingvin Share is in early stage and could include some bugs > Pleas note that Pingvin Share is in early stage and could include some bugs
### Recommended installation
1. Download the `docker-compose.yml` file 1. Download the `docker-compose.yml` file
2. Run `docker-compose up -d` 2. Run `docker-compose up -d`
The website is now listening available on `http://localhost:3000`, have fun with Pingvin Share 🐧! The website is now listening available on `http://localhost:3000`, have fun with Pingvin Share 🐧!
### Additional resources
- [Synology NAS installation](https://mariushosting.com/how-to-install-pingvin-share-on-your-synology-nas/)
### Upgrade to a new version ### Upgrade to a new version
Run `docker compose pull && docker compose up -d` to update your docker container Run `docker compose pull && docker compose up -d` to update your docker container

7
SECURITY.md Normal file
View File

@@ -0,0 +1,7 @@
# Security Policy
## Supported Versions
As Pingvin Share is in beta, older versions don't get security updates. Please consider to update Pingvin Share regularly. Updates can be automated with e.g [Watchtower](https://github.com/containrrr/watchtower).
## Reporting a Vulnerability
Thank you for taking the time to report a vulnerability. Please DO NOT create an issue on GitHub because the vulnerability could get exploited. Instead please write an email to [elias@eliasschneider.com](mailto:elias@eliasschneider.com).

View File

@@ -22,6 +22,7 @@
"@nestjs/platform-express": "^9.2.1", "@nestjs/platform-express": "^9.2.1",
"@nestjs/schedule": "^2.1.0", "@nestjs/schedule": "^2.1.0",
"@nestjs/throttler": "^3.1.0", "@nestjs/throttler": "^3.1.0",
"@prisma/client": "^4.7.1",
"archiver": "^5.3.1", "archiver": "^5.3.1",
"argon2": "^0.30.2", "argon2": "^0.30.2",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
@@ -36,13 +37,13 @@
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.6.0" "rxjs": "^7.6.0",
"ts-node": "^10.9.1"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^9.1.5", "@nestjs/cli": "^9.1.5",
"@nestjs/schematics": "^9.0.3", "@nestjs/schematics": "^9.0.3",
"@nestjs/testing": "^9.2.1", "@nestjs/testing": "^9.2.1",
"@prisma/client": "^4.7.1",
"@types/archiver": "^5.3.1", "@types/archiver": "^5.3.1",
"@types/cron": "^2.0.0", "@types/cron": "^2.0.0",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
@@ -63,7 +64,6 @@
"prisma": "^4.7.1", "prisma": "^4.7.1",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"ts-loader": "^9.4.2", "ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"tsconfig-paths": "4.1.1", "tsconfig-paths": "4.1.1",
"typescript": "^4.9.3", "typescript": "^4.9.3",
"wait-on": "^6.0.1" "wait-on": "^6.0.1"

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Share" ADD COLUMN "description" TEXT;

View File

@@ -39,6 +39,7 @@ model Share {
isZipReady Boolean @default(false) isZipReady Boolean @default(false)
views Int @default(0) views Int @default(0)
expiration DateTime expiration DateTime
description String?
creatorId String? creatorId String?
creator User? @relation(fields: [creatorId], references: [id], onDelete: Cascade) creator User? @relation(fields: [creatorId], references: [id], onDelete: Cascade)

View File

@@ -13,6 +13,7 @@ import { FileModule } from "./file/file.module";
import { PrismaModule } from "./prisma/prisma.module"; import { PrismaModule } from "./prisma/prisma.module";
import { ShareModule } from "./share/share.module"; import { ShareModule } from "./share/share.module";
import { UserModule } from "./user/user.module"; import { UserModule } from "./user/user.module";
import { JobsModule } from "./jobs/jobs.module";
@Module({ @Module({
imports: [ imports: [
@@ -22,6 +23,7 @@ import { UserModule } from "./user/user.module";
EmailModule, EmailModule,
PrismaModule, PrismaModule,
ConfigModule, ConfigModule,
JobsModule,
UserModule, UserModule,
MulterModule.registerAsync({ MulterModule.registerAsync({
useFactory: (config: ConfigService) => ({ useFactory: (config: ConfigService) => ({

View File

@@ -28,7 +28,7 @@ export class EmailService {
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`, from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
to: recipientEmail, to: recipientEmail,
subject: "Files shared with you", subject: "Files shared with you",
text: `Hey!\n${creator.username} shared some files with you. View or dowload the files with this link: ${shareUrl}.\nShared securely with Pingvin Share 🐧`, text: `Hey!\n${creator.username} shared some files with you. View or dowload the files with this link: ${shareUrl}\nShared securely with Pingvin Share 🐧`,
}); });
} }
} }

View File

@@ -79,7 +79,7 @@ export class FileController {
const zip = this.fileService.getZip(shareId); const zip = this.fileService.getZip(shareId);
res.set({ res.set({
"Content-Type": "application/zip", "Content-Type": "application/zip",
"Content-Disposition": `attachment ; filename="pingvin-share-${shareId}"`, "Content-Disposition": `attachment ; filename="pingvin-share-${shareId}.zip"`,
}); });
return new StreamableFile(zip); return new StreamableFile(zip);

View File

@@ -0,0 +1,9 @@
import { Module } from "@nestjs/common";
import { FileModule } from "src/file/file.module";
import { JobsService } from "./jobs.service";
@Module({
imports: [FileModule],
providers: [JobsService],
})
export class JobsModule {}

View File

@@ -1,5 +1,4 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
@Injectable() @Injectable()
@@ -8,7 +7,7 @@ export class PrismaService extends PrismaClient {
super({ super({
datasources: { datasources: {
db: { db: {
url: "file:../data/pingvin-share.db", url: "file:../data/pingvin-share.db?connection_limit=1",
}, },
}, },
}); });

View File

@@ -1,9 +1,11 @@
import { Type } from "class-transformer"; import { Type } from "class-transformer";
import { import {
IsEmail, IsEmail,
IsOptional,
IsString, IsString,
Length, Length,
Matches, Matches,
MaxLength,
ValidateNested, ValidateNested,
} from "class-validator"; } from "class-validator";
import { ShareSecurityDTO } from "./shareSecurity.dto"; import { ShareSecurityDTO } from "./shareSecurity.dto";
@@ -19,6 +21,10 @@ export class CreateShareDTO {
@IsString() @IsString()
expiration: string; expiration: string;
@MaxLength(512)
@IsOptional()
description: string;
@IsEmail({}, { each: true }) @IsEmail({}, { each: true })
recipients: string[]; recipients: string[];

View File

@@ -17,6 +17,9 @@ export class ShareDTO {
@Type(() => PublicUserDTO) @Type(() => PublicUserDTO)
creator: PublicUserDTO; creator: PublicUserDTO;
@Expose()
description: string;
from(partial: Partial<ShareDTO>) { from(partial: Partial<ShareDTO>) {
return plainToClass(ShareDTO, partial, { excludeExtraneousValues: true }); return plainToClass(ShareDTO, partial, { excludeExtraneousValues: true });
} }

View File

@@ -105,9 +105,10 @@ export class ShareService {
); );
// Asynchronously create a zip of all files // Asynchronously create a zip of all files
this.createZip(id).then(() => if (share.files.length > 1)
this.prisma.share.update({ where: { id }, data: { isZipReady: true } }) this.createZip(id).then(() =>
); this.prisma.share.update({ where: { id }, data: { isZipReady: true } })
);
// Send email for each recepient // Send email for each recepient
for (const recepient of share.recipients) { for (const recepient of share.recipients) {

View File

@@ -1,4 +1,4 @@
import { PickType } from "@nestjs/mapped-types"; import { PickType } from "@nestjs/mapped-types";
import { UserDTO } from "./user.dto"; import { UserDTO } from "./user.dto";
export class PublicUserDTO extends PickType(UserDTO, ["email"] as const) {} export class PublicUserDTO extends PickType(UserDTO, ["username"] as const) {}

View File

@@ -1,6 +1,6 @@
{ {
"info": { "info": {
"_postman_id": "243b0832-3a6a-4389-bb71-4d988c0a86d9", "_postman_id": "84a95987-2997-429a-aba6-d38289b0b76a",
"name": "Pingvin Share Testing", "name": "Pingvin Share Testing",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "17822132" "_exporter_id": "17822132"
@@ -431,7 +431,7 @@
" const responseBody = pm.response.json();", " const responseBody = pm.response.json();",
" pm.expect(responseBody).to.have.property(\"id\")", " pm.expect(responseBody).to.have.property(\"id\")",
" pm.expect(responseBody).to.have.property(\"expiration\")", " pm.expect(responseBody).to.have.property(\"expiration\")",
" pm.expect(Object.keys(responseBody).length).be.equal(2)", " pm.expect(Object.keys(responseBody).length).be.equal(3)",
"});", "});",
"" ""
], ],
@@ -517,6 +517,60 @@
}, },
"response": [] "response": []
}, },
{
"name": "Upload file 2",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 201\", () => {",
" pm.response.to.have.status(201);",
"});",
"",
"pm.test(\"Response body correct\", () => {",
" const responseBody = pm.response.json();",
" pm.expect(responseBody).to.have.property(\"id\")",
" pm.expect(Object.keys(responseBody).length).be.equal(1)",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "formdata",
"formdata": [
{
"key": "file",
"type": "file",
"src": "./test/system/test-file.txt"
}
]
},
"url": {
"raw": "{{API_URL}}/shares/:shareId/files",
"host": [
"{{API_URL}}"
],
"path": [
"shares",
":shareId",
"files"
],
"variable": [
{
"key": "shareId",
"value": "test-share"
}
]
}
},
"response": []
},
{ {
"name": "Complete share", "name": "Complete share",
"event": [ "event": [
@@ -532,7 +586,7 @@
" const responseBody = pm.response.json();", " const responseBody = pm.response.json();",
" pm.expect(responseBody).to.have.property(\"id\")", " pm.expect(responseBody).to.have.property(\"id\")",
" pm.expect(responseBody).to.have.property(\"expiration\")", " pm.expect(responseBody).to.have.property(\"expiration\")",
" pm.expect(Object.keys(responseBody).length).be.equal(2)", " pm.expect(Object.keys(responseBody).length).be.equal(3)",
"});", "});",
"" ""
], ],
@@ -942,9 +996,9 @@
" pm.response.to.have.status(200);", " pm.response.to.have.status(200);",
"});", "});",
"", "",
"pm.test(\"Response contains 1 file\", () => {", "pm.test(\"Response contains 2 files\", () => {",
" const responseBody = pm.response.json();", " const responseBody = pm.response.json();",
" pm.expect(responseBody.files.length).be.equal(1)", " pm.expect(responseBody.files.length).be.equal(2)",
"});", "});",
"", "",
"", "",

View File

@@ -1,10 +1,8 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const withPWA = require("next-pwa")({ const withPWA = require("next-pwa")({
dest: "public", dest: "public",
disable: process.env.NODE_ENV == "development" disable: process.env.NODE_ENV == "development",
}); });
module.exports = withPWA({ output: "standalone" });
module.exports = withPWA();

View File

@@ -26,6 +26,7 @@
"next-cookies": "^2.0.3", "next-cookies": "^2.0.3",
"next-http-proxy-middleware": "^1.2.5", "next-http-proxy-middleware": "^1.2.5",
"next-pwa": "^5.6.0", "next-pwa": "^5.6.0",
"p-limit": "^4.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "^4.7.1", "react-icons": "^4.7.1",
@@ -6161,6 +6162,35 @@
} }
}, },
"node_modules/p-limit": { "node_modules/p-limit": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
"integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
"dependencies": {
"yocto-queue": "^1.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"dependencies": {
"p-limit": "^3.0.2"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate/node_modules/p-limit": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
@@ -6175,14 +6205,11 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/p-locate": { "node_modules/p-locate/node_modules/yocto-queue": {
"version": "5.0.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true, "dev": true,
"dependencies": {
"p-limit": "^3.0.2"
},
"engines": { "engines": {
"node": ">=10" "node": ">=10"
}, },
@@ -8049,12 +8076,11 @@
} }
}, },
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
"dev": true,
"engines": { "engines": {
"node": ">=10" "node": ">=12.20"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
@@ -12482,12 +12508,11 @@
} }
}, },
"p-limit": { "p-limit": {
"version": "3.1.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
"dev": true,
"requires": { "requires": {
"yocto-queue": "^0.1.0" "yocto-queue": "^1.0.0"
} }
}, },
"p-locate": { "p-locate": {
@@ -12497,6 +12522,23 @@
"dev": true, "dev": true,
"requires": { "requires": {
"p-limit": "^3.0.2" "p-limit": "^3.0.2"
},
"dependencies": {
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"requires": {
"yocto-queue": "^0.1.0"
}
},
"yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
}
} }
}, },
"p-map": { "p-map": {
@@ -13865,10 +13907,9 @@
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
}, },
"yocto-queue": { "yocto-queue": {
"version": "0.1.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g=="
"dev": true
}, },
"yup": { "yup": {
"version": "0.32.11", "version": "0.32.11",

View File

@@ -27,6 +27,7 @@
"next-cookies": "^2.0.3", "next-cookies": "^2.0.3",
"next-http-proxy-middleware": "^1.2.5", "next-http-proxy-middleware": "^1.2.5",
"next-pwa": "^5.6.0", "next-pwa": "^5.6.0",
"p-limit": "^4.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "^4.7.1", "react-icons": "^4.7.1",

View File

@@ -0,0 +1,16 @@
import { Stack, TextInput } from "@mantine/core";
import { ModalsContextProps } from "@mantine/modals/lib/context";
const showShareLinkModal = (modals: ModalsContextProps, shareId: string) => {
const link = `${window.location.origin}/share/${shareId}`;
return modals.openModal({
title: "Share link",
children: (
<Stack align="stretch">
<TextInput variant="filled" value={link} />
</Stack>
),
});
};
export default showShareLinkModal;

View File

@@ -1,4 +1,12 @@
import { ActionIcon, Code, Group, Skeleton, Table, Text } from "@mantine/core"; import {
ActionIcon,
Box,
Code,
Group,
Skeleton,
Table,
Text,
} from "@mantine/core";
import { useModals } from "@mantine/modals"; import { useModals } from "@mantine/modals";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { TbEdit, TbLock } from "react-icons/tb"; import { TbEdit, TbLock } from "react-icons/tb";
@@ -43,53 +51,55 @@ const AdminConfigTable = () => {
)); ));
return ( return (
<Table verticalSpacing="sm" horizontalSpacing="xl" withBorder> <Box sx={{ display: "block", overflowX: "auto" }}>
<thead> <Table verticalSpacing="sm" horizontalSpacing="xl" withBorder>
<tr> <thead>
<th>Key</th> <tr>
<th>Value</th> <th>Key</th>
<th></th> <th>Value</th>
</tr> <th></th>
</thead> </tr>
<tbody> </thead>
{isLoading <tbody>
? skeletonRows {isLoading
: configVariables.map((configVariable) => ( ? skeletonRows
<tr key={configVariable.key}> : configVariables.map((configVariable) => (
<td style={{ maxWidth: "200px" }}> <tr key={configVariable.key}>
<Code>{configVariable.key}</Code>{" "} <td style={{ maxWidth: "200px" }}>
{configVariable.secret && <TbLock />} <br /> <Code>{configVariable.key}</Code>{" "}
<Text size="xs" color="dimmed"> {configVariable.secret && <TbLock />} <br />
{configVariable.description} <Text size="xs" color="dimmed">
</Text> {configVariable.description}
</td> </Text>
<td> </td>
{configVariable.obscured <td>
? "•".repeat(configVariable.value.length) {configVariable.obscured
: configVariable.value} ? "•".repeat(configVariable.value.length)
</td> : configVariable.value}
<td> </td>
<Group position="right"> <td>
<ActionIcon <Group position="right">
color="primary" <ActionIcon
variant="light" color="primary"
size={25} variant="light"
onClick={() => size={25}
showUpdateConfigVariableModal( onClick={() =>
modals, showUpdateConfigVariableModal(
configVariable, modals,
getConfigVariables configVariable,
) getConfigVariables
} )
> }
<TbEdit /> >
</ActionIcon> <TbEdit />
</Group> </ActionIcon>
</td> </Group>
</tr> </td>
))} </tr>
</tbody> ))}
</Table> </tbody>
</Table>
</Box>
); );
}; };

View File

@@ -18,8 +18,8 @@ const ManageUserTable = ({
const modals = useModals(); const modals = useModals();
return ( return (
<Box sx={{ display: "block", overflowX: "auto", whiteSpace: "nowrap" }}> <Box sx={{ display: "block", overflowX: "auto" }}>
<Table verticalSpacing="sm" highlightOnHover> <Table verticalSpacing="sm">
<thead> <thead>
<tr> <tr>
<th>Username</th> <th>Username</th>

View File

@@ -9,35 +9,10 @@ const FileList = ({
shareId, shareId,
isLoading, isLoading,
}: { }: {
files: any[]; files?: any[];
shareId: string; shareId: string;
isLoading: boolean; isLoading: boolean;
}) => { }) => {
const rows = files.map((file) => (
<tr key={file.name}>
<td>{file.name}</td>
<td>{byteStringToHumanSizeString(file.size)}</td>
<td>
{file.uploadingState ? (
file.uploadingState != "finished" ? (
<Loader size={22} />
) : (
<TbCircleCheck color="green" size={22} />
)
) : (
<ActionIcon
size={25}
onClick={async () => {
await shareService.downloadFile(shareId, file.id);
}}
>
<TbDownload />
</ActionIcon>
)}
</td>
</tr>
));
return ( return (
<Table> <Table>
<thead> <thead>
@@ -47,7 +22,34 @@ const FileList = ({
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody>{isLoading ? skeletonRows : rows}</tbody> <tbody>
{isLoading
? skeletonRows
: files!.map((file) => (
<tr key={file.name}>
<td>{file.name}</td>
<td>{byteStringToHumanSizeString(file.size)}</td>
<td>
{file.uploadingState ? (
file.uploadingState != "finished" ? (
<Loader size={22} />
) : (
<TbCircleCheck color="green" size={22} />
)
) : (
<ActionIcon
size={25}
onClick={async () => {
await shareService.downloadFile(shareId, file.id);
}}
>
<TbDownload />
</ActionIcon>
)}
</td>
</tr>
))}
</tbody>
</Table> </Table>
); );
}; };

View File

@@ -40,14 +40,16 @@ const Body = ({ share }: { share: Share }) => {
variant="filled" variant="filled"
value={link} value={link}
rightSection={ rightSection={
<ActionIcon window.isSecureContext && (
onClick={() => { <ActionIcon
clipboard.copy(link); onClick={() => {
toast.success("Your link was copied to the keyboard."); clipboard.copy(link);
}} toast.success("Your link was copied to the keyboard.");
> }}
<TbCopy /> >
</ActionIcon> <TbCopy />
</ActionIcon>
)
} }
/> />
<Text <Text

View File

@@ -12,6 +12,7 @@ import {
Select, Select,
Stack, Stack,
Text, Text,
Textarea,
TextInput, TextInput,
Title, Title,
} from "@mantine/core"; } from "@mantine/core";
@@ -22,7 +23,7 @@ import { useState } from "react";
import { TbAlertCircle } from "react-icons/tb"; import { TbAlertCircle } from "react-icons/tb";
import * as yup from "yup"; import * as yup from "yup";
import shareService from "../../../services/share.service"; import shareService from "../../../services/share.service";
import { ShareSecurity } from "../../../types/share.type"; import { CreateShare } from "../../../types/share.type";
import ExpirationPreview from "../ExpirationPreview"; import ExpirationPreview from "../ExpirationPreview";
const showCreateUploadModal = ( const showCreateUploadModal = (
@@ -32,12 +33,7 @@ const showCreateUploadModal = (
allowUnauthenticatedShares: boolean; allowUnauthenticatedShares: boolean;
enableEmailRecepients: boolean; enableEmailRecepients: boolean;
}, },
uploadCallback: ( uploadCallback: (createShare: CreateShare) => void
id: string,
expiration: string,
recipients: string[],
security: ShareSecurity
) => void
) => { ) => {
return modals.openModal({ return modals.openModal({
title: <Title order={4}>Share</Title>, title: <Title order={4}>Share</Title>,
@@ -54,12 +50,7 @@ const CreateUploadModalBody = ({
uploadCallback, uploadCallback,
options, options,
}: { }: {
uploadCallback: ( uploadCallback: (createShare: CreateShare) => void;
id: string,
expiration: string,
recipients: string[],
security: ShareSecurity
) => void;
options: { options: {
isUserSignedIn: boolean; isUserSignedIn: boolean;
allowUnauthenticatedShares: boolean; allowUnauthenticatedShares: boolean;
@@ -88,6 +79,7 @@ const CreateUploadModalBody = ({
recipients: [] as string[], recipients: [] as string[],
password: undefined, password: undefined,
maxViews: undefined, maxViews: undefined,
description: undefined,
expiration_num: 1, expiration_num: 1,
expiration_unit: "-days", expiration_unit: "-days",
never_expires: false, never_expires: false,
@@ -116,9 +108,15 @@ const CreateUploadModalBody = ({
const expiration = form.values.never_expires const expiration = form.values.never_expires
? "never" ? "never"
: form.values.expiration_num + form.values.expiration_unit; : form.values.expiration_num + form.values.expiration_unit;
uploadCallback(values.link, expiration, values.recipients, { uploadCallback({
password: values.password, id: values.link,
maxViews: values.maxViews, expiration: expiration,
recipients: values.recipients,
description: values.description,
security: {
password: values.password,
maxViews: values.maxViews,
},
}); });
modals.closeAll(); modals.closeAll();
} }
@@ -228,6 +226,18 @@ const CreateUploadModalBody = ({
{ExpirationPreview({ form })} {ExpirationPreview({ form })}
</Text> </Text>
<Accordion> <Accordion>
<Accordion.Item value="description" sx={{ borderBottom: "none" }}>
<Accordion.Control>Description</Accordion.Control>
<Accordion.Panel>
<Stack align="stretch">
<Textarea
variant="filled"
placeholder="Note for the recepients"
{...form.getInputProps("description")}
/>
</Stack>
</Accordion.Panel>
</Accordion.Item>
{options.enableEmailRecepients && ( {options.enableEmailRecepients && (
<Accordion.Item value="recipients" sx={{ borderBottom: "none" }}> <Accordion.Item value="recipients" sx={{ borderBottom: "none" }}>
<Accordion.Control>Email recipients</Accordion.Control> <Accordion.Control>Email recipients</Accordion.Control>
@@ -258,6 +268,7 @@ const CreateUploadModalBody = ({
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
)} )}
<Accordion.Item value="security" sx={{ borderBottom: "none" }}> <Accordion.Item value="security" sx={{ borderBottom: "none" }}>
<Accordion.Control>Security options</Accordion.Control> <Accordion.Control>Security options</Accordion.Control>
<Accordion.Panel> <Accordion.Panel>

View File

@@ -73,7 +73,7 @@ function App({ Component, pageProps }: AppProps) {
<LoadingOverlay visible overlayOpacity={1} /> <LoadingOverlay visible overlayOpacity={1} />
) : ( ) : (
<ConfigContext.Provider value={configVariables}> <ConfigContext.Provider value={configVariables}>
<UserContext.Provider value={user}> <UserContext.Provider value={user} >
<LoadingOverlay visible={isLoading} overlayOpacity={1} /> <LoadingOverlay visible={isLoading} overlayOpacity={1} />
<Header /> <Header />
<Container> <Container>

View File

@@ -17,6 +17,7 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { TbLink, TbTrash } from "react-icons/tb"; import { TbLink, TbTrash } from "react-icons/tb";
import showShareLinkModal from "../../components/account/showShareLinkModal";
import Meta from "../../components/Meta"; import Meta from "../../components/Meta";
import useUser from "../../hooks/user.hook"; import useUser from "../../hooks/user.hook";
import shareService from "../../services/share.service"; import shareService from "../../services/share.service";
@@ -83,12 +84,16 @@ const MyShares = () => {
variant="light" variant="light"
size={25} size={25}
onClick={() => { onClick={() => {
clipboard.copy( if (window.isSecureContext) {
`${window.location.origin}/share/${share.id}` clipboard.copy(
); `${window.location.origin}/share/${share.id}`
toast.success( );
"Your link was copied to the keyboard." toast.success(
); "Your link was copied to the keyboard."
);
} else {
showShareLinkModal(modals, share.id);
}
}} }}
> >
<TbLink /> <TbLink />

View File

@@ -1,4 +1,4 @@
import { Button, Stack, Text, Title } from "@mantine/core"; import { Box, Button, Stack, Text, Title } from "@mantine/core";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react"; import { useState } from "react";
import AdminConfigTable from "../../components/admin/AdminConfigTable"; import AdminConfigTable from "../../components/admin/AdminConfigTable";
@@ -28,7 +28,9 @@ const Setup = () => {
<Logo height={80} width={80} /> <Logo height={80} width={80} />
<Title order={2}>Welcome to Pingvin Share</Title> <Title order={2}>Welcome to Pingvin Share</Title>
<Text>Let's customize Pingvin Share for you! </Text> <Text>Let's customize Pingvin Share for you! </Text>
<AdminConfigTable /> <Box style={{ width: "100%" }}>
<AdminConfigTable />
</Box>
<Button <Button
loading={isLoading} loading={isLoading}
onClick={async () => { onClick={async () => {

View File

@@ -10,7 +10,7 @@ const SignUp = () => {
const router = useRouter(); const router = useRouter();
if (user) { if (user) {
router.replace("/"); router.replace("/");
} else if (config.get("ALLOW_REGISTRATION") == "false") { } else if (!config.get("ALLOW_REGISTRATION")) {
router.replace("/auth/signIn"); router.replace("/auth/signIn");
} else { } else {
return ( return (

View File

@@ -1,4 +1,4 @@
import { Group } from "@mantine/core"; import { Box, Group, Text, Title } from "@mantine/core";
import { useModals } from "@mantine/modals"; import { useModals } from "@mantine/modals";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -8,6 +8,7 @@ import FileList from "../../components/share/FileList";
import showEnterPasswordModal from "../../components/share/showEnterPasswordModal"; import showEnterPasswordModal from "../../components/share/showEnterPasswordModal";
import showErrorModal from "../../components/share/showErrorModal"; import showErrorModal from "../../components/share/showErrorModal";
import shareService from "../../services/share.service"; import shareService from "../../services/share.service";
import { Share as ShareType } from "../../types/share.type";
export function getServerSideProps(context: GetServerSidePropsContext) { export function getServerSideProps(context: GetServerSidePropsContext) {
return { return {
@@ -17,7 +18,7 @@ export function getServerSideProps(context: GetServerSidePropsContext) {
const Share = ({ shareId }: { shareId: string }) => { const Share = ({ shareId }: { shareId: string }) => {
const modals = useModals(); const modals = useModals();
const [fileList, setFileList] = useState<any[]>([]); const [share, setShare] = useState<ShareType>();
const getShareToken = async (password?: string) => { const getShareToken = async (password?: string) => {
await shareService await shareService
@@ -41,7 +42,7 @@ const Share = ({ shareId }: { shareId: string }) => {
shareService shareService
.get(shareId) .get(shareId)
.then((share) => { .then((share) => {
setFileList(share.files); setShare(share);
}) })
.catch((e) => { .catch((e) => {
const { error } = e.response.data; const { error } = e.response.data;
@@ -77,14 +78,16 @@ const Share = ({ shareId }: { shareId: string }) => {
title={`Share ${shareId}`} title={`Share ${shareId}`}
description="Look what I've shared with you." description="Look what I've shared with you."
/> />
<Group position="right" mb="lg">
<DownloadAllButton shareId={shareId} /> <Group position="apart" mb="lg">
<Box style={{ maxWidth: "70%" }}>
<Title order={3}>{share?.id}</Title>
<Text size="sm">{share?.description}</Text>
</Box>
{share?.files.length > 1 && <DownloadAllButton shareId={shareId} />}
</Group> </Group>
<FileList
files={fileList} <FileList files={share?.files} shareId={shareId} isLoading={!share} />
shareId={shareId}
isLoading={fileList.length == 0}
/>
</> </>
); );
}; };

View File

@@ -2,6 +2,7 @@ import { Button, Group } from "@mantine/core";
import { useModals } from "@mantine/modals"; import { useModals } from "@mantine/modals";
import axios from "axios"; import axios from "axios";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import pLimit from "p-limit";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Meta from "../components/Meta"; import Meta from "../components/Meta";
import Dropzone from "../components/upload/Dropzone"; import Dropzone from "../components/upload/Dropzone";
@@ -12,10 +13,11 @@ import useConfig from "../hooks/config.hook";
import useUser from "../hooks/user.hook"; import useUser from "../hooks/user.hook";
import shareService from "../services/share.service"; import shareService from "../services/share.service";
import { FileUpload } from "../types/File.type"; import { FileUpload } from "../types/File.type";
import { ShareSecurity } from "../types/share.type"; import { CreateShare, Share } from "../types/share.type";
import toast from "../utils/toast.util"; import toast from "../utils/toast.util";
let share: any; let createdShare: Share;
const promiseLimit = pLimit(3);
const Upload = () => { const Upload = () => {
const router = useRouter(); const router = useRouter();
@@ -26,12 +28,7 @@ const Upload = () => {
const [files, setFiles] = useState<FileUpload[]>([]); const [files, setFiles] = useState<FileUpload[]>([]);
const [isUploading, setisUploading] = useState(false); const [isUploading, setisUploading] = useState(false);
const uploadFiles = async ( const uploadFiles = async (share: CreateShare) => {
id: string,
expiration: string,
recipients: string[],
security: ShareSecurity
) => {
setisUploading(true); setisUploading(true);
try { try {
setFiles((files) => setFiles((files) =>
@@ -40,8 +37,10 @@ const Upload = () => {
return file; return file;
}) })
); );
share = await shareService.create(id, expiration, recipients, security); createdShare = await shareService.create(share);
for (let i = 0; i < files.length; i++) {
const uploadPromises = files.map((file, i) => {
// Callback to indicate current upload progress
const progressCallBack = (progress: number) => { const progressCallBack = (progress: number) => {
setFiles((files) => { setFiles((files) => {
return files.map((file, callbackIndex) => { return files.map((file, callbackIndex) => {
@@ -54,11 +53,15 @@ const Upload = () => {
}; };
try { try {
await shareService.uploadFile(share.id, files[i], progressCallBack); return promiseLimit(() =>
shareService.uploadFile(share.id, file, progressCallBack)
);
} catch { } catch {
files[i].uploadingProgress = -1; file.uploadingProgress = -1;
} }
} });
await Promise.all(uploadPromises);
} catch (e) { } catch (e) {
if (axios.isAxiosError(e)) { if (axios.isAxiosError(e)) {
toast.error(e.response?.data?.message ?? "An unkown error occured."); toast.error(e.response?.data?.message ?? "An unkown error occured.");
@@ -84,9 +87,9 @@ const Upload = () => {
toast.error(`${fileErrorCount} file(s) failed to upload. Try again.`); toast.error(`${fileErrorCount} file(s) failed to upload. Try again.`);
} else { } else {
shareService shareService
.completeShare(share.id) .completeShare(createdShare.id)
.then(() => { .then(() => {
showCompletedUploadModal(modals, share); showCompletedUploadModal(modals, createdShare);
setFiles([]); setFiles([]);
}) })
.catch(() => .catch(() =>

View File

@@ -1,19 +1,22 @@
import { import {
CreateShare,
MyShare, MyShare,
Share, Share,
ShareMetaData, ShareMetaData,
ShareSecurity,
} from "../types/share.type"; } from "../types/share.type";
import api from "./api.service"; import api from "./api.service";
const create = async ( const create = async (share: CreateShare) => {
id: string, const { id, expiration, recipients, security, description } = share;
expiration: string, return (
recipients: string[], await api.post("shares", {
security?: ShareSecurity id,
) => { expiration,
return (await api.post("shares", { id, expiration, recipients, security })) recipients,
.data; security,
description,
})
).data;
}; };
const completeShare = async (id: string) => { const completeShare = async (id: string) => {

View File

@@ -4,9 +4,18 @@ export type Share = {
id: string; id: string;
files: any; files: any;
creator: User; creator: User;
description?: string;
expiration: Date; expiration: Date;
}; };
export type CreateShare = {
id: string;
description?: string;
recipients: string[];
expiration: string;
security: ShareSecurity;
};
export type ShareMetaData = { export type ShareMetaData = {
id: string; id: string;
isZipReady: boolean; isZipReady: boolean;

View File

@@ -1,6 +1,6 @@
{ {
"name": "pingvin-share", "name": "pingvin-share",
"version": "0.3.3", "version": "0.3.6",
"scripts": { "scripts": {
"format": "cd frontend && npm run format && cd ../backend && npm run format", "format": "cd frontend && npm run format && cd ../backend && npm run format",
"lint": "cd frontend && npm run lint && cd ../backend && npm run lint", "lint": "cd frontend && npm run lint && cd ../backend && npm run lint",