├── .DS_Store ├── .babelrc ├── .dockerignore ├── .env.example ├── .github └── workflows │ ├── docker-build-dev.yml │ ├── docker-build.yml │ └── release-please.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── backend ├── config │ ├── .env.development.example │ ├── .env.production.example │ └── .env.test.example ├── controllers │ ├── file-controller.ts │ ├── folder-controller.ts │ └── user-controller.ts ├── cookies │ └── create-cookies.ts ├── db │ ├── connections │ │ ├── mongoose-server-utils.ts │ │ ├── mongoose.ts │ │ └── s3.ts │ └── mongoDB │ │ ├── fileDB.ts │ │ ├── folderDB.ts │ │ ├── thumbnailDB.ts │ │ └── userDB.ts ├── enviroment │ ├── env.ts │ └── get-env-variables.ts ├── express-routers │ ├── file-router.ts │ ├── folder-router.ts │ └── user-router.ts ├── key │ ├── get-key.ts │ └── get-web-UI-key.ts ├── middleware │ ├── auth.ts │ ├── authFullUser.ts │ ├── authLogout.ts │ ├── authRefresh.ts │ ├── authStreamVideo.ts │ ├── emailAuth.ts │ ├── files │ │ └── files-middleware.ts │ ├── folders │ │ └── folder-middleware.ts │ ├── tempAuth.ts │ ├── tempAuthVideo.ts │ ├── user │ │ └── user-middleware.ts │ └── utils │ │ └── middleware-utils.ts ├── models │ ├── file-model.ts │ ├── file-system-model.ts │ ├── folder-model.ts │ ├── thumbnail-model.ts │ └── user-model.ts ├── server │ ├── server-start.ts │ └── server.ts ├── services │ ├── chunk-service │ │ ├── actions │ │ │ ├── S3-actions.ts │ │ │ ├── file-system-actions.ts │ │ │ └── helper-actions.ts │ │ ├── chunk-service.ts │ │ ├── store-types.ts │ │ └── utils │ │ │ ├── ChunkInterface.ts │ │ │ ├── awaitStream.ts │ │ │ ├── awaitUploadStreamFS.ts │ │ │ ├── awaitUploadStreamS3.ts │ │ │ ├── cachedSubscriptionStatuses.ts │ │ │ ├── createImageThumbnail.ts │ │ │ ├── createVideoThumbnail.ts │ │ │ ├── fixEndChunkLength.ts │ │ │ ├── fixStartChunkLength.ts │ │ │ ├── getBusboyData.ts │ │ │ ├── getFileData.ts │ │ │ ├── getFileSize.ts │ │ │ ├── getFolderUploadBusboyData.ts │ │ │ ├── getPrevIVFS.ts │ │ │ ├── getPrevIVS3.ts │ │ │ ├── getPublicFileData.ts │ │ │ ├── getThumbnailData.ts │ │ │ ├── removeChunksFS.ts │ │ │ ├── removeChunksS3.ts │ │ │ ├── removeTempToken.ts │ │ │ ├── storageHelper.ts │ │ │ └── tempCreateVideoThumbnail.ts │ ├── file-service │ │ └── file-service.ts │ ├── folder-service │ │ └── folder-service.ts │ └── user-service │ │ └── user-service.ts ├── tempStorage │ └── tempStorage.ts ├── tsconfig.json ├── types │ ├── file-types.ts │ └── folder-types.ts └── utils │ ├── ConflictError.ts │ ├── ForbiddenError.ts │ ├── InternalServerError.ts │ ├── NotAuthorizedError.ts │ ├── NotEmailVerifiedError.ts │ ├── NotFoundError.ts │ ├── NotValidDataError.ts │ ├── convertDriveFolderToMongoFolder.ts │ ├── convertDriveFoldersToMongoFolders.ts │ ├── convertDriveListToMongoList.ts │ ├── convertDriveToMongo.ts │ ├── createEmailTransporter.ts │ ├── createQuery.ts │ ├── createQueryGoogle.ts │ ├── createQueryGoogleFolder.ts │ ├── getFSStoragePath.ts │ ├── getKeyFromTerminal.ts │ ├── imageChecker.ts │ ├── mobileCheck.ts │ ├── sanitizeFilename.ts │ ├── sendPasswordResetEmail.ts │ ├── sendShareEmail.ts │ ├── sendVerificationEmail.ts │ ├── sortBySwitch.ts │ ├── sortBySwitchFolder.ts │ ├── sortGoogleMongoFolderList.ts │ ├── sortGoogleMongoList.ts │ ├── sortGoogleMongoQuickFiles.ts │ ├── streamToBuffer.ts │ ├── userUpdateCheck.ts │ └── videoChecker.ts ├── docker-compose-test.yml ├── docker-compose.yml ├── eslint.config.mjs ├── github_images ├── context.png ├── download.png ├── homepage.png ├── image-viewer.png ├── media-viewer.png ├── move.png ├── multiselect.png ├── search.png ├── share.png ├── trash.png ├── upload.png ├── video-viewer.png └── youtube-video.jpeg ├── index.html ├── jest.config.js ├── key ├── getKey.js └── getNewKey.js ├── nodemon.json ├── package.json ├── postcss.config.js ├── public ├── .DS_Store └── images │ ├── .DS_Store │ └── icon.png ├── serverUtils ├── backupDatabase.js ├── changeEncryptionPassword.js ├── cleanDatabase.js ├── createIndexes.js ├── createTempDirectory.js ├── createThumbnailBuffer.js ├── createVideoThumbnails.js ├── deleteDatabase.js ├── deleteTempDatabase.js ├── getEnvVaribables.js ├── migrateMyDrive4.js ├── mongoServerUtil.js ├── removeOldPersonalData.js ├── removeOldSubscriptionData.js ├── removeTokens.js ├── restoreDatabase.js ├── restoreFromTempDirectory.js └── setupServer.js ├── src ├── api │ ├── filesAPI.ts │ ├── foldersAPI.ts │ └── userAPI.ts ├── app.tsx ├── axiosInterceptor │ └── index.ts ├── components │ ├── AddNewDropdown │ │ └── AddNewDropdown.tsx │ ├── ContextMenu │ │ └── ContextMenu.tsx │ ├── Dataform │ │ └── Dataform.tsx │ ├── DownloadPage │ │ └── DownloadPage.tsx │ ├── FileInfoPopup │ │ └── FileInfoPopup.tsx │ ├── FileItem │ │ └── FileItem.tsx │ ├── Files │ │ └── Files.tsx │ ├── FolderItem │ │ └── FolderItem.tsx │ ├── Folders │ │ └── Folders.tsx │ ├── Header │ │ └── Header.tsx │ ├── Homepage │ │ └── Homepage.tsx │ ├── LeftSection │ │ └── LeftSection.tsx │ ├── LoginPage │ │ └── LoginPage.tsx │ ├── MainSection │ │ └── MainSection.tsx │ ├── MediaItem │ │ └── MediaItem.tsx │ ├── Medias │ │ └── Medias.tsx │ ├── MoverPopup │ │ └── MoverPopup.tsx │ ├── MultiSelectBar │ │ └── MultiSelectBar.tsx │ ├── ParentBar │ │ └── ParentBar.tsx │ ├── PhotoViewerPopup │ │ └── PhotoViewerPopup.tsx │ ├── QuickAccess │ │ └── QuickAccess.tsx │ ├── QuickAccessItem │ │ └── QuickAccessItem.tsx │ ├── ResetPasswordPage │ │ └── ResetPasswordPage.tsx │ ├── RightSection │ │ └── RightSection.tsx │ ├── SearchBar │ │ └── SearchBar.tsx │ ├── SearchBarItem │ │ └── SearchBarItem.tsx │ ├── SettingsPage │ │ ├── SettingsAccountSection.tsx │ │ ├── SettingsChangePasswordPopup.tsx │ │ ├── SettingsGeneralSection.tsx │ │ └── SettingsPage.tsx │ ├── SharePopup │ │ └── SharePopup.tsx │ ├── Spinner │ │ └── Spinner.tsx │ ├── UploadItem │ │ └── UploadItem.tsx │ ├── Uploader │ │ └── Uploader.tsx │ └── VerifyEmailPage │ │ └── VerifyEmailPage.tsx ├── config │ ├── .env.development.example │ └── .env.production.example ├── enviroment │ └── envFrontEnd.js ├── hooks │ ├── actions.ts │ ├── contextMenu.ts │ ├── files.ts │ ├── folders.ts │ ├── infiniteScroll.ts │ ├── preferenceSetter.ts │ ├── store.ts │ ├── user.ts │ └── utils.ts ├── icons │ ├── AccountIcon.tsx │ ├── ActionsIcon.tsx │ ├── AlertIcon.tsx │ ├── ArrowBackIcon.tsx │ ├── CalendarIcon.tsx │ ├── CheckCircleIcon.tsx │ ├── ChevronOutline.tsx │ ├── ChevronSolid.tsx │ ├── CircleLeftIcon.tsx │ ├── CircleRightIcon.tsx │ ├── ClockIcon.tsx │ ├── CloseIcon.tsx │ ├── CreateFolderIcon.tsx │ ├── DownloadIcon.tsx │ ├── FileDetailsIcon.tsx │ ├── FolderIcon.tsx │ ├── FolderUploadIcon.tsx │ ├── HomeIconOutline.tsx │ ├── HomeListIcon.tsx │ ├── LockIcon.tsx │ ├── MenuIcon.tsx │ ├── MinimizeIcon.tsx │ ├── MoveIcon.tsx │ ├── MultiSelectIcon.tsx │ ├── OneIcon.tsx │ ├── PhotoIcon.tsx │ ├── PlayIcon.tsx │ ├── PublicIcon.tsx │ ├── RenameIcon.tsx │ ├── RestoreIcon.tsx │ ├── SearchIcon.tsx │ ├── SettingsIcon.tsx │ ├── SettingsIconSolid.tsx │ ├── ShareIcon.tsx │ ├── SpacerIcon.tsx │ ├── StorageIcon.tsx │ ├── TrashIcon.tsx │ ├── TuneIcon.tsx │ └── UploadFileIcon.tsx ├── popups │ ├── file.ts │ ├── folder.ts │ └── user.ts ├── providers │ └── AuthProvider.js ├── reducers │ ├── filter.ts │ ├── general.ts │ ├── leftSection.ts │ ├── selected.ts │ ├── uploader.ts │ └── user.ts ├── routers │ ├── AppRouter.jsx │ ├── PrivateRoute.jsx │ └── PublicRoute.jsx ├── store │ └── configureStore.ts ├── styles │ ├── base │ │ └── _base.scss │ ├── components │ │ ├── _Spinner.scss │ │ └── _Swal.scss │ └── styles.scss ├── types │ ├── file.ts │ ├── folders.ts │ └── user.ts └── utils │ ├── InternalServerError.js │ ├── NotAuthorizedError.js │ ├── NotFoundError.js │ ├── PWAUtils.ts │ ├── cancelTokenManager.ts │ ├── capitalize.ts │ ├── convertDriveListToMongoList.js │ ├── convertDriveToMongo.js │ ├── createError.js │ ├── createQuery.js │ ├── files.ts │ ├── getBackendURL.ts │ ├── imageChecker.js │ ├── mobileCheck.ts │ ├── reduceQuickItemList.js │ ├── sortBySwitch.js │ ├── sortBySwitchFolder.js │ ├── updateSettings.js │ └── videoChecker.js ├── tailwind.config.js ├── tests ├── controller │ ├── file-controller.test.js │ ├── folder.controller.test.js │ └── user-controller.test.js └── utils │ ├── db-setup.js │ ├── express-app.js │ └── fileUtils.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "presets": ["@babel/env", "@babel/react"], 4 | "plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-object-rest-spread"] 5 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .gitignore 3 | .dockerignore 4 | docker-compose* 5 | Dockerfile 6 | makefile 7 | htmlcov/ 8 | coverage.xml 9 | .coverage* 10 | 11 | .vscode/ 12 | *.dat 13 | 14 | .DS_Store 15 | node_modules 16 | /build 17 | /package 18 | .env 19 | .env.* 20 | !.env.example 21 | vite.config.js.timestamp-* 22 | vite.config.ts.timestamp-* 23 | .idea -------------------------------------------------------------------------------- /.github/workflows/docker-build-dev.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build and Push (Development) 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-and-push-dev: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | packages: write 14 | security-events: write 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Docker Setup QEMU 21 | uses: docker/setup-qemu-action@v3 22 | id: qemu 23 | with: 24 | platforms: amd64,arm64 25 | 26 | - name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@v3 28 | 29 | - name: Log into ghcr.io registry 30 | uses: docker/login-action@v3 31 | with: 32 | registry: ghcr.io 33 | username: ${{ github.repository_owner }} 34 | password: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Log in to Docker Hub 37 | uses: docker/login-action@v3 38 | with: 39 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 40 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 41 | 42 | - name: Build Docker Metadata 43 | id: docker-metadata 44 | uses: docker/metadata-action@v5 45 | with: 46 | images: | 47 | ghcr.io/subnub/mydrive 48 | docker.io/kylehoell/mydrive 49 | flavor: | 50 | latest=auto 51 | tags: | 52 | type=ref,event=branch 53 | type=sha,commit=${{ github.sha }} 54 | type=raw,value=dev,enable={{is_default_branch}} 55 | 56 | - name: Push Service Image to repo 57 | uses: docker/build-push-action@v5 58 | with: 59 | context: . 60 | file: ./Dockerfile 61 | push: true 62 | provenance: mode=max 63 | tags: ${{ steps.docker-metadata.outputs.tags }} 64 | labels: ${{ steps.docker-metadata.outputs.labels }} 65 | platforms: linux/amd64,linux/arm64 66 | cache-from: type=gha,scope=${{ github.workflow }} 67 | cache-to: type=gha,mode=max,scope=${{ github.workflow }} 68 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build and Push (Production) 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build-and-push: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | packages: write 15 | security-events: write 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Docker Setup QEMU 22 | uses: docker/setup-qemu-action@v3 23 | id: qemu 24 | with: 25 | platforms: amd64,arm64 26 | 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v3 29 | 30 | - name: Log into ghcr.io registry 31 | uses: docker/login-action@v3 32 | with: 33 | registry: ghcr.io 34 | username: ${{ github.repository_owner }} 35 | password: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Log in to Docker Hub 38 | uses: docker/login-action@v3 39 | with: 40 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 41 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 42 | 43 | - name: Build Docker Metadata 44 | id: docker-metadata 45 | uses: docker/metadata-action@v5 46 | with: 47 | images: | 48 | ghcr.io/subnub/mydrive 49 | docker.io/kylehoell/mydrive 50 | flavor: | 51 | latest=auto 52 | tags: | 53 | type=ref,event=tag 54 | type=sha,commit=${{ github.sha }} 55 | type=semver,pattern={{version}} 56 | type=raw,value=latest,enable={{is_default_branch}} 57 | 58 | - name: Push Service Image to repo 59 | uses: docker/build-push-action@v5 60 | with: 61 | context: . 62 | file: ./Dockerfile 63 | push: true 64 | provenance: mode=max 65 | tags: ${{ steps.docker-metadata.outputs.tags }} 66 | labels: ${{ steps.docker-metadata.outputs.labels }} 67 | platforms: linux/amd64,linux/arm64 68 | cache-from: type=gha,scope=${{ github.workflow }} 69 | cache-to: type=gha,mode=max,scope=${{ github.workflow }} 70 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: 'Release Please' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | 13 | jobs: 14 | release-please: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: googleapis/release-please-action@v4 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | release-type: node -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/dist/ 3 | config/dev.env 4 | config/prod.env 5 | config/test.env 6 | config/.env.development 7 | config/.env.test 8 | config/.env.production 9 | config/.env.test 10 | .env 11 | .env.development 12 | .env.test 13 | .env.production 14 | .well-known/ 15 | .eslintrc.js 16 | .eslintrc.json 17 | changeEncrytionPassword/ 18 | certificate.ca-bundle 19 | certificate.crt 20 | certificate.key 21 | package-lock.json 22 | ._* 23 | .DS_Store 24 | dist/ 25 | rds-combined-ca-bundle.pem 26 | docker-variables.env 27 | dist-frontend/ 28 | dist-backend/ 29 | stats.html -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 4.0.2 (2025-03-01) 4 | 5 | 6 | ### Features 7 | 8 | * add docker ([#77](https://github.com/subnub/myDrive/issues/77)) ([22939cf](https://github.com/subnub/myDrive/commit/22939cf21dc2df8281c588206098f4aaf5472b19)) 9 | 10 | 11 | ### Miscellaneous Chores 12 | 13 | * release 4.0.2 ([c145b75](https://github.com/subnub/myDrive/commit/c145b7526b185b57214a946858fcff41ccd67d9e)) 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS builder 2 | 3 | # Install build dependencies 4 | RUN apk add --no-cache python3 make g++ ffmpeg && \ 5 | ln -sf python3 /usr/bin/python 6 | 7 | WORKDIR /usr/app-production 8 | COPY package*.json ./ 9 | 10 | RUN npm install 11 | 12 | COPY . . 13 | RUN npm run build 14 | 15 | # Remove dev dependencies 16 | RUN npm prune --production 17 | 18 | FROM node:20-alpine 19 | 20 | ENV FS_DIRECTORY=/data/ 21 | ENV TEMP_DIRECTORY=/temp/ 22 | 23 | # Install runtime dependencies 24 | RUN apk add --no-cache ffmpeg 25 | 26 | WORKDIR /usr/app-production 27 | COPY --from=builder /usr/app-production . 28 | 29 | EXPOSE 8080 30 | EXPOSE 3000 31 | 32 | CMD ["npm", "run", "start"] 33 | -------------------------------------------------------------------------------- /backend/cookies/create-cookies.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "express"; 2 | import env from "../enviroment/env"; 3 | 4 | const maxAgeAccess = 60 * 1000 * 20; 5 | //const maxAgeAccess = 1000; 6 | const maxAgeRefresh = 60 * 1000 * 60 * 24 * 30; 7 | //const maxAgeRefresh = 1000; 8 | const maxAgeStreamVideo = 60 * 1000 * 60 * 24; 9 | 10 | const secureCookies = env.secureCookies 11 | ? env.secureCookies === "true" 12 | ? true 13 | : false 14 | : false; 15 | 16 | export const createLoginCookie = ( 17 | res: Response, 18 | accessToken: string, 19 | refreshToken: string 20 | ) => { 21 | res.cookie("access-token", accessToken, { 22 | httpOnly: true, 23 | maxAge: maxAgeAccess, 24 | sameSite: "strict", 25 | secure: secureCookies, 26 | }); 27 | 28 | res.cookie("refresh-token", refreshToken, { 29 | httpOnly: true, 30 | maxAge: maxAgeRefresh, 31 | sameSite: "strict", 32 | secure: secureCookies, 33 | }); 34 | }; 35 | 36 | export const createLogoutCookie = (res: Response) => { 37 | res.cookie( 38 | "access-token", 39 | {}, 40 | { 41 | httpOnly: true, 42 | maxAge: 0, 43 | sameSite: "strict", 44 | secure: secureCookies, 45 | } 46 | ); 47 | 48 | res.cookie( 49 | "refresh-token", 50 | {}, 51 | { 52 | httpOnly: true, 53 | maxAge: 0, 54 | sameSite: "strict", 55 | secure: secureCookies, 56 | } 57 | ); 58 | }; 59 | 60 | export const createStreamVideoCookie = ( 61 | res: Response, 62 | streamVideoAccessToken: string 63 | ) => { 64 | res.cookie("video-access-token", streamVideoAccessToken, { 65 | httpOnly: true, 66 | maxAge: maxAgeStreamVideo, 67 | sameSite: "strict", 68 | secure: secureCookies, 69 | }); 70 | }; 71 | 72 | export const removeStreamVideoCookie = (res: Response) => { 73 | res.cookie( 74 | "video-access-token", 75 | {}, 76 | { 77 | httpOnly: true, 78 | maxAge: 0, 79 | sameSite: "strict", 80 | secure: secureCookies, 81 | } 82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /backend/db/connections/mongoose-server-utils.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import env from "../../enviroment/env"; 3 | 4 | mongoose.connect(env.mongoURL!, { 5 | socketTimeoutMS: 30000000, 6 | }); 7 | 8 | export default mongoose; 9 | -------------------------------------------------------------------------------- /backend/db/connections/mongoose.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import env from "../../enviroment/env"; 3 | import fs from "fs"; 4 | 5 | const DBUrl = env.mongoURL as string; 6 | 7 | if (env.useDocumentDB === "true") { 8 | console.log("Using DocumentDB"); 9 | 10 | if (env.documentDBBundle === "true") { 11 | const fileBuffer = fs.readFileSync("./rds-combined-ca-bundle.pem"); 12 | const mongooseCertificateConnect = mongoose as any; 13 | 14 | mongooseCertificateConnect.connect(DBUrl, { 15 | useCreateIndex: true, 16 | useUnifiedTopology: true, 17 | sslValidate: true, 18 | sslCA: fileBuffer, 19 | }); 20 | } else { 21 | mongoose.connect(DBUrl, {}); 22 | } 23 | } else { 24 | mongoose.connect(DBUrl, {}); 25 | } 26 | 27 | export default mongoose; 28 | -------------------------------------------------------------------------------- /backend/db/connections/s3.ts: -------------------------------------------------------------------------------- 1 | import AWS from "aws-sdk"; 2 | import env from "../../enviroment/env"; 3 | 4 | AWS.config.update({ 5 | accessKeyId: env.s3ID, 6 | secretAccessKey: env.s3Key, 7 | }); 8 | 9 | const s3 = new AWS.S3(); 10 | 11 | export default s3; 12 | module.exports = s3; 13 | -------------------------------------------------------------------------------- /backend/db/mongoDB/thumbnailDB.ts: -------------------------------------------------------------------------------- 1 | import Thumbnail from "../../models/thumbnail-model"; 2 | import { ObjectId } from "mongodb"; 3 | 4 | class ThumbnailDB { 5 | constructor() {} 6 | 7 | // READ 8 | 9 | getThumbnailInfo = async (userID: string, thumbnailID: string) => { 10 | const thumbnail = await Thumbnail.findOne({ 11 | _id: new ObjectId(thumbnailID), 12 | owner: userID, 13 | }); 14 | return thumbnail; 15 | }; 16 | 17 | // DELETE 18 | 19 | removeThumbnail = async (userID: string, thumbnailID: ObjectId) => { 20 | const result = await Thumbnail.deleteOne({ 21 | _id: thumbnailID, 22 | owner: userID, 23 | }); 24 | return result; 25 | }; 26 | } 27 | 28 | export default ThumbnailDB; 29 | -------------------------------------------------------------------------------- /backend/db/mongoDB/userDB.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from "mongodb"; 2 | import User from "../../models/user-model"; 3 | 4 | // READ 5 | 6 | class UserDB { 7 | constructor() {} 8 | 9 | getUserInfo = async (userID: string) => { 10 | const user = await User.findOne({ _id: new ObjectId(userID) }); 11 | return user; 12 | }; 13 | } 14 | 15 | export default UserDB; 16 | -------------------------------------------------------------------------------- /backend/enviroment/get-env-variables.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | const getEnvVariables = () => { 4 | const configPath = path.join(__dirname, "..", "..", "backend", "config"); 5 | 6 | const processType = process.env.NODE_ENV; 7 | 8 | if (processType === "production" || processType === undefined) { 9 | require("dotenv").config({ path: configPath + "/.env.production" }); 10 | } else if (processType === "development") { 11 | require("dotenv").config({ path: configPath + "/.env.development" }); 12 | } else if (processType === "test") { 13 | require("dotenv").config({ path: configPath + "/.env.test" }); 14 | } 15 | }; 16 | 17 | export default getEnvVariables; 18 | module.exports = getEnvVariables; 19 | -------------------------------------------------------------------------------- /backend/express-routers/folder-router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import auth from "../middleware/auth"; 3 | import FolderController from "../controllers/folder-controller"; 4 | import { 5 | createFolderValidationRules, 6 | deleteFolderValidationRules, 7 | downloadZipValidationRules, 8 | getFolderInfoValidationRules, 9 | getFolderListValidationRules, 10 | moveFolderListValidationRules, 11 | moveFolderValidationRules, 12 | renameFolderValidationRules, 13 | restoreFolderValidationRules, 14 | trashFolderValidationRules, 15 | } from "../middleware/folders/folder-middleware"; 16 | import authFullUser from "../middleware/authFullUser"; 17 | 18 | const folderController = new FolderController(); 19 | const router = Router(); 20 | 21 | // GET 22 | 23 | router.get( 24 | "/folder-service/info/:id", 25 | auth, 26 | getFolderInfoValidationRules, 27 | folderController.getInfo 28 | ); 29 | 30 | router.get( 31 | "/folder-service/list", 32 | auth, 33 | getFolderListValidationRules, 34 | folderController.getFolderList 35 | ); 36 | 37 | router.get( 38 | "/folder-service/move-folder-list", 39 | auth, 40 | moveFolderListValidationRules, 41 | folderController.getMoveFolderList 42 | ); 43 | 44 | router.get( 45 | "/folder-service/download-zip", 46 | auth, 47 | downloadZipValidationRules, 48 | folderController.downloadZip 49 | ); 50 | 51 | // PATCH 52 | 53 | router.patch( 54 | "/folder-service/rename", 55 | auth, 56 | renameFolderValidationRules, 57 | folderController.renameFolder 58 | ); 59 | 60 | router.patch( 61 | "/folder-service/move", 62 | auth, 63 | moveFolderValidationRules, 64 | folderController.moveFolder 65 | ); 66 | 67 | router.patch( 68 | "/folder-service/trash", 69 | auth, 70 | trashFolderValidationRules, 71 | folderController.trashFolder 72 | ); 73 | 74 | router.patch( 75 | "/folder-service/restore", 76 | auth, 77 | restoreFolderValidationRules, 78 | folderController.restoreFolder 79 | ); 80 | 81 | // DELETE 82 | 83 | router.delete( 84 | "/folder-service/remove", 85 | auth, 86 | deleteFolderValidationRules, 87 | folderController.deleteFolder 88 | ); 89 | 90 | router.delete("/folder-service/remove-all", auth, folderController.deleteAll); 91 | 92 | // POST 93 | 94 | router.post( 95 | "/folder-service/create", 96 | auth, 97 | createFolderValidationRules, 98 | folderController.createFolder 99 | ); 100 | 101 | router.post( 102 | "/folder-service/upload", 103 | authFullUser, 104 | folderController.uploadFolder 105 | ); 106 | 107 | export default router; 108 | -------------------------------------------------------------------------------- /backend/express-routers/user-router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import auth from "../middleware/auth"; 3 | import UserController from "../controllers/user-controller"; 4 | import authRefresh from "../middleware/authRefresh"; 5 | import authLogout from "../middleware/authLogout"; 6 | import { 7 | changePasswordValidationRules, 8 | createAccountValidationRules, 9 | loginAccountValidationRules, 10 | } from "../middleware/user/user-middleware"; 11 | 12 | const userController = new UserController(); 13 | 14 | const router = Router(); 15 | 16 | // GET 17 | 18 | router.get("/user-service/user", auth, userController.getUser); 19 | 20 | router.get("/user-service/user-detailed", auth, userController.getUserDetailed); 21 | 22 | // POST 23 | 24 | router.post( 25 | "/user-service/login", 26 | loginAccountValidationRules, 27 | userController.login 28 | ); 29 | 30 | router.post("/user-service/logout", authLogout, userController.logout); 31 | 32 | router.post("/user-service/logout-all", authLogout, userController.logoutAll); 33 | 34 | router.post( 35 | "/user-service/create", 36 | createAccountValidationRules, 37 | userController.createUser 38 | ); 39 | 40 | router.post("/user-service/get-token", authRefresh, userController.getToken); 41 | 42 | // PATCH 43 | 44 | router.patch( 45 | "/user-service/change-password", 46 | auth, 47 | changePasswordValidationRules, 48 | userController.changePassword 49 | ); 50 | 51 | router.patch( 52 | "/user-service/resend-verify-email", 53 | auth, 54 | userController.resendVerifyEmail 55 | ); 56 | 57 | router.patch("/user-service/verify-email", userController.verifyEmail); 58 | 59 | router.patch("/user-service/reset-password", userController.resetPassword); 60 | 61 | router.patch( 62 | "/user-service/send-password-reset", 63 | userController.sendPasswordReset 64 | ); 65 | 66 | export default router; 67 | -------------------------------------------------------------------------------- /backend/key/get-key.ts: -------------------------------------------------------------------------------- 1 | import env from "../enviroment/env"; 2 | import crypto from "crypto"; 3 | import getKeyFromTerminal from "../utils/getKeyFromTerminal"; 4 | 5 | const getKey = async () => { 6 | if ( 7 | process.env.KEY || 8 | process.env.NODE_ENV === "development" || 9 | process.env.NODE_ENV === "test" 10 | ) { 11 | const password = process.env.KEY; 12 | if (!password) { 13 | console.log(`Key is required for ${process.env.NODE_ENV} server`); 14 | throw new Error(`Key is required for ${process.env.NODE_ENV} server`); 15 | } 16 | 17 | env.key = crypto.createHash("md5").update(password).digest("hex"); 18 | } else if (process.env.NODE_ENV === "production" && !process.env.KEY) { 19 | const terminalPassword = await getKeyFromTerminal(); 20 | 21 | if (!terminalPassword || !terminalPassword.length) { 22 | console.log( 23 | "Terminal key is required for production server, or create a .env file with KEY" 24 | ); 25 | throw new Error( 26 | "Terminal key is required for production server, or create a .env file with KEY" 27 | ); 28 | } 29 | 30 | const password = crypto 31 | .createHash("md5") 32 | .update(terminalPassword) 33 | .digest("hex"); 34 | 35 | env.key = password; 36 | } 37 | }; 38 | 39 | export default getKey; 40 | -------------------------------------------------------------------------------- /backend/key/get-web-UI-key.ts: -------------------------------------------------------------------------------- 1 | import express, {Request, Response} from "express"; 2 | import http from "http"; 3 | import path from "path"; 4 | import bodyParser from "body-parser"; 5 | const app = express(); 6 | 7 | const getWebUIKey = () => { 8 | 9 | const publicPath = path.join(__dirname, "..", "..", "webUI"); 10 | 11 | 12 | return new Promise((resolve, reject) => { 13 | 14 | app.use(express.static(publicPath)); 15 | app.use(express.json()); 16 | app.use(bodyParser.json({limit: "50mb"})); 17 | app.use(bodyParser.urlencoded({limit: "50mb", extended: true, parameterLimit:50000})) 18 | 19 | app.post("/submit", (req: Request, res: Response) => { 20 | 21 | const password = req.body.password; 22 | 23 | if (password && password.length > 0) { 24 | 25 | console.log("Got WebUI key"); 26 | res.send(); 27 | server.close(); 28 | resolve(password); 29 | } 30 | }) 31 | 32 | app.get("*", (req: Request, res: Response) => { 33 | 34 | res.sendFile(path.join(publicPath, "index.html")); 35 | 36 | }) 37 | 38 | const port = process.env.HTTP_PORT || process.env.PORT || "3000"; 39 | const url = process.env.DOCKER ? undefined : "localhost"; 40 | 41 | const server = http.createServer(app) as any; 42 | 43 | server.listen(port, () => { 44 | 45 | console.log(`\nPlease navigate to http://localhost:${port} to enter encryption key\n`) 46 | 47 | }); 48 | 49 | }) 50 | } 51 | 52 | export default getWebUIKey; -------------------------------------------------------------------------------- /backend/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import env from "../enviroment/env"; 3 | import { Request, Response, NextFunction } from "express"; 4 | 5 | interface RequestType extends Request { 6 | user?: userAccessType; 7 | token?: string; 8 | encryptedToken?: string; 9 | } 10 | 11 | type jwtType = { 12 | iv: Buffer; 13 | user: userAccessType; 14 | }; 15 | 16 | type userAccessType = { 17 | _id: string; 18 | emailVerified: boolean; 19 | email: string; 20 | admin: boolean; 21 | botChecked: boolean; 22 | username: string; 23 | }; 24 | 25 | const auth = async (req: RequestType, res: Response, next: NextFunction) => { 26 | try { 27 | const accessToken = req.cookies["access-token"]; 28 | 29 | if (!accessToken) throw new Error("No Access Token"); 30 | 31 | const decoded = jwt.verify(accessToken, env.passwordAccess!) as jwtType; 32 | 33 | const user = decoded.user; 34 | 35 | if (!user) throw new Error("No User"); 36 | 37 | req.user = user; 38 | 39 | next(); 40 | } catch (e: unknown) { 41 | if ( 42 | e instanceof Error && 43 | e.message !== "No Access Token" && 44 | e.message !== "No User" 45 | ) { 46 | console.log("\nAuthorization Middleware Error:", e.message); 47 | } 48 | 49 | res.status(401).send("Error Authenticating"); 50 | } 51 | }; 52 | 53 | export default auth; 54 | -------------------------------------------------------------------------------- /backend/middleware/authFullUser.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import User, { UserInterface } from "../models/user-model"; 3 | import env from "../enviroment/env"; 4 | import { Request, Response, NextFunction } from "express"; 5 | 6 | interface RequestType extends Request { 7 | user?: UserInterface; 8 | token?: string; 9 | encryptedToken?: string; 10 | } 11 | 12 | type jwtType = { 13 | iv: Buffer; 14 | user: userAccessType; 15 | }; 16 | 17 | type userAccessType = { 18 | _id: string; 19 | emailVerified: boolean; 20 | email: string; 21 | admin: boolean; 22 | botChecked: boolean; 23 | username: string; 24 | }; 25 | 26 | const authFullUser = async ( 27 | req: RequestType, 28 | res: Response, 29 | next: NextFunction 30 | ) => { 31 | try { 32 | const accessToken = req.cookies["access-token"]; 33 | 34 | if (!accessToken) throw new Error("No Access Token"); 35 | 36 | const decoded = jwt.verify(accessToken, env.passwordAccess!) as jwtType; 37 | 38 | const user = decoded.user; 39 | 40 | if (!user) throw new Error("No User"); 41 | 42 | const fullUser = await User.findById(user._id); 43 | 44 | if (!fullUser) throw new Error("No User"); 45 | 46 | req.user = fullUser; 47 | 48 | next(); 49 | } catch (e) { 50 | if ( 51 | e instanceof Error && 52 | e.message !== "No Access Token" && 53 | e.message !== "No User" && 54 | e.message !== "Email Not Verified" 55 | ) 56 | console.log("\nAuthorization Full User Middleware Error:", e.message); 57 | 58 | res.status(401).send("Error Authenticating"); 59 | } 60 | }; 61 | 62 | export default authFullUser; 63 | -------------------------------------------------------------------------------- /backend/middleware/authLogout.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import env from "../enviroment/env"; 3 | import { Request, Response, NextFunction } from "express"; 4 | import { createLogoutCookie } from "../cookies/create-cookies"; 5 | 6 | interface RequestType extends Request { 7 | user?: userAccessType; 8 | token?: string; 9 | encryptedToken?: string; 10 | } 11 | 12 | type jwtType = { 13 | iv: Buffer; 14 | user: userAccessType; 15 | }; 16 | 17 | type userAccessType = { 18 | _id: string; 19 | emailVerified: boolean; 20 | email: string; 21 | botChecked: boolean; 22 | }; 23 | 24 | const auth = async (req: RequestType, res: Response, next: NextFunction) => { 25 | try { 26 | const accessToken = req.cookies["access-token"]; 27 | 28 | if (!accessToken) throw new Error("No Access Token"); 29 | 30 | const decoded = jwt.verify(accessToken, env.passwordAccess!) as jwtType; 31 | 32 | const user = decoded.user; 33 | 34 | if (!user) throw new Error("No User"); 35 | 36 | req.user = user; 37 | 38 | next(); 39 | } catch (e) { 40 | if ( 41 | e instanceof Error && 42 | e.message !== "No Access Token" && 43 | e.message !== "No User" 44 | ) { 45 | console.log("\nAuthorization Logout Middleware Error:", e.message); 46 | } 47 | 48 | createLogoutCookie(res); 49 | return res.status(401).send("Error Authenticating"); 50 | } 51 | }; 52 | 53 | export default auth; 54 | -------------------------------------------------------------------------------- /backend/middleware/emailAuth.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/backend/middleware/emailAuth.ts -------------------------------------------------------------------------------- /backend/middleware/tempAuth.ts: -------------------------------------------------------------------------------- 1 | // import jwt from "jsonwebtoken"; 2 | // import User, {UserInterface} from "../models/user"; 3 | // import env from "../enviroment/env"; 4 | // import {Request, Response, NextFunction} from "express"; 5 | 6 | 7 | // interface RequestType extends Request { 8 | // user?: UserInterface, 9 | // auth?: boolean, 10 | // encryptedTempToken?: string, 11 | // } 12 | 13 | // type jwtType = { 14 | // iv: Buffer, 15 | // _id: string, 16 | // } 17 | 18 | // const tempAuth = async(req: RequestType, res: Response, next: NextFunction) => { 19 | 20 | // try { 21 | 22 | // const token = req.params.tempToken; 23 | 24 | // const decoded = jwt.verify(token, env.passwordAccess!) as jwtType; 25 | 26 | // const iv = decoded.iv; 27 | 28 | // const user = await User.findOne({_id: decoded._id}) as UserInterface; 29 | // const encrpytionKey = user.getEncryptionKey(); 30 | 31 | // const encryptedToken = user.encryptToken(token, encrpytionKey, iv); 32 | 33 | // let tokenFound = false; 34 | // for (let i = 0; i < user.tempTokens.length; i++) { 35 | 36 | // const currentToken = user.tempTokens[i].token; 37 | 38 | // if (currentToken === encryptedToken) { 39 | // tokenFound = true; 40 | // break; 41 | // } 42 | // } 43 | 44 | // if (!user || !tokenFound) { 45 | 46 | // throw new Error("User Not Found") 47 | 48 | // } else { 49 | 50 | // user.tempTokens = user.tempTokens.filter((filterToken) => { 51 | 52 | // return filterToken.token !== encryptedToken 53 | // }) 54 | 55 | // await user.save(); 56 | 57 | // req.user = user; 58 | // req.auth = true; 59 | // req.encryptedTempToken = encryptedToken; 60 | 61 | // next(); 62 | // } 63 | 64 | // } catch (e) { 65 | // console.log(e); 66 | // res.status(401).send(); 67 | // } 68 | // } 69 | 70 | // export default tempAuth; -------------------------------------------------------------------------------- /backend/middleware/tempAuthVideo.ts: -------------------------------------------------------------------------------- 1 | // import jwt from "jsonwebtoken"; 2 | // import User, {UserInterface} from "../models/user"; 3 | // import env from "../enviroment/env"; 4 | // import {Request, Response, NextFunction} from "express"; 5 | 6 | 7 | // interface RequestType extends Request { 8 | // user?: UserInterface, 9 | // auth?: boolean, 10 | // encryptedTempToken?: string, 11 | // } 12 | 13 | // type jwtType = { 14 | // iv: Buffer, 15 | // _id: string, 16 | // cookie: string 17 | // } 18 | 19 | // const tempAuthVideo = async(req: RequestType, res: Response, next: NextFunction) => { 20 | 21 | // try { 22 | 23 | // const token = req.params.tempToken; 24 | 25 | // const decoded = jwt.verify(token, env.passwordAccess!) as jwtType; 26 | 27 | // const iv = decoded.iv; 28 | 29 | // if (req.params.uuid !== decoded.cookie) { 30 | 31 | // throw new Error("Cookie mismatch") 32 | // } 33 | 34 | // const user = await User.findOne({_id: decoded._id}) as UserInterface; 35 | // const encrpytionKey = user.getEncryptionKey(); 36 | 37 | // const encryptedToken = user.encryptToken(token, encrpytionKey, iv); 38 | 39 | // let tokenFound = false; 40 | // for (let i = 0; i < user.tempTokens.length; i++) { 41 | 42 | // const currentToken = user.tempTokens[i].token; 43 | 44 | // if (currentToken === encryptedToken) { 45 | // tokenFound = true; 46 | // break; 47 | // } 48 | // } 49 | 50 | // if (!user || !tokenFound) { 51 | 52 | // throw new Error("User not found"); 53 | 54 | // } else { 55 | 56 | // await user.save(); 57 | 58 | // req.user = user; 59 | // req.auth = true; 60 | // req.encryptedTempToken = encryptedToken; 61 | // next(); 62 | // } 63 | 64 | // } catch (e) { 65 | // console.log(e); 66 | // res.status(401).send(); 67 | // } 68 | // } 69 | 70 | // export default tempAuthVideo; -------------------------------------------------------------------------------- /backend/middleware/user/user-middleware.ts: -------------------------------------------------------------------------------- 1 | import { body, param, query, validationResult } from "express-validator"; 2 | import { Request, Response, NextFunction } from "express"; 3 | import { middlewareValidationFunction } from "../utils/middleware-utils"; 4 | 5 | // PATCH 6 | 7 | export const changePasswordValidationRules = [ 8 | body("oldPassword") 9 | .exists() 10 | .withMessage("Old password is required") 11 | .isString() 12 | .withMessage("Old password must be a string") 13 | .isLength({ min: 6, max: 256 }) 14 | .withMessage( 15 | "Old password must be at least 6 characters and at most 256 characters" 16 | ), 17 | body("newPassword") 18 | .exists() 19 | .withMessage("New password is required") 20 | .isString() 21 | .withMessage("New password must be a string") 22 | .isLength({ min: 6, max: 256 }) 23 | .withMessage( 24 | "New password must be at least 6 characters and at most 256 characters" 25 | ), 26 | middlewareValidationFunction, 27 | ]; 28 | 29 | // POST 30 | 31 | export const createAccountValidationRules = [ 32 | body("email") 33 | .exists() 34 | .withMessage("Email is required") 35 | .isString() 36 | .withMessage("Email must be a string") 37 | .isLength({ min: 3, max: 320 }) 38 | .withMessage( 39 | "Email must be at least 3 characters and at most 320 characters" 40 | ) 41 | .isEmail() 42 | .withMessage("Email is invalid"), 43 | body("password") 44 | .exists() 45 | .withMessage("Password is required") 46 | .isString() 47 | .withMessage("Password must be a string") 48 | .isLength({ min: 6, max: 256 }) 49 | .withMessage( 50 | "Password must be at least 6 characters and at most 256 characters" 51 | ), 52 | middlewareValidationFunction, 53 | ]; 54 | 55 | export const loginAccountValidationRules = [ 56 | body("email") 57 | .exists() 58 | .withMessage("Email is required") 59 | .isString() 60 | .withMessage("Email must be a string") 61 | .isLength({ min: 3, max: 320 }) 62 | .withMessage( 63 | "Email must be at least 3 characters and at most 320 characters" 64 | ) 65 | .isEmail() 66 | .withMessage("Email is invalid"), 67 | body("password") 68 | .exists() 69 | .withMessage("Password is required") 70 | .isString() 71 | .withMessage("Password must be a string") 72 | .isLength({ min: 6, max: 256 }) 73 | .withMessage( 74 | "Password must be at least 6 characters and at most 256 characters" 75 | ), 76 | middlewareValidationFunction, 77 | ]; 78 | -------------------------------------------------------------------------------- /backend/middleware/utils/middleware-utils.ts: -------------------------------------------------------------------------------- 1 | import e, { NextFunction } from "express"; 2 | import { validationResult } from "express-validator"; 3 | import { Request, Response } from "express"; 4 | import NotAuthorizedError from "../../utils/NotAuthorizedError"; 5 | import NotFoundError from "../../utils/NotFoundError"; 6 | import InternalServerError from "../../utils/InternalServerError"; 7 | import ForbiddenError from "../../utils/ForbiddenError"; 8 | import NotValidDataError from "../../utils/NotValidDataError"; 9 | import ConflictError from "../../utils/ConflictError"; 10 | 11 | export const middlewareValidationFunction = ( 12 | req: Request, 13 | res: Response, 14 | next: NextFunction 15 | ) => { 16 | const errors = validationResult(req); 17 | if (!errors.isEmpty()) { 18 | return res.status(400).json({ errors: errors.array() }); 19 | } 20 | next(); 21 | }; 22 | 23 | export const middlewareErrorHandler = ( 24 | error: Error, 25 | _req: Request, 26 | res: Response, 27 | _next: NextFunction 28 | ) => { 29 | console.log("Express route error: ", error); 30 | 31 | if ( 32 | error instanceof NotAuthorizedError || 33 | error instanceof ForbiddenError || 34 | error instanceof NotFoundError || 35 | error instanceof InternalServerError || 36 | error instanceof NotValidDataError || 37 | error instanceof ConflictError 38 | ) { 39 | return res.status(error.code).send(error.message); 40 | } 41 | 42 | res.status(500).send("Server error"); 43 | }; 44 | -------------------------------------------------------------------------------- /backend/models/file-system-model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, {Document} from "mongoose"; 2 | 3 | const fileSystemSchema = new mongoose.Schema({ 4 | 5 | name: { 6 | type: String, 7 | required: true, 8 | }, 9 | owner: { 10 | type: String, 11 | required: true 12 | }, 13 | path: { 14 | type: String, 15 | required: true 16 | }, 17 | parent: { 18 | type: String, 19 | required: true, 20 | }, 21 | parentList: { 22 | type: Array, 23 | required: true 24 | }, 25 | hasThumbnail: { 26 | type: Boolean, 27 | required: true 28 | }, 29 | thumbnailID: { 30 | type: String 31 | }, 32 | originalSize: { 33 | type: Number, 34 | required: true 35 | }, 36 | size: { 37 | type: Number, 38 | required: true 39 | }, 40 | isVideo: { 41 | type: Boolean, 42 | required: true 43 | }, 44 | IV: { 45 | type: Buffer, 46 | required: true 47 | } 48 | 49 | }, { 50 | timestamps: true 51 | }) 52 | 53 | export interface FileSystemInterface extends Document { 54 | name: string, 55 | owner: string, 56 | path: string, 57 | parent: string, 58 | parentList: string[], 59 | hasThumbnail: boolean, 60 | thumbnailID?: string, 61 | originalSize: number, 62 | size: number, 63 | isVideo: boolean, 64 | IV: Buffer 65 | } 66 | 67 | const FileSystem = mongoose.model("FileSystem", fileSystemSchema); 68 | 69 | export default FileSystem; -------------------------------------------------------------------------------- /backend/models/folder-model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from "mongoose"; 2 | 3 | const folderSchema = new mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | required: true, 8 | validate(value: any) { 9 | if (!value || value.length === 0 || value.length >= 256) { 10 | throw new Error( 11 | "Name is required and length must be greater than 0 and 256 characters max" 12 | ); 13 | } 14 | }, 15 | }, 16 | parent: { 17 | type: String, 18 | required: true, 19 | }, 20 | owner: { 21 | type: String, 22 | required: true, 23 | }, 24 | parentList: { 25 | type: Array, 26 | required: true, 27 | }, 28 | personalFolder: Boolean, 29 | trashed: Boolean, 30 | }, 31 | { 32 | timestamps: true, 33 | } 34 | ); 35 | 36 | export interface FolderInterface 37 | extends mongoose.Document { 38 | name: string; 39 | parent: string; 40 | owner: string; 41 | createdAt: Date; 42 | updatedAt: Date; 43 | parentList: string[]; 44 | _doc?: any; 45 | personalFolder?: boolean; 46 | trashed: boolean | null; 47 | } 48 | 49 | folderSchema.index({ createdAt: 1 }, { background: true }); 50 | folderSchema.index({ owner: 1 }, { background: true }); 51 | folderSchema.index({ trashed: 1 }, { background: true }); 52 | folderSchema.index({ parent: 1 }, { background: true }); 53 | folderSchema.index({ name: 1 }, { background: true }); 54 | 55 | const Folder = mongoose.model("Folder", folderSchema); 56 | 57 | export default Folder; 58 | -------------------------------------------------------------------------------- /backend/models/thumbnail-model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from "mongoose"; 2 | 3 | const thumbnailSchema = new mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | required: true, 8 | validate(value: any) { 9 | if (!value || value.length === 0 || value.length >= 256) { 10 | throw new Error( 11 | "Name is required and length must be greater than 0 and 256 characters max" 12 | ); 13 | } 14 | }, 15 | }, 16 | owner: { 17 | type: String, 18 | required: true, 19 | }, 20 | 21 | data: { 22 | type: Buffer, 23 | }, 24 | path: { 25 | type: String, 26 | }, 27 | 28 | IV: { 29 | type: Buffer, 30 | }, 31 | s3ID: String, 32 | personalFile: String, 33 | }, 34 | { 35 | timestamps: true, 36 | } 37 | ); 38 | 39 | export interface ThumbnailInterface extends Document { 40 | _id: any; 41 | name: string; 42 | owner: string; 43 | data?: any; 44 | path?: string; 45 | IV: Buffer; 46 | s3ID?: string; 47 | personalFile?: boolean; 48 | createdAt: Date; 49 | updatedAt: Date; 50 | } 51 | 52 | thumbnailSchema.index({ owner: 1 }); 53 | 54 | const Thumbnail = mongoose.model( 55 | "Thumbnail", 56 | thumbnailSchema 57 | ); 58 | 59 | export default Thumbnail; 60 | module.exports = Thumbnail; 61 | -------------------------------------------------------------------------------- /backend/server/server-start.ts: -------------------------------------------------------------------------------- 1 | import getEnvVariables from "../enviroment/get-env-variables"; 2 | getEnvVariables(); 3 | import getKey from "../key/get-key"; 4 | import servers from "./server"; 5 | 6 | const { server, serverHttps } = servers; 7 | 8 | const serverStart = async () => { 9 | await getKey(); 10 | 11 | console.log("ENV", process.env.NODE_ENV); 12 | 13 | const httpPort = process.env.HTTP_PORT || process.env.PORT || 3000; 14 | const httpsPort = process.env.HTTPS_PORT || 8080 15 | 16 | if (process.env.NODE_ENV === "production" && process.env.SSL === "true") { 17 | server.listen(httpPort, process.env.URL, () => { 18 | console.log("Http Server Running On Port:", httpPort); 19 | }); 20 | 21 | serverHttps.listen(httpsPort, function () { 22 | console.log("Https Server Running On Port:", httpsPort); 23 | }); 24 | } else if (process.env.NODE_ENV === "production") { 25 | server.listen(httpPort, process.env.URL, () => { 26 | console.log("Http Server (No-SSL) Running On Port:", httpPort); 27 | }); 28 | } else { 29 | server.listen(httpPort, process.env.URL, () => { 30 | console.log("\nDevelopment Backend Server Running On :", httpPort); 31 | }); 32 | } 33 | }; 34 | 35 | serverStart(); 36 | -------------------------------------------------------------------------------- /backend/server/server.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import path from "path"; 3 | import userRouter from "../express-routers/user-router"; 4 | import fileRouter from "../express-routers/file-router"; 5 | import folderRouter from "../express-routers/folder-router"; 6 | import bodyParser from "body-parser"; 7 | import https from "https"; 8 | import fs from "fs"; 9 | import helmet from "helmet"; 10 | import busboy from "connect-busboy"; 11 | import compression from "compression"; 12 | import http from "http"; 13 | import cookieParser from "cookie-parser"; 14 | import env from "../enviroment/env"; 15 | import { middlewareErrorHandler } from "../middleware/utils/middleware-utils"; 16 | import cors from "cors"; 17 | // import requestIp from "request-ip"; 18 | 19 | const app = express(); 20 | const publicPath = path.join(__dirname, "..", "..", "dist-frontend"); 21 | 22 | let server: any; 23 | let serverHttps: any; 24 | 25 | if (process.env.SSL === "true") { 26 | const certPath = env.httpsCrtPath || "certificate.crt" 27 | const caPath = env.httpsCaPath || "certificate.ca-bundle" 28 | const keyPath = env.httpsKeyPath || "certificate.key" 29 | const cert = fs.readFileSync(certPath); 30 | const ca = fs.readFileSync(caPath); 31 | const key = fs.readFileSync(keyPath); 32 | 33 | const options = { 34 | cert, 35 | ca, 36 | key, 37 | }; 38 | 39 | serverHttps = https.createServer(options, app); 40 | } 41 | 42 | server = http.createServer(app); 43 | 44 | require("../db/connections/mongoose"); 45 | 46 | app.use(cors()); 47 | app.use(cookieParser(env.passwordCookie)); 48 | app.use(helmet()); 49 | app.use(compression()); 50 | app.use(express.json()); 51 | app.use(express.static(publicPath, { index: false })); 52 | app.use(bodyParser.json({ limit: "50mb" })); 53 | app.use( 54 | bodyParser.urlencoded({ 55 | limit: "50mb", 56 | extended: true, 57 | parameterLimit: 50000, 58 | }) 59 | ); 60 | // app.use(requestIp.mw()); 61 | 62 | app.use( 63 | busboy({ 64 | highWaterMark: 2 * 1024 * 1024, 65 | }) 66 | ); 67 | 68 | app.use(userRouter, fileRouter, folderRouter); 69 | 70 | app.use(middlewareErrorHandler); 71 | 72 | //const nodeMode = process.env.NODE_ENV ? "Production" : "Development/Testing"; 73 | 74 | //console.log("Node Enviroment Mode:", nodeMode); 75 | 76 | if (process.env.NODE_ENV === "production") { 77 | app.get("*", (_: Request, res: Response) => { 78 | res.sendFile(path.join(publicPath, "index.html")); 79 | }); 80 | } 81 | 82 | export default { server, serverHttps }; 83 | -------------------------------------------------------------------------------- /backend/services/chunk-service/actions/file-system-actions.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { UserInterface } from "../../../models/user-model"; 3 | import { GenericParams, IStorageActions } from "../store-types"; 4 | import env from "../../../enviroment/env"; 5 | import { getFSStoragePath } from "../../../utils/getFSStoragePath"; 6 | 7 | class FilesystemActions implements IStorageActions { 8 | async getAuth() { 9 | return {}; 10 | } 11 | 12 | createReadStream(params: GenericParams): NodeJS.ReadableStream { 13 | if (!params.filePath) throw new Error("File path not configured"); 14 | const fsReadableStream = fs.createReadStream(params.filePath); 15 | return fsReadableStream; 16 | } 17 | createReadStreamWithRange(params: GenericParams, start: number, end: number) { 18 | if (!params.filePath) throw new Error("File path not configured"); 19 | const fsReadableStream = fs.createReadStream(params.filePath, { 20 | start, 21 | end, 22 | }); 23 | return fsReadableStream; 24 | } 25 | removeChunks(params: GenericParams) { 26 | return new Promise((resolve, reject) => { 27 | if (!params.filePath) { 28 | reject("File path not configured"); 29 | return; 30 | } 31 | fs.unlink(params.filePath, (err) => { 32 | if (err) { 33 | reject("Error removing file"); 34 | return; 35 | } 36 | 37 | resolve(); 38 | }); 39 | }); 40 | } 41 | getPrevIV(params: GenericParams, start: number) { 42 | return new Promise((resolve, reject) => { 43 | if (!params.filePath) throw new Error("File path not configured"); 44 | const stream = fs.createReadStream(params.filePath, { 45 | start, 46 | end: start + 15, 47 | }); 48 | 49 | stream.on("data", (data) => { 50 | resolve(data); 51 | }); 52 | }); 53 | } 54 | uploadFile = (params: GenericParams, stream: NodeJS.ReadableStream) => { 55 | return new Promise((resolve, reject) => { 56 | resolve(); 57 | }); 58 | }; 59 | createWriteStream = ( 60 | params: GenericParams, 61 | stream: NodeJS.ReadableStream, 62 | randomID: string 63 | ) => { 64 | const path = `${getFSStoragePath()}${randomID}`; 65 | return { 66 | writeStream: fs.createWriteStream(path), 67 | emitter: null, 68 | }; 69 | }; 70 | } 71 | 72 | export { FilesystemActions }; 73 | -------------------------------------------------------------------------------- /backend/services/chunk-service/actions/helper-actions.ts: -------------------------------------------------------------------------------- 1 | import { S3Actions } from "../actions/S3-actions"; 2 | import { FilesystemActions } from "../actions/file-system-actions"; 3 | import env from "../../../enviroment/env"; 4 | 5 | export const getStorageActions = () => { 6 | if (env.dbType === "s3") { 7 | return new S3Actions(); 8 | } else { 9 | return new FilesystemActions(); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /backend/services/chunk-service/store-types.ts: -------------------------------------------------------------------------------- 1 | import internal from "stream"; 2 | 3 | export interface GenericParams { 4 | Key?: string; 5 | Bucket?: string; 6 | filePath?: string; 7 | [key: string]: any; 8 | } 9 | 10 | export interface IStorageActions { 11 | getAuth(): Object; 12 | createReadStream( 13 | params: GenericParams 14 | ): NodeJS.ReadableStream | internal.Readable; 15 | removeChunks(params: GenericParams): Promise; 16 | } 17 | -------------------------------------------------------------------------------- /backend/services/chunk-service/utils/ChunkInterface.ts: -------------------------------------------------------------------------------- 1 | import { UserInterface } from "../../../models/user-model"; 2 | import { FileInterface } from "../../../models/file-model"; 3 | import { Request, Response } from "express"; 4 | import { FolderInterface } from "../../../models/folder-model"; 5 | import crypto from "crypto"; 6 | 7 | interface ChunkInterface { 8 | uploadFile: ( 9 | user: UserInterface, 10 | busboy: any, 11 | req: Request 12 | ) => Promise; 13 | downloadFile: (user: UserInterface, fileID: string, res: Response) => void; 14 | getThumbnail: (user: UserInterface, id: string) => Promise; 15 | getFullThumbnail: ( 16 | user: UserInterface, 17 | fileID: string, 18 | res: Response 19 | ) => void; 20 | getPublicDownload: (fileID: string, tempToken: any, res: Response) => void; 21 | streamVideo: ( 22 | user: UserInterface, 23 | fileID: string, 24 | headers: any, 25 | res: Response, 26 | req: Request 27 | ) => void; 28 | getFileReadStream: (user: UserInterface, fileID: string) => any; 29 | trashMulti: ( 30 | userID: string, 31 | items: { 32 | type: "file" | "folder"; 33 | id: string; 34 | file?: FileInterface; 35 | folder?: FolderInterface; 36 | }[] 37 | ) => Promise; 38 | } 39 | 40 | export default ChunkInterface; 41 | -------------------------------------------------------------------------------- /backend/services/chunk-service/utils/awaitStream.ts: -------------------------------------------------------------------------------- 1 | const awaitStream = ( 2 | inputSteam: any, 3 | outputStream: any, 4 | allStreamsToErrorCatch: any[] 5 | ) => { 6 | return new Promise((resolve, reject) => { 7 | allStreamsToErrorCatch.forEach((currentStream: any) => { 8 | currentStream.on("error", (e: Error) => { 9 | reject({ 10 | message: "Await Stream Input Error", 11 | code: 500, 12 | error: e, 13 | }); 14 | }); 15 | }); 16 | 17 | inputSteam.pipe(outputStream).on("finish", (data: T) => { 18 | resolve(data); 19 | }); 20 | }); 21 | }; 22 | 23 | export default awaitStream; 24 | -------------------------------------------------------------------------------- /backend/services/chunk-service/utils/awaitUploadStreamFS.ts: -------------------------------------------------------------------------------- 1 | import {Request} from "express" 2 | import removeChunksFS from "./removeChunksFS"; 3 | 4 | const awaitUploadStream = (inputSteam: any, outputStream: any, req: Request, path: string, allStreamsToCatchError: any[]) => { 5 | 6 | return new Promise((resolve, reject) => { 7 | 8 | allStreamsToCatchError.forEach((currentStream: any) => { 9 | 10 | currentStream.on("error", (e: Error) => { 11 | 12 | removeChunksFS(path); 13 | 14 | reject({ 15 | message: "Await Stream Input Error", 16 | code: 500, 17 | error: e 18 | }) 19 | }) 20 | }) 21 | 22 | req.on("aborted", () => { 23 | 24 | removeChunksFS(path); 25 | }) 26 | 27 | inputSteam.pipe(outputStream).on("finish", (data: T) => { 28 | resolve(data); 29 | }) 30 | }) 31 | } 32 | 33 | export default awaitUploadStream; -------------------------------------------------------------------------------- /backend/services/chunk-service/utils/awaitUploadStreamS3.ts: -------------------------------------------------------------------------------- 1 | import { ManagedUpload } from "aws-sdk/clients/s3"; 2 | import s3 from "../../../db/connections/s3"; 3 | 4 | const uploadStreamS3 = (params: any) => { 5 | return new Promise((resolve, reject) => { 6 | s3.upload(params, (err: any, data: ManagedUpload.SendData) => { 7 | if (err) { 8 | console.log("Amazon upload err", err); 9 | reject("Amazon upload error"); 10 | } 11 | 12 | resolve(); 13 | }); 14 | }); 15 | }; 16 | 17 | export default uploadStreamS3; 18 | -------------------------------------------------------------------------------- /backend/services/chunk-service/utils/cachedSubscriptionStatuses.ts: -------------------------------------------------------------------------------- 1 | // class CachedSubsriptions { 2 | 3 | // cachedSubscriptionStatuses: any; 4 | 5 | // constructor() { 6 | // this.cachedSubscriptionStatuses = {} 7 | // } 8 | 9 | // addToCachedSubscriptionStatus = (userID: string) => { 10 | // this.cachedSubscriptionStatuses[userID] = true; 11 | // console.log("new cached", this.cachedSubscriptionStatuses) 12 | // } 13 | 14 | // checkCachedSubscriptionStatus = (userID: string) => { 15 | // console.log("cache check", this.cachedSubscriptionStatuses) 16 | // return this.cachedSubscriptionStatuses[userID]; 17 | // } 18 | // } 19 | 20 | // export default CachedSubsriptions; 21 | 22 | // const cachedSubscriptionStatuses: any = {}; 23 | 24 | // export default cachedSubscriptionStatuses; 25 | 26 | // import redis from "redis"; 27 | 28 | // const client = redis.createClient(); 29 | 30 | // export const setCachedValue = (userID: string) => { 31 | 32 | // const date = new Date(); 33 | 34 | // return new Promise((resolve, reject) => { 35 | // client.set(userID, date.getTime().toString(), (err, res) => { 36 | // if (err) { 37 | // console.log("Redis key err", err); 38 | // reject() 39 | // } 40 | // resolve(); 41 | // }); 42 | // }) 43 | // } 44 | 45 | // export const checkCachedValue = (userID: string) => { 46 | 47 | // return new Promise((resolve, reject) => { 48 | // client.get(userID, (err) => { 49 | // if (err) { 50 | // console.log("Redis key err", err); 51 | // reject() 52 | // } else { 53 | // resolve(); 54 | // } 55 | // }) 56 | // }) 57 | // } -------------------------------------------------------------------------------- /backend/services/chunk-service/utils/fixEndChunkLength.ts: -------------------------------------------------------------------------------- 1 | const fixEndChunkLength = (length: number) => { 2 | return Math.floor((length - 1) / 16) * 16 + 16; 3 | }; 4 | 5 | export default fixEndChunkLength; 6 | -------------------------------------------------------------------------------- /backend/services/chunk-service/utils/fixStartChunkLength.ts: -------------------------------------------------------------------------------- 1 | const fixStartChunkLength = (length: number) => { 2 | 3 | return Math.floor((length-1) / 16) * 16 - 16; 4 | } 5 | 6 | export default fixStartChunkLength; -------------------------------------------------------------------------------- /backend/services/chunk-service/utils/getFileSize.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | const getFileSize = (path: string) => { 4 | 5 | return new Promise((resolve, reject) => { 6 | 7 | fs.stat(path, (error, stats) => { 8 | 9 | if (error) { 10 | 11 | resolve(0); 12 | } 13 | 14 | resolve(stats.size); 15 | }); 16 | }) 17 | } 18 | 19 | export default getFileSize; -------------------------------------------------------------------------------- /backend/services/chunk-service/utils/getPrevIVFS.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | const getPrevIV = (start: number, path: string) => { 4 | 5 | return new Promise((resolve, reject) => { 6 | 7 | const stream = fs.createReadStream(path, { 8 | start, 9 | end: start + 15 10 | }) 11 | 12 | stream.on("data", (data) => { 13 | 14 | resolve(data); 15 | }) 16 | }) 17 | } 18 | 19 | export default getPrevIV; -------------------------------------------------------------------------------- /backend/services/chunk-service/utils/getPrevIVS3.ts: -------------------------------------------------------------------------------- 1 | import s3 from "../../../db/connections/s3"; 2 | import env from "../../../enviroment/env"; 3 | import { UserInterface } from "../../../models/user-model"; 4 | 5 | const getPrevIV = ( 6 | start: number, 7 | key: string, 8 | isPersonal: boolean, 9 | user: UserInterface 10 | ) => { 11 | return new Promise(async (resolve, reject) => { 12 | const params: any = { 13 | Bucket: env.s3Bucket, 14 | Key: key, 15 | Range: `bytes=${start}-${start + 15}`, 16 | }; 17 | 18 | const stream = s3.getObject(params).createReadStream(); 19 | 20 | stream.on("data", (data: any) => { 21 | resolve(data); 22 | }); 23 | }); 24 | }; 25 | 26 | export default getPrevIV; 27 | -------------------------------------------------------------------------------- /backend/services/chunk-service/utils/getThumbnailData.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "stream"; 2 | import { UserInterface } from "../../../models/user-model"; 3 | import { Response } from "express"; 4 | import ForbiddenError from "../../../utils/ForbiddenError"; 5 | import NotFoundError from "../../../utils/NotFoundError"; 6 | import crypto from "crypto"; 7 | import { createGenericParams } from "./storageHelper"; 8 | import { getStorageActions } from "../actions/helper-actions"; 9 | 10 | import ThumbnailDB from "../../../db/mongoDB/thumbnailDB"; 11 | 12 | const thumbnailDB = new ThumbnailDB(); 13 | 14 | const storageActions = getStorageActions(); 15 | 16 | const proccessData = ( 17 | res: Response, 18 | thumbnailID: string, 19 | user: UserInterface 20 | ) => { 21 | const eventEmitter = new EventEmitter(); 22 | 23 | const processFile = async () => { 24 | try { 25 | const thumbnail = await thumbnailDB.getThumbnailInfo( 26 | user._id.toString(), 27 | thumbnailID 28 | ); 29 | 30 | if (!thumbnail) throw new NotFoundError("Thumbnail Not Found"); 31 | 32 | const password = user.getEncryptionKey(); 33 | 34 | if (!password) throw new ForbiddenError("Invalid Encryption Key"); 35 | 36 | const IV = thumbnail.IV; 37 | 38 | const readStreamParams = createGenericParams({ 39 | filePath: thumbnail.path, 40 | Key: thumbnail.s3ID, 41 | }); 42 | 43 | const readStream = storageActions.createReadStream(readStreamParams); 44 | 45 | const CIPHER_KEY = crypto.createHash("sha256").update(password).digest(); 46 | 47 | const decipher = crypto.createDecipheriv("aes256", CIPHER_KEY, IV); 48 | 49 | decipher.on("error", (e: Error) => { 50 | eventEmitter.emit("error", e); 51 | }); 52 | 53 | readStream.on("error", (e: Error) => { 54 | eventEmitter.emit("error", e); 55 | }); 56 | 57 | res.on("error", (e: Error) => { 58 | eventEmitter.emit("error", e); 59 | }); 60 | 61 | readStream 62 | .pipe(decipher) 63 | .pipe(res) 64 | .on("finish", () => { 65 | eventEmitter.emit("finish"); 66 | }); 67 | } catch (e) { 68 | eventEmitter.emit("error", e); 69 | } 70 | }; 71 | 72 | processFile(); 73 | 74 | return eventEmitter; 75 | }; 76 | 77 | const getThumbnailData = ( 78 | res: Response, 79 | thumbnailID: string, 80 | user: UserInterface 81 | ) => { 82 | return new Promise((resolve, reject) => { 83 | const eventEmitter = proccessData(res, thumbnailID, user); 84 | eventEmitter.on("finish", (data) => { 85 | resolve(data); 86 | }); 87 | eventEmitter.on("error", (e) => { 88 | reject(e); 89 | }); 90 | }); 91 | }; 92 | 93 | export default getThumbnailData; 94 | -------------------------------------------------------------------------------- /backend/services/chunk-service/utils/removeChunksFS.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | const removeChunksFS = (path: string) => { 4 | return new Promise((resolve, reject) => { 5 | fs.unlink(path, (err) => { 6 | if (err) { 7 | console.log("Could not remove fs file", err); 8 | resolve(); 9 | } 10 | 11 | resolve(); 12 | }); 13 | }); 14 | }; 15 | 16 | export default removeChunksFS; 17 | module.exports = removeChunksFS; 18 | -------------------------------------------------------------------------------- /backend/services/chunk-service/utils/removeChunksS3.ts: -------------------------------------------------------------------------------- 1 | //import s3 from "../../../db/s3"; 2 | 3 | const removeChunksS3 = (s3: any, parmas: any) => { 4 | return new Promise((resolve, reject) => { 5 | s3.deleteObject(parmas, (err: any, data: any) => { 6 | if (err) { 7 | console.log("Could not remove S3 file"); 8 | reject("Could Not Remove S3 File"); 9 | } 10 | 11 | resolve(); 12 | }); 13 | }); 14 | }; 15 | 16 | export default removeChunksS3; 17 | module.exports = removeChunksS3; 18 | -------------------------------------------------------------------------------- /backend/services/chunk-service/utils/removeTempToken.ts: -------------------------------------------------------------------------------- 1 | import { UserInterface } from "../../../models/user-model"; 2 | 3 | const removeTempToken = async (user: UserInterface, tempToken: any) => { 4 | user.tempTokens = user.tempTokens.filter((filterToken) => { 5 | return filterToken.token !== tempToken; 6 | }); 7 | 8 | await user.save(); 9 | }; 10 | 11 | export default removeTempToken; 12 | -------------------------------------------------------------------------------- /backend/services/chunk-service/utils/storageHelper.ts: -------------------------------------------------------------------------------- 1 | import env from "../../../enviroment/env"; 2 | import { getFSStoragePath } from "../../../utils/getFSStoragePath"; 3 | 4 | type GenericParmasType = { 5 | filePath?: string; 6 | Key?: string; 7 | Bucket?: string; 8 | }; 9 | 10 | export const createGenericParams = ({ filePath, Key }: GenericParmasType) => { 11 | // TODO: Remove file split after migration 12 | if (env.dbType === "fs") { 13 | if (filePath?.includes("/")) { 14 | const filePathSplit = filePath!.split("/"); 15 | const fileName = filePathSplit[filePathSplit.length - 1]; 16 | return { 17 | filePath: getFSStoragePath() + fileName, 18 | }; 19 | } else { 20 | return { 21 | filePath: getFSStoragePath() + Key!, 22 | }; 23 | } 24 | } else { 25 | return { 26 | Key, 27 | }; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /backend/tempStorage/tempStorage.ts: -------------------------------------------------------------------------------- 1 | const tempStorage: any = {}; 2 | 3 | export default tempStorage; -------------------------------------------------------------------------------- /backend/types/file-types.ts: -------------------------------------------------------------------------------- 1 | export interface FileListQueryType { 2 | userID: string; 3 | search: string | undefined; 4 | parent: string; 5 | startAtDate: string | undefined; 6 | startAtName: string | undefined; 7 | trashMode: boolean; 8 | mediaMode: boolean; 9 | sortBy: string; 10 | mediaFilter: string; 11 | } 12 | -------------------------------------------------------------------------------- /backend/types/folder-types.ts: -------------------------------------------------------------------------------- 1 | export interface FolderListQueryType { 2 | userID: string; 3 | search: string | undefined; 4 | parent: string; 5 | trashMode: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /backend/utils/ConflictError.ts: -------------------------------------------------------------------------------- 1 | class ConflictError extends Error { 2 | code: number; 3 | 4 | constructor(args: any) { 5 | super(args); 6 | 7 | this.code = 409; 8 | } 9 | } 10 | 11 | export default ConflictError; 12 | -------------------------------------------------------------------------------- /backend/utils/ForbiddenError.ts: -------------------------------------------------------------------------------- 1 | class ForbiddenError extends Error { 2 | code: number; 3 | 4 | constructor(args: any) { 5 | super(args); 6 | 7 | this.code = 403; 8 | } 9 | } 10 | 11 | export default ForbiddenError; 12 | -------------------------------------------------------------------------------- /backend/utils/InternalServerError.ts: -------------------------------------------------------------------------------- 1 | class InternalServerError extends Error { 2 | code: number; 3 | constructor(args: any) { 4 | super(args); 5 | 6 | this.code = 500; 7 | } 8 | } 9 | 10 | export default InternalServerError; 11 | -------------------------------------------------------------------------------- /backend/utils/NotAuthorizedError.ts: -------------------------------------------------------------------------------- 1 | class NotAuthorizedError extends Error { 2 | code: number; 3 | 4 | constructor(args: any) { 5 | super(args); 6 | 7 | this.code = 401; 8 | } 9 | } 10 | 11 | export default NotAuthorizedError; 12 | -------------------------------------------------------------------------------- /backend/utils/NotEmailVerifiedError.ts: -------------------------------------------------------------------------------- 1 | class NotEmailVerifiedError extends Error { 2 | code: number; 3 | isCustomError: boolean; 4 | 5 | constructor(args: any) { 6 | super(args); 7 | 8 | this.code = 404; 9 | this.isCustomError = true; 10 | } 11 | } 12 | 13 | export default NotEmailVerifiedError; 14 | -------------------------------------------------------------------------------- /backend/utils/NotFoundError.ts: -------------------------------------------------------------------------------- 1 | class NotFoundError extends Error { 2 | code: number; 3 | 4 | constructor(args: any) { 5 | super(args); 6 | 7 | this.code = 404; 8 | } 9 | } 10 | 11 | export default NotFoundError; 12 | -------------------------------------------------------------------------------- /backend/utils/NotValidDataError.ts: -------------------------------------------------------------------------------- 1 | class NotValidDataError extends Error { 2 | code: number; 3 | 4 | constructor(args: any) { 5 | super(args); 6 | 7 | this.code = 403; 8 | } 9 | } 10 | 11 | export default NotValidDataError; 12 | -------------------------------------------------------------------------------- /backend/utils/convertDriveFolderToMongoFolder.ts: -------------------------------------------------------------------------------- 1 | const convertDriveFolderToMongoFolder = (driveObj: any, ownerID: string) => { 2 | 3 | let convertedObj:any = {}; 4 | convertedObj._id = driveObj.id; 5 | convertedObj.name = driveObj.name; 6 | convertedObj.createdAt = driveObj.createdTime; 7 | convertedObj.owner = ownerID; 8 | convertedObj.parent = driveObj.parents[driveObj.parents.length - 1]; 9 | convertedObj.parentList = driveObj.parents; 10 | convertedObj.updatedAt = driveObj.createdTime; 11 | convertedObj.drive = true; 12 | convertedObj.googleDoc = driveObj.mimeType === "application/vnd.google-apps.document"; 13 | 14 | return convertedObj; 15 | } 16 | 17 | export default convertDriveFolderToMongoFolder; -------------------------------------------------------------------------------- /backend/utils/convertDriveFoldersToMongoFolders.ts: -------------------------------------------------------------------------------- 1 | import convertDriveFolderToMongoFolder from "./convertDriveFolderToMongoFolder"; 2 | 3 | const convertDriveFoldersToMongoFolders = (driveObjs: any, ownerID: string) => { 4 | 5 | let convertedFolders = []; 6 | 7 | for (let currentFolder of driveObjs) { 8 | convertedFolders.push(convertDriveFolderToMongoFolder(currentFolder, ownerID)) 9 | } 10 | 11 | return convertedFolders; 12 | } 13 | 14 | export default convertDriveFoldersToMongoFolders; -------------------------------------------------------------------------------- /backend/utils/convertDriveListToMongoList.ts: -------------------------------------------------------------------------------- 1 | import convertDriveToMongo from "./convertDriveToMongo"; 2 | 3 | const convertDriveListToMongoList = (driveObjs: any, ownerID:string, pageToken?: string | null | undefined) => { 4 | 5 | let convertedObjs = []; 6 | 7 | for (let currentObj of driveObjs) { 8 | convertedObjs.push(convertDriveToMongo(currentObj, ownerID, pageToken)); 9 | } 10 | 11 | return convertedObjs; 12 | } 13 | 14 | export default convertDriveListToMongoList; -------------------------------------------------------------------------------- /backend/utils/convertDriveToMongo.ts: -------------------------------------------------------------------------------- 1 | import videoChecker from "./videoChecker"; 2 | import { FileInterface } from "../models/file-model"; 3 | 4 | const convertDriveToMongo = ( 5 | driveObj: any, 6 | ownerID: string, 7 | pageToken?: string | undefined | null 8 | ) => { 9 | let convertedObj: any = {}; 10 | convertedObj._id = driveObj.id; 11 | convertedObj.filename = driveObj.name; 12 | convertedObj.length = driveObj.size; 13 | convertedObj.uploadDate = driveObj.modifiedTime; 14 | convertedObj.pageToken = pageToken; 15 | convertedObj.metadata = { 16 | IV: "", 17 | hasThumbnail: driveObj.hasThumbnail, 18 | isVideo: videoChecker(driveObj.name), 19 | owner: ownerID, 20 | parent: 21 | driveObj.parents[driveObj.parents.length - 1] === "root" 22 | ? "/" 23 | : driveObj.parents[driveObj.parents.length - 1], 24 | parentList: driveObj.parents, 25 | size: driveObj.size, 26 | drive: true, 27 | googleDoc: driveObj.mimeType === "application/vnd.google-apps.document", 28 | thumbnailID: driveObj.thumbnailLink, 29 | link: driveObj.shared ? driveObj.webViewLink : undefined, 30 | linkType: driveObj.shared ? "public" : undefined, 31 | }; 32 | 33 | return convertedObj; 34 | }; 35 | 36 | export default convertDriveToMongo; 37 | -------------------------------------------------------------------------------- /backend/utils/createEmailTransporter.ts: -------------------------------------------------------------------------------- 1 | import env from "../enviroment/env"; 2 | import nodemailer from "nodemailer"; 3 | 4 | const createEmailTransporter = () => { 5 | const emailVerification = env.emailVerification === "true"; 6 | const emailAPIKey = env.emailAPIKey; 7 | const emailDomain = env.emailDomain; 8 | const emailHost = env.emailHost; 9 | const emailPort = env.emailPort; 10 | const emailAddress = env.emailAddress; 11 | 12 | if (!emailVerification) { 13 | throw new Error("Email Verification Not Enabled"); 14 | } 15 | 16 | if (!emailAPIKey || !emailDomain || !emailHost || !emailAddress) { 17 | throw new Error("Email Verification Not Setup Correctly"); 18 | } 19 | 20 | const transporter = nodemailer.createTransport({ 21 | // @ts-ignore 22 | host: emailHost, 23 | port: emailPort || 587, 24 | auth: { 25 | user: emailDomain, 26 | pass: emailAPIKey, 27 | }, 28 | }); 29 | 30 | return transporter; 31 | }; 32 | 33 | export default createEmailTransporter; 34 | -------------------------------------------------------------------------------- /backend/utils/createQueryGoogle.ts: -------------------------------------------------------------------------------- 1 | const createQueryGoogle = (query: any, parent: any) => { 2 | 3 | let queryBuilder = `mimeType != "application/vnd.google-apps.folder"` 4 | 5 | let orderBy = "" 6 | 7 | if (query.sortby === "date_desc" || query.sortby === "DEFAULT") { 8 | orderBy = "modifiedTime desc" 9 | } else if (query.sortby === "date_asc") { 10 | orderBy = "modifiedTime asc" 11 | } else if (query.sortby === "alp_desc") { 12 | orderBy = "name desc" 13 | } else { 14 | orderBy = "name asc" 15 | } 16 | 17 | if (query.search && query.search.length !== 0) { 18 | queryBuilder += ` and name contains "${query.search}"` 19 | } else { 20 | queryBuilder += ` and "${parent}" in parents` 21 | } 22 | 23 | queryBuilder += ` and trashed=false`; 24 | 25 | return {queryBuilder, orderBy} 26 | } 27 | 28 | export interface googleQueryType { 29 | limit: number, 30 | parent: string, 31 | pageToken: string, 32 | } 33 | 34 | export default createQueryGoogle; -------------------------------------------------------------------------------- /backend/utils/createQueryGoogleFolder.ts: -------------------------------------------------------------------------------- 1 | const createQueryGoogleFolder = (query: any, parent: string) => { 2 | 3 | let orderBy = "" 4 | 5 | if (query.sortby === "date_desc") { 6 | orderBy = "modifiedTime desc" 7 | } else if (query.sortby === "date_asc") { 8 | orderBy = "modifiedTime asc" 9 | } else if (query.sortby === "alp_desc") { 10 | orderBy = "name desc" 11 | } else { 12 | orderBy = "name asc" 13 | } 14 | 15 | let queryBuilder = `mimeType = "application/vnd.google-apps.folder"` 16 | 17 | if (query.search && query.search.length !== 0) { 18 | queryBuilder += ` and name contains "${query.search}"` 19 | } else { 20 | queryBuilder += ` and "${parent}" in parents`; 21 | } 22 | 23 | queryBuilder += ` and trashed=false`; 24 | 25 | return {orderBy, queryBuilder} 26 | } 27 | 28 | export default createQueryGoogleFolder; -------------------------------------------------------------------------------- /backend/utils/getFSStoragePath.ts: -------------------------------------------------------------------------------- 1 | import env from "../enviroment/env"; 2 | 3 | export const getFSStoragePath = () => { 4 | return env.fsDirectory 5 | }; 6 | -------------------------------------------------------------------------------- /backend/utils/getKeyFromTerminal.ts: -------------------------------------------------------------------------------- 1 | import prompts from "prompts"; 2 | 3 | const getKeyFromTerminal = async () => { 4 | return new Promise((resolve, _) => { 5 | setTimeout(async () => { 6 | const response = await prompts({ 7 | type: "password", 8 | name: "key", 9 | message: "Enter Server Encryption Key", 10 | }); 11 | 12 | resolve(response.key); 13 | }, 1500); 14 | }); 15 | }; 16 | 17 | export default getKeyFromTerminal; 18 | -------------------------------------------------------------------------------- /backend/utils/imageChecker.ts: -------------------------------------------------------------------------------- 1 | const imageExtList = ["jpeg", "jpg", "png", "gif", "svg", "tiff", "bmp"]; 2 | 3 | const imageChecker = (filename: string) => { 4 | if (filename.length < 1 || !filename.includes(".")) { 5 | return false; 6 | } 7 | 8 | const extSplit = filename.split("."); 9 | 10 | if (extSplit.length <= 1) { 11 | return false; 12 | } 13 | 14 | const ext = extSplit[extSplit.length - 1]; 15 | 16 | return imageExtList.includes(ext.toLowerCase()); 17 | }; 18 | 19 | export default imageChecker; 20 | -------------------------------------------------------------------------------- /backend/utils/mobileCheck.ts: -------------------------------------------------------------------------------- 1 | declare let window:any 2 | 3 | const mobilecheck = () => { 4 | var check = false; 5 | (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera); 6 | return check; 7 | }; 8 | export default mobilecheck; 9 | -------------------------------------------------------------------------------- /backend/utils/sanitizeFilename.ts: -------------------------------------------------------------------------------- 1 | const sanitizeFilename = (filename: string) => { 2 | filename = filename.replace(/[\u0000-\u001F\u007F\u202F]/g, " "); 3 | filename = filename.replace(/["<>:|?*\\;]/g, "_"); 4 | filename = filename.trim(); 5 | 6 | return filename; 7 | }; 8 | 9 | export default sanitizeFilename; 10 | -------------------------------------------------------------------------------- /backend/utils/sendPasswordResetEmail.ts: -------------------------------------------------------------------------------- 1 | import SMTPTransport from "nodemailer/lib/smtp-transport"; 2 | import env from "../enviroment/env"; 3 | import { UserInterface } from "../models/user-model"; 4 | import nodemailer from "nodemailer"; 5 | import createEmailTransporter from "./createEmailTransporter"; 6 | 7 | type MailOptionsType = { 8 | from: string; 9 | to: string; 10 | subject: string; 11 | text: string; 12 | }; 13 | 14 | const sendEmail = ( 15 | transporter: nodemailer.Transporter, 16 | mailOptions: MailOptionsType 17 | ) => { 18 | return new Promise((resolve, reject) => { 19 | transporter.sendMail(mailOptions, (error, info) => { 20 | if (error) { 21 | reject(error); 22 | } else { 23 | resolve(info); 24 | } 25 | }); 26 | }); 27 | }; 28 | 29 | const sendPasswordResetEmail = async ( 30 | user: UserInterface, 31 | resetToken: string 32 | ) => { 33 | try { 34 | // TODO: Fix any, for some reason some envs come up with a ts error for this 35 | const transporter = createEmailTransporter() as any; 36 | 37 | const emailAddress = env.emailAddress!; 38 | const url = env.remoteURL + `/reset-password/${resetToken}`; 39 | 40 | const mailOptions = { 41 | from: emailAddress, 42 | to: user.email, 43 | subject: "myDrive Password Reset", 44 | text: 45 | "Please navigate to the following link to reset your password: " + url, 46 | }; 47 | 48 | await sendEmail(transporter, mailOptions); 49 | 50 | return true; 51 | } catch (e) { 52 | console.log("Error sending password reset email", e); 53 | return false; 54 | } 55 | }; 56 | 57 | export default sendPasswordResetEmail; 58 | -------------------------------------------------------------------------------- /backend/utils/sendShareEmail.ts: -------------------------------------------------------------------------------- 1 | import env from "../enviroment/env"; 2 | // import sgMail from "@sendgrid/mail"; 3 | 4 | const currentURL = env.remoteURL; 5 | 6 | const sendShareEmail = async (file: any, respient: string) => { 7 | // if (process.env.NODE_ENV === "test") { 8 | // return; 9 | // } 10 | // const apiKey: any = env.sendgridKey; 11 | // const sendgridEmail:any = env.sendgridEmail; 12 | // sgMail.setApiKey(apiKey); 13 | // const fileLink = `${currentURL}/download-page/${file._id}/${file.metadata.link}` 14 | // const msg = { 15 | // to: respient, 16 | // from: sendgridEmail, 17 | // subject: "A File Was Shared With You Through myDrive", 18 | // text: `Please navigate to the following link to view the file ${fileLink}` 19 | // } 20 | // await sgMail.send(msg); 21 | }; 22 | 23 | export default sendShareEmail; 24 | -------------------------------------------------------------------------------- /backend/utils/sendVerificationEmail.ts: -------------------------------------------------------------------------------- 1 | import SMTPTransport from "nodemailer/lib/smtp-transport"; 2 | import env from "../enviroment/env"; 3 | import { UserInterface } from "../models/user-model"; 4 | import nodemailer from "nodemailer"; 5 | import createEmailTransporter from "./createEmailTransporter"; 6 | 7 | type MailOptionsType = { 8 | from: string; 9 | to: string; 10 | subject: string; 11 | text: string; 12 | }; 13 | 14 | const sendEmail = ( 15 | transporter: nodemailer.Transporter, 16 | mailOptions: MailOptionsType 17 | ) => { 18 | return new Promise((resolve, reject) => { 19 | transporter.sendMail(mailOptions, (error, info) => { 20 | if (error) { 21 | reject(error); 22 | } else { 23 | resolve(info); 24 | } 25 | }); 26 | }); 27 | }; 28 | 29 | const sendVerificationEmail = async ( 30 | user: UserInterface, 31 | emailToken: string 32 | ) => { 33 | try { 34 | // TODO: Fix any, for some reason some envs come up with a ts error for this 35 | const transporter = createEmailTransporter() as any; 36 | 37 | const emailAddress = env.emailAddress!; 38 | const url = env.remoteURL + `/verify-email/${emailToken}`; 39 | 40 | const mailOptions = { 41 | from: emailAddress, 42 | to: user.email, 43 | subject: "myDrive Email Verification", 44 | text: 45 | "Please navigate to the following link to verify your email address: " + 46 | url, 47 | }; 48 | 49 | await sendEmail(transporter, mailOptions); 50 | 51 | return true; 52 | } catch (e) { 53 | console.log("Error sending email verification", e); 54 | return false; 55 | } 56 | }; 57 | 58 | export default sendVerificationEmail; 59 | -------------------------------------------------------------------------------- /backend/utils/sortBySwitch.ts: -------------------------------------------------------------------------------- 1 | type SortOrder = 1 | -1; 2 | 3 | interface SortBy { 4 | [key: string]: SortOrder; 5 | } 6 | 7 | const sortBySwitch = (sortBy: string): SortBy => { 8 | if (sortBy === "date_desc") { 9 | return { uploadDate: -1 }; 10 | } else if (sortBy === "date_asc") { 11 | return { uploadDate: 1 }; 12 | } else if (sortBy === "alp_desc") { 13 | return { filename: -1 }; 14 | } else { 15 | return { filename: 1 }; 16 | } 17 | }; 18 | 19 | export default sortBySwitch; 20 | -------------------------------------------------------------------------------- /backend/utils/sortBySwitchFolder.ts: -------------------------------------------------------------------------------- 1 | import { SortOrder } from "mongoose"; 2 | 3 | interface SortBy { 4 | [key: string]: SortOrder; 5 | } 6 | 7 | const sortBySwitchFolder = (sortBy: string): SortBy => { 8 | switch (sortBy) { 9 | case "alp_asc": 10 | return { name: 1 }; 11 | case "alp_desc": 12 | return { name: -1 }; 13 | case "date_asc": 14 | return { createdAt: 1 }; 15 | default: 16 | return { createdAt: -1 }; 17 | } 18 | }; 19 | 20 | export default sortBySwitchFolder; 21 | -------------------------------------------------------------------------------- /backend/utils/sortGoogleMongoFolderList.ts: -------------------------------------------------------------------------------- 1 | import { FolderInterface } from "../models/folder-model"; 2 | 3 | const sortGoogleMongoFolderList = ( 4 | combinedList: FolderInterface[], 5 | query: any 6 | ) => { 7 | if (query.sortby === "date_desc" || query.sortby === "DEFAULT") { 8 | combinedList = combinedList.sort((a, b) => { 9 | const convertedDateA = new Date(a.createdAt).getTime(); 10 | const convertedDateB = new Date(b.createdAt).getTime(); 11 | return convertedDateB - convertedDateA; 12 | }); 13 | } else if (query.sortby === "date_asc") { 14 | combinedList = combinedList.sort((a, b) => { 15 | const convertedDateA = new Date(a.createdAt).getTime(); 16 | const convertedDateB = new Date(b.createdAt).getTime(); 17 | return convertedDateA - convertedDateB; 18 | }); 19 | } else if (query.sortby === "alp_desc") { 20 | combinedList = combinedList.sort((a, b) => { 21 | const name1 = a.name.toLowerCase(); 22 | const name2 = b.name.toLowerCase(); 23 | 24 | if (name1 > name2) { 25 | return -1; 26 | } 27 | 28 | if (name2 > name1) { 29 | return 1; 30 | } 31 | 32 | return 0; 33 | }); 34 | } else if (query.sortby === "alp_asc") { 35 | combinedList = combinedList.sort((a, b) => { 36 | const name1 = a.name.toLowerCase(); 37 | const name2 = b.name.toLowerCase(); 38 | 39 | if (name1 > name2) { 40 | return 1; 41 | } 42 | 43 | if (name2 > name1) { 44 | return -1; 45 | } 46 | 47 | return 0; 48 | }); 49 | } 50 | 51 | return combinedList; 52 | }; 53 | 54 | export default sortGoogleMongoFolderList; 55 | -------------------------------------------------------------------------------- /backend/utils/sortGoogleMongoList.ts: -------------------------------------------------------------------------------- 1 | import { FileInterface } from "../models/file-model"; 2 | 3 | const sortGoogleMongoList = (fileList: FileInterface[], query: any) => { 4 | let combinedList = fileList; 5 | 6 | if (query.sortby === "date_desc" || query.sortby === "DEFAULT") { 7 | combinedList = combinedList.sort((a, b) => { 8 | const convertedDateA = new Date(a.uploadDate).getTime(); 9 | const convertedDateB = new Date(b.uploadDate).getTime(); 10 | //onsole.log("data", b.uploadDate, convertedDate) 11 | return convertedDateB - convertedDateA; 12 | }); 13 | } else if (query.sortby === "date_asc") { 14 | combinedList = combinedList.sort((a, b) => { 15 | const convertedDateA = new Date(a.uploadDate).getTime(); 16 | const convertedDateB = new Date(b.uploadDate).getTime(); 17 | //onsole.log("data", b.uploadDate, convertedDate) 18 | return convertedDateA - convertedDateB; 19 | }); 20 | } else if (query.sortby === "alp_desc") { 21 | combinedList = combinedList.sort((a, b) => { 22 | const name1 = a.filename.toLowerCase(); 23 | const name2 = b.filename.toLowerCase(); 24 | 25 | if (name1 > name2) { 26 | return -1; 27 | } 28 | 29 | if (name2 > name1) { 30 | return 1; 31 | } 32 | 33 | return 0; 34 | }); 35 | } else if (query.sortby === "alp_asc") { 36 | combinedList = combinedList.sort((a, b) => { 37 | const name1 = a.filename.toLowerCase(); 38 | const name2 = b.filename.toLowerCase(); 39 | 40 | if (name1 > name2) { 41 | return 1; 42 | } 43 | 44 | if (name2 > name1) { 45 | return -1; 46 | } 47 | 48 | return 0; 49 | }); 50 | } 51 | 52 | return combinedList; 53 | }; 54 | 55 | export default sortGoogleMongoList; 56 | -------------------------------------------------------------------------------- /backend/utils/sortGoogleMongoQuickFiles.ts: -------------------------------------------------------------------------------- 1 | const sortGoogleMongoQuickFiles = (convertedFiles: any[], quickList: any[]) => { 2 | 3 | let combinedData = [...convertedFiles, ...quickList] 4 | 5 | combinedData = combinedData.sort((a, b) => { 6 | const convertedDateA = new Date(a.uploadDate).getTime(); 7 | const convertedDateB = new Date(b.uploadDate).getTime(); 8 | 9 | return convertedDateB - convertedDateA; 10 | }) 11 | 12 | if (combinedData.length >= 10) { 13 | combinedData = combinedData.slice(0, 10); 14 | } 15 | 16 | return combinedData; 17 | } 18 | 19 | export default sortGoogleMongoQuickFiles; -------------------------------------------------------------------------------- /backend/utils/streamToBuffer.ts: -------------------------------------------------------------------------------- 1 | const streamToBuffer = (stream: any) => { 2 | return new Promise((resolve, reject) => { 3 | const chunks: any[] = []; 4 | stream.on("data", (chunk: any) => chunks.push(chunk)); 5 | stream.on("error", reject); 6 | stream.on("end", () => resolve(Buffer.concat(chunks))); 7 | }); 8 | }; 9 | 10 | export default streamToBuffer; 11 | -------------------------------------------------------------------------------- /backend/utils/userUpdateCheck.ts: -------------------------------------------------------------------------------- 1 | import User, { UserInterface } from "../models/user-model"; 2 | import mongoose from "mongoose"; 3 | import { Request, Response, NextFunction } from "express"; 4 | import NotFoundError from "./NotFoundError"; 5 | import { createLoginCookie } from "../cookies/create-cookies"; 6 | 7 | // interface RequestType extends Request { 8 | // user?: userAccessType, 9 | // token?: string, 10 | // encryptedToken?: string, 11 | // } 12 | 13 | type userAccessType = { 14 | _id: mongoose.Types.ObjectId; 15 | emailVerified: boolean; 16 | email: string; 17 | botChecked: boolean; 18 | }; 19 | 20 | const userUpdateCheck = async ( 21 | res: Response, 22 | id: mongoose.Types.ObjectId, 23 | uuid: string | undefined 24 | ) => { 25 | const updatedUser = await User.findById(id); 26 | 27 | if (!updatedUser) throw new NotFoundError("Cannot find updated user auth"); 28 | 29 | if (updatedUser.emailVerified) { 30 | const { accessToken, refreshToken } = await updatedUser.generateAuthToken( 31 | uuid 32 | ); 33 | createLoginCookie(res, accessToken, refreshToken); 34 | } 35 | 36 | let strippedUser: userAccessType = { 37 | _id: updatedUser._id, 38 | emailVerified: updatedUser.emailVerified!, 39 | email: updatedUser.email, 40 | botChecked: false, 41 | }; 42 | 43 | return strippedUser; 44 | }; 45 | 46 | export default userUpdateCheck; 47 | -------------------------------------------------------------------------------- /backend/utils/videoChecker.ts: -------------------------------------------------------------------------------- 1 | const videoExtList = [ 2 | "3g2", 3 | "3gp", 4 | "aaf", 5 | "asf", 6 | "avchd", 7 | "avi", 8 | "drc", 9 | "flv", 10 | "m2v", 11 | "m4p", 12 | "m4v", 13 | "mkv", 14 | "mng", 15 | "mov", 16 | "mp2", 17 | "mp4", 18 | "mpe", 19 | "mpeg", 20 | "mpg", 21 | "mpv", 22 | "mxf", 23 | "nsv", 24 | "ogg", 25 | "ogv", 26 | "qt", 27 | "rm", 28 | "rmvb", 29 | "roq", 30 | "svi", 31 | "vob", 32 | "webm", 33 | "wmv", 34 | "yuv" 35 | ] 36 | 37 | const videoChecker = (filename: string) => { 38 | 39 | if (filename.length < 1 || !filename.includes(".")) { 40 | 41 | return false; 42 | } 43 | 44 | const extSplit = filename.split("."); 45 | 46 | if (extSplit.length <= 1) { 47 | 48 | return false; 49 | } 50 | 51 | const ext = extSplit[extSplit.length - 1]; 52 | 53 | return videoExtList.includes(ext.toLowerCase()); 54 | 55 | } 56 | 57 | export default videoChecker; -------------------------------------------------------------------------------- /docker-compose-test.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | container_name: mydrive 7 | volumes: 8 | # Use the following volumes section if you want to use bind mounts: 9 | # - /path/to/data:/data/ 10 | # - /path/to/temp:/temp/ 11 | 12 | # Use the following volumes section if you want to use named volumes: 13 | - mydrive-data:/data/ 14 | - mydrive-temp:/temp/ 15 | ports: 16 | - "${HTTP_PORT:-3000}:3000" 17 | # Optional: Uncomment the following line if you want to use HTTPS 18 | # - "${HTTPS_PORT:-8080}:8080" 19 | 20 | # Use expose: if using a reverse proxy 21 | # expose: 22 | # - 3000 23 | # - 8080 24 | env_file: 25 | - .env.test # Copy .env.example to .env.test or .env and fill in the values 26 | 27 | mongo: 28 | image: mongo:8 29 | container_name: mongo 30 | restart: always 31 | environment: 32 | MONGO_INITDB_ROOT_USERNAME: username 33 | MONGO_INITDB_ROOT_PASSWORD: password 34 | expose: 35 | - 27017 36 | volumes: 37 | - db-data:/data/db 38 | healthcheck: 39 | test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] 40 | timeout: 10s 41 | interval: 10s 42 | retries: 10 43 | start_period: 10s 44 | 45 | # Use the following volumes section if you want to use named volumes. Useful for development. 46 | volumes: 47 | mydrive-data: 48 | mydrive-temp: 49 | db-data: 50 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | container_name: mydrive 4 | image: kylehoell/mydrive:latest 5 | volumes: 6 | # Use the following volumes section if you want to use bind mounts: 7 | # - /path/to/data:/data/ 8 | # - /path/to/temp:/temp/ 9 | 10 | # Use the following volumes section if you want to use named volumes: 11 | - mydrive-data:/data/ 12 | - mydrive-temp:/temp/ 13 | ports: 14 | - "${HTTP_PORT:-3000}:3000" 15 | # Optional: Uncomment the following line if you want to use HTTPS 16 | #- "${HTTPS_PORT:-8080}:8080" 17 | 18 | # Use expose: if using a reverse proxy 19 | # expose: 20 | # - 3000 21 | # - 8080 22 | env_file: 23 | - .env # Copy .env.example to .env and fill in the values 24 | 25 | mongo: 26 | image: mongo:8 27 | container_name: mongo 28 | restart: always 29 | environment: 30 | MONGO_INITDB_ROOT_USERNAME: username 31 | MONGO_INITDB_ROOT_PASSWORD: password 32 | expose: 33 | - 27017 34 | volumes: 35 | - db-data:/data/db 36 | healthcheck: 37 | test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] 38 | timeout: 10s 39 | interval: 10s 40 | retries: 10 41 | start_period: 10s 42 | 43 | # Use the following volumes section if you want to use named volumes. 44 | volumes: 45 | mydrive-data: 46 | mydrive-temp: 47 | db-data: 48 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | import tseslint from "typescript-eslint"; 4 | import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; 5 | import pluginReactHooks from "eslint-plugin-react-hooks"; 6 | import pluginReact from "eslint-plugin-react"; 7 | 8 | export default [ 9 | { files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"] }, 10 | { languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } }, 11 | { languageOptions: { globals: globals.browser } }, 12 | pluginJs.configs.recommended, 13 | ...tseslint.configs.recommended, 14 | { 15 | ...pluginReactConfig, 16 | plugins: { 17 | react: pluginReact, 18 | "react-hooks": pluginReactHooks, 19 | }, 20 | rules: { 21 | "react/react-in-jsx-scope": "off", 22 | "react-hooks/rules-of-hooks": "error", 23 | "react-hooks/exhaustive-deps": "error", 24 | }, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /github_images/context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/github_images/context.png -------------------------------------------------------------------------------- /github_images/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/github_images/download.png -------------------------------------------------------------------------------- /github_images/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/github_images/homepage.png -------------------------------------------------------------------------------- /github_images/image-viewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/github_images/image-viewer.png -------------------------------------------------------------------------------- /github_images/media-viewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/github_images/media-viewer.png -------------------------------------------------------------------------------- /github_images/move.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/github_images/move.png -------------------------------------------------------------------------------- /github_images/multiselect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/github_images/multiselect.png -------------------------------------------------------------------------------- /github_images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/github_images/search.png -------------------------------------------------------------------------------- /github_images/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/github_images/share.png -------------------------------------------------------------------------------- /github_images/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/github_images/trash.png -------------------------------------------------------------------------------- /github_images/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/github_images/upload.png -------------------------------------------------------------------------------- /github_images/video-viewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/github_images/video-viewer.png -------------------------------------------------------------------------------- /github_images/youtube-video.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/github_images/youtube-video.jpeg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | myDrive 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /key/getKey.js: -------------------------------------------------------------------------------- 1 | const prompt = require('password-prompt') 2 | const env = require("../dist/enviroment/env") 3 | const crypto = require("crypto"); 4 | 5 | // NOT IN USE 6 | 7 | const getKey = async() => { 8 | 9 | if (process.env.KEY) { 10 | // For Docker 11 | 12 | let password = process.env.KEY; 13 | 14 | password = crypto.createHash("md5").update(password).digest("hex"); 15 | 16 | env.key = password; 17 | 18 | //console.log("Docker Key", env.key); 19 | 20 | } else if (process.env.NODE_ENV) { 21 | 22 | let password = await prompt("Enter Server Encryption Password: ", {method: "hide"}); 23 | 24 | password = crypto.createHash("md5").update(password).digest("hex"); 25 | 26 | env.key = password; 27 | 28 | } else { 29 | 30 | let password = "1234"; 31 | 32 | password = crypto.createHash("md5").update(password).digest("hex"); 33 | 34 | env.key = password; 35 | } 36 | } 37 | 38 | module.exports = getKey; -------------------------------------------------------------------------------- /key/getNewKey.js: -------------------------------------------------------------------------------- 1 | const prompt = require('password-prompt') 2 | import env from "../backend/enviroment/env"; 3 | const crypto = require("crypto"); 4 | 5 | const getKey = async() => { 6 | 7 | let password = await prompt("Enter New Server Encryption Password: ", {method: "hide"}); 8 | 9 | let confirmPassword = await prompt("Verify New Server Encryption Password: ", {method: "hide"}); 10 | 11 | if (password !== confirmPassword) { 12 | console.log("New Passwords do not match, exiting..."); 13 | process.exit(); 14 | } 15 | 16 | password = crypto.createHash("md5").update(password).digest("hex"); 17 | 18 | env.newKey = password; 19 | 20 | } 21 | 22 | module.exports = getKey; -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["dist-backend/"], 3 | "ignore": ["src", "node_modules"], 4 | "ext": "js,json", 5 | "exec": "node dist-backend/server/server-start.js" 6 | } 7 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/public/.DS_Store -------------------------------------------------------------------------------- /public/images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/public/images/.DS_Store -------------------------------------------------------------------------------- /public/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subnub/myDrive/bae5dbbd53dfda744bc81d681aa87d9fee8979d8/public/images/icon.png -------------------------------------------------------------------------------- /serverUtils/backupDatabase.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("../backend/db/mongooseServerUtils"); 2 | const conn = mongoose.connection; 3 | const prompts = require("prompts"); 4 | const createTempDirectory = require("./createTempDirectory"); 5 | 6 | const waitForDatabase = () => { 7 | 8 | return new Promise((resolve, reject) => { 9 | 10 | if (conn.readyState !== 1) { 11 | 12 | conn.once("open", () => { 13 | 14 | resolve(); 15 | 16 | }) 17 | 18 | } else { 19 | 20 | resolve(); 21 | } 22 | 23 | }) 24 | } 25 | 26 | const copyDatabase = async() => { 27 | 28 | console.log("Waiting For Database Connection..."); 29 | await waitForDatabase(); 30 | console.log("Connected To Database\n"); 31 | 32 | const userConfimation = await prompts({ 33 | type: 'text', 34 | message: "Warning: This will create a new Database backup, overwriting\n" + 35 | "the current database backup. Only ONE Database backup\n" + 36 | "can Be Stored At A Time.\n" + 37 | "For more permanent backups, use MongoExport, or \n" + 38 | "Backup data manually. \n" + 39 | "Would you like to continue? (Yes/No)", 40 | name: "value" 41 | }) 42 | 43 | if (!userConfimation.value || userConfimation.value.toLowerCase() !== "yes") { 44 | 45 | console.log("Exiting...") 46 | process.exit() 47 | return; 48 | } 49 | 50 | await createTempDirectory(); 51 | 52 | console.log("Finished Copying Database, Exiting..."); 53 | process.exit(); 54 | } 55 | 56 | copyDatabase() -------------------------------------------------------------------------------- /serverUtils/createIndexes.js: -------------------------------------------------------------------------------- 1 | const getEnvVariables = require("../dist/enviroment/getEnvVariables"); 2 | getEnvVariables(); 3 | const mongoose = require("./mongoServerUtil"); 4 | const conn = mongoose.connection; 5 | 6 | const waitForDatabase = () => { 7 | 8 | return new Promise((resolve, reject) => { 9 | 10 | if (conn.readyState !== 1) { 11 | 12 | conn.once("open", () => { 13 | 14 | resolve(); 15 | 16 | }) 17 | 18 | } else { 19 | 20 | resolve(); 21 | } 22 | 23 | }) 24 | } 25 | 26 | const createIndexes = async() => { 27 | 28 | console.log("Waiting For Database..."); 29 | await waitForDatabase(); 30 | console.log("Connected To Database\n"); 31 | 32 | console.log("Creating Indexes...") 33 | await conn.db.collection("fs.files").createIndex({uploadDate: 1}); 34 | await conn.db.collection("fs.files").createIndex({uploadDate: -1}); 35 | await conn.db.collection("fs.files").createIndex({filename: 1}); 36 | await conn.db.collection("fs.files").createIndex({filename: -1}); 37 | await conn.db.collection("fs.files").createIndex({"metadata.owner": 1}); 38 | 39 | await conn.db.collection("folders").createIndex({createdAt: 1}) 40 | await conn.db.collection("folders").createIndex({createdAt: -1}) 41 | await conn.db.collection("folders").createIndex({name: 1}); 42 | await conn.db.collection("folders").createIndex({name: -1}) 43 | await conn.db.collection("folders").createIndex({owner: 1}) 44 | 45 | await conn.db.collection("thumbnails").createIndex({owner: 1}) 46 | console.log("Indexes Created"); 47 | 48 | process.exit(); 49 | 50 | } 51 | 52 | createIndexes(); -------------------------------------------------------------------------------- /serverUtils/createVideoThumbnails.js: -------------------------------------------------------------------------------- 1 | const getEnvVariables = require("../dist-backend/enviroment/get-env-variables"); 2 | getEnvVariables(); 3 | const mongoose = require("./mongoServerUtil"); 4 | const conn = mongoose.connection; 5 | const File = require("../dist-backend/models/file-model"); 6 | const User = require("../dist-backend/models/user-model"); 7 | const createVideoThumbnail = 8 | require("../dist-backend/services/chunk-service/utils/createVideoThumbnail").default; 9 | const getKey = require("../dist-backend/key/get-key").default; 10 | 11 | const waitForDatabase = () => { 12 | return new Promise((resolve, reject) => { 13 | // Wait for the database to be ready. 14 | const timeoutWait = () => { 15 | setTimeout(() => resolve(), 3000); 16 | }; 17 | 18 | if (conn.readyState !== 1) { 19 | conn.once("open", () => { 20 | timeoutWait(); 21 | }); 22 | } else { 23 | timeoutWait(); 24 | } 25 | }); 26 | }; 27 | 28 | // Wait to be after anything else may be printed to the terminal 29 | const terminalWait = () => { 30 | return new Promise((resolve) => { 31 | setTimeout(() => resolve(), 2000); 32 | }); 33 | }; 34 | 35 | const updateDocs = async () => { 36 | await terminalWait(); 37 | console.log(`Updating video thumbnails, env is ${process.env.NODE_ENV}`); 38 | 39 | console.log("\nWaiting for database..."); 40 | await waitForDatabase(); 41 | console.log("Connected to database\n"); 42 | 43 | console.log("Getting Key..."); 44 | await getKey(); 45 | console.log("Key Got\n"); 46 | 47 | // console.log("env", process.env.KEY); 48 | 49 | console.log("Getting file list..."); 50 | const files = await File.find({ 51 | filename: { 52 | $regex: 53 | /\.(mp4|mov|avi|mkv|webm|wmv|flv|mpg|mpeg|3gp|3g2|mxf|ogv|ogg|m4v)$/i, 54 | }, 55 | "metadata.thumbnailID": "", 56 | }); 57 | 58 | console.log("Found", files.length, "files"); 59 | 60 | for (let i = 0; i < files.length; i++) { 61 | try { 62 | const currentFile = files[i]; 63 | 64 | console.log(`Progress ${i + 1}/${files.length}`); 65 | 66 | const user = await User.findById(currentFile.metadata.owner); 67 | 68 | await createVideoThumbnail(currentFile, currentFile.filename, user); 69 | } catch (e) { 70 | console.log("error creating video thumbnail", e); 71 | } 72 | } 73 | 74 | console.log("Done"); 75 | 76 | process.exit(); 77 | }; 78 | 79 | updateDocs(); 80 | -------------------------------------------------------------------------------- /serverUtils/deleteDatabase.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("../backend/db/mongooseServerUtils"); 2 | const prompts = require("prompts"); 3 | const conn = mongoose.connection; 4 | 5 | const waitForDatabase = () => { 6 | 7 | return new Promise((resolve, reject) => { 8 | 9 | if (conn.readyState !== 1) { 10 | 11 | conn.once("open", () => { 12 | 13 | resolve(); 14 | 15 | }) 16 | 17 | } else { 18 | 19 | resolve(); 20 | } 21 | 22 | }) 23 | } 24 | 25 | const deleteTempDatabase = async() => { 26 | 27 | console.log("Waiting For Database..."); 28 | await waitForDatabase(); 29 | console.log("Connected To Database\n") 30 | 31 | const userConfimation = await prompts({ 32 | type: 'text', 33 | message: "Warning: This will delete all the data in the Main Database,\n" + 34 | "this will not delete any data in the Database Backup.\n" + 35 | "Would you like to continue? (Yes/No)", 36 | name: "value" 37 | }) 38 | 39 | if (!userConfimation.value || userConfimation.value.toLowerCase() !== "yes") { 40 | 41 | console.log("Exiting...") 42 | process.exit() 43 | return; 44 | } 45 | 46 | console.log("Removing Collections..."); 47 | 48 | try { 49 | await conn.db.collection("fs.files").drop(); 50 | } catch (e) {} 51 | 52 | try { 53 | await conn.db.collection("fs.chunks").drop(); 54 | } catch (e) {} 55 | 56 | try { 57 | await conn.db.collection("thumbnails").drop(); 58 | } catch (e) {} 59 | 60 | try { 61 | await conn.db.collection("folders").drop(); 62 | } catch (e) {} 63 | 64 | try { 65 | await conn.db.collection("videos.files").drop(); 66 | } catch (e) {} 67 | 68 | try { 69 | await conn.db.collection("videos.chunks").drop(); 70 | } catch (e) {} 71 | 72 | try { 73 | await conn.db.collection("users").drop(); 74 | } catch (e) {} 75 | 76 | console.log("Removed Collections\n") 77 | 78 | process.exit(); 79 | } 80 | 81 | deleteTempDatabase(); -------------------------------------------------------------------------------- /serverUtils/deleteTempDatabase.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("../backend/db/mongooseServerUtils"); 2 | const prompts = require("prompts"); 3 | const conn = mongoose.connection; 4 | 5 | const waitForDatabase = () => { 6 | 7 | return new Promise((resolve, reject) => { 8 | 9 | if (conn.readyState !== 1) { 10 | 11 | conn.once("open", () => { 12 | 13 | resolve(); 14 | 15 | }) 16 | 17 | } else { 18 | 19 | resolve(); 20 | } 21 | 22 | }) 23 | } 24 | 25 | const deleteTempDatabase = async() => { 26 | 27 | console.log("Waiting For Database..."); 28 | await waitForDatabase(); 29 | console.log("Connected To Database\n") 30 | 31 | const userConfimation = await prompts({ 32 | type: 'text', 33 | message: "Warning: Deleting the Backup Database cannot be undone,\n" + 34 | "Would you like to continue? (Yes/No)", 35 | name: "value" 36 | }) 37 | 38 | if (!userConfimation.value || userConfimation.value.toLowerCase() !== "yes") { 39 | 40 | console.log("Exiting...") 41 | process.exit() 42 | return; 43 | } 44 | 45 | console.log("Removing Temporary Collections..."); 46 | 47 | try { 48 | await conn.db.collection("temp-fs.files").drop(); 49 | } catch (e) {} 50 | 51 | try { 52 | await conn.db.collection("temp-fs.chunks").drop(); 53 | } catch (e) {} 54 | 55 | try { 56 | await conn.db.collection("temp-thumbnails").drop(); 57 | } catch (e) {} 58 | 59 | try { 60 | await conn.db.collection("temp-folders").drop(); 61 | } catch (e) {} 62 | 63 | try { 64 | await conn.db.collection("temp-videos.files").drop(); 65 | } catch (e) {} 66 | 67 | try { 68 | await conn.db.collection("temp-videos.chunks").drop(); 69 | } catch (e) {} 70 | 71 | try { 72 | await conn.db.collection("temp-users").drop(); 73 | } catch (e) {} 74 | 75 | console.log("Removed Temporary Collections, Exiting..."); 76 | process.exit(); 77 | } 78 | 79 | deleteTempDatabase(); -------------------------------------------------------------------------------- /serverUtils/getEnvVaribables.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const getEnvVariables = () => { 4 | 5 | const configPath = path.join(__dirname, "..", "config"); 6 | 7 | require('dotenv').config({ path: configPath + "/prod.env"}) 8 | } 9 | 10 | module.exports = getEnvVariables; -------------------------------------------------------------------------------- /serverUtils/migrateMyDrive4.js: -------------------------------------------------------------------------------- 1 | const getEnvVariables = require("../dist-backend/enviroment/get-env-variables"); 2 | getEnvVariables(); 3 | const mongoose = require("./mongoServerUtil"); 4 | const conn = mongoose.connection; 5 | const File = require("../dist-backend/models/file-model"); 6 | 7 | const waitForDatabase = () => { 8 | return new Promise((resolve, reject) => { 9 | if (conn.readyState !== 1) { 10 | conn.once("open", () => { 11 | resolve(); 12 | }); 13 | } else { 14 | resolve(); 15 | } 16 | }); 17 | }; 18 | 19 | const updateDocs = async () => { 20 | console.log("\nWaiting for database..."); 21 | await waitForDatabase(); 22 | console.log("Connected to database\n"); 23 | 24 | console.log("Getting file list..."); 25 | const files = await File.find({}); 26 | console.log("Found", files.length, "files"); 27 | 28 | for (let i = 0; i < files.length; i++) { 29 | const currentFile = files[i]; 30 | 31 | await File.updateOne( 32 | { _id: currentFile._id }, 33 | { 34 | $set: { 35 | "metadata.owner": currentFile.metadata.owner.toString(), 36 | "metadata.thumbnailID": currentFile.metadata.thumbnailID.toString(), 37 | }, 38 | } 39 | ); 40 | } 41 | 42 | console.log("Done"); 43 | 44 | process.exit(); 45 | }; 46 | 47 | updateDocs(); 48 | -------------------------------------------------------------------------------- /serverUtils/mongoServerUtil.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | mongoose.connect(process.env.MONGODB_URL, {}); 4 | 5 | module.exports = mongoose; 6 | -------------------------------------------------------------------------------- /serverUtils/removeOldPersonalData.js: -------------------------------------------------------------------------------- 1 | const getEnvVariables = require("../dist/enviroment/getEnvVariables"); 2 | getEnvVariables() 3 | const mongoose = require("./mongoServerUtil"); 4 | const conn = mongoose.connection; 5 | const Thumbnail = require("../dist/models/thumbnail"); 6 | const File = require("../dist/models/file"); 7 | const User = require('../dist/models/user'); 8 | 9 | const DAY_LIMIT = 0; 10 | 11 | const waitForDatabase = () => { 12 | 13 | return new Promise((resolve, reject) => { 14 | 15 | if (conn.readyState !== 1) { 16 | 17 | conn.once("open", () => { 18 | 19 | resolve(); 20 | 21 | }) 22 | 23 | } else { 24 | 25 | resolve(); 26 | } 27 | 28 | }) 29 | } 30 | 31 | const removePersonalMetadata = async(user) => { 32 | 33 | const fileList = await conn.db.collection("fs.files").find({ 34 | "metadata.owner": user._id, 35 | "metadata.personalFile": true, 36 | }).toArray(); 37 | 38 | for (let currentFile of fileList) { 39 | 40 | await File.deleteOne({_id: currentFile._id}); 41 | 42 | if (currentFile.metadata.hasThumbnail) { 43 | 44 | await Thumbnail.deleteOne({_id: currentFile.metadata.thumbnailID}) 45 | } 46 | } 47 | 48 | await conn.db.collection("folders").deleteMany({'owner': user._id.toString(), 'personalFolder': true}) 49 | } 50 | 51 | const removeOldPeronsalData = async() => { 52 | 53 | console.log("Waiting for mongoDB Database..."); 54 | await waitForDatabase(); 55 | console.log("MongoDB Connection established\n"); 56 | 57 | const userList = await User.find({'personalStorageCanceledDate': {$exists: true}}); 58 | 59 | console.log('user list', userList.length); 60 | 61 | for (const currentUser of userList) { 62 | 63 | let date = new Date(currentUser.personalStorageCanceledDate); 64 | date.setDate(date.getDate() + DAY_LIMIT); 65 | 66 | const nowDate = new Date(); 67 | 68 | if (date.getTime() <= nowDate) { 69 | console.log(`\nUser ${currentUser.email} over expire limit for personal data, deleting metadata...`); 70 | await removePersonalMetadata(currentUser); 71 | console.log(`Removed user ${currentUser.email} personal metadata successfully`); 72 | } 73 | } 74 | 75 | console.log('\nFinished removing expired personal metadata') 76 | process.exit(); 77 | } 78 | 79 | removeOldPeronsalData(); -------------------------------------------------------------------------------- /serverUtils/removeTokens.js: -------------------------------------------------------------------------------- 1 | const getEnvVariables = require("../dist/enviroment/getEnvVariables"); 2 | getEnvVariables(); 3 | const mongoose = require("./mongoServerUtil"); 4 | const conn = mongoose.connection; 5 | const User = require("../dist/models/user"); 6 | 7 | const waitForDatabase = () => { 8 | 9 | return new Promise((resolve, reject) => { 10 | 11 | if (conn.readyState !== 1) { 12 | 13 | conn.once("open", () => { 14 | 15 | resolve(); 16 | 17 | }) 18 | 19 | } else { 20 | 21 | resolve(); 22 | } 23 | 24 | }) 25 | } 26 | 27 | const removeTokens = async() => { 28 | 29 | console.log("\nWaiting for database..."); 30 | await waitForDatabase(); 31 | console.log("Connected to database\n"); 32 | 33 | console.log("Removing tokens from users..."); 34 | const userList = await User.find({}); 35 | await User.updateMany({}, { 36 | tokens: [], 37 | tempTokens: [] 38 | }) 39 | console.log("Removed tokens from", userList.length, "users"); 40 | 41 | process.exit(); 42 | } 43 | 44 | removeTokens(); -------------------------------------------------------------------------------- /serverUtils/restoreDatabase.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("../backend/db/mongooseServerUtils"); 2 | const prompts = require("prompts"); 3 | const restoreFromTempDirectory = require("./restoreFromTempDirectory"); 4 | const conn = mongoose.connection; 5 | 6 | const waitForDatabase = () => { 7 | 8 | return new Promise((resolve, reject) => { 9 | 10 | if (conn.readyState !== 1) { 11 | 12 | conn.once("open", () => { 13 | 14 | resolve(); 15 | 16 | }) 17 | 18 | } else { 19 | 20 | resolve(); 21 | } 22 | 23 | }) 24 | } 25 | 26 | const restoreDatabase = async() => { 27 | 28 | const userConfimation = await prompts({ 29 | type: 'text', 30 | message: "Warning: This will delete ALL data," + 31 | " other than the Data Backup created by CopyDatabase. \nMake sure to first run CopyDatabase, and backup" + 32 | " your data, \nWould you like to continue? (Yes/No)", 33 | name: "value" 34 | }) 35 | 36 | if (!userConfimation.value || userConfimation.value.toLowerCase() !== "yes") { 37 | 38 | console.log("Exiting...") 39 | process.exit() 40 | return; 41 | 42 | } else { 43 | 44 | await waitForDatabase(); 45 | 46 | await restoreFromTempDirectory(); 47 | 48 | console.log("Finished Restoring Data, Exiting..."); 49 | process.exit(); 50 | } 51 | 52 | } 53 | 54 | restoreDatabase() 55 | 56 | -------------------------------------------------------------------------------- /src/api/userAPI.ts: -------------------------------------------------------------------------------- 1 | import axios from "../axiosInterceptor"; 2 | 3 | // GET 4 | 5 | export const getUserToken = async () => { 6 | const response = await axios.post("/user-service/get-token"); 7 | response.data; 8 | }; 9 | 10 | export const getUserAPI = async () => { 11 | const response = await axios.get("/user-service/user"); 12 | return response.data; 13 | }; 14 | 15 | export const getUserDetailedAPI = async () => { 16 | const response = await axios.get("/user-service/user-detailed"); 17 | return response.data; 18 | }; 19 | 20 | // POST 21 | 22 | export const loginAPI = async (email: string, password: string) => { 23 | const response = await axios.post("/user-service/login", { 24 | email, 25 | password, 26 | }); 27 | return response.data; 28 | }; 29 | 30 | export const createAccountAPI = async (email: string, password: string) => { 31 | const response = await axios.post("/user-service/create", { 32 | email, 33 | password, 34 | }); 35 | return response.data; 36 | }; 37 | 38 | export const logoutAPI = async () => { 39 | const response = await axios.post("/user-service/logout"); 40 | return response.data; 41 | }; 42 | 43 | export const logoutAllAPI = async () => { 44 | const response = await axios.post("/user-service/logout-all"); 45 | return response.data; 46 | }; 47 | 48 | export const getAccessToken = async (uuid: string) => { 49 | const response = await axios.post("/user-service/get-token", undefined, { 50 | headers: { 51 | uuid, 52 | }, 53 | }); 54 | return response.data; 55 | }; 56 | 57 | // PATCH 58 | 59 | export const changePasswordAPI = async ( 60 | oldPassword: string, 61 | newPassword: string 62 | ) => { 63 | const response = await axios.patch("/user-service/change-password", { 64 | oldPassword, 65 | newPassword, 66 | }); 67 | return response.data; 68 | }; 69 | 70 | export const resendVerifyEmailAPI = async () => { 71 | const response = await axios.patch("/user-service/resend-verify-email"); 72 | return response.data; 73 | }; 74 | 75 | export const verifyEmailAPI = async (emailToken: string) => { 76 | const response = await axios.patch("/user-service/verify-email", { 77 | emailToken, 78 | }); 79 | return response.data; 80 | }; 81 | 82 | export const sendPasswordResetAPI = async (email: string) => { 83 | const response = await axios.patch("/user-service/send-password-reset", { 84 | email, 85 | }); 86 | return response.data; 87 | }; 88 | 89 | export const resetPasswordAPI = async ( 90 | password: string, 91 | passwordToken: string 92 | ) => { 93 | const response = await axios.patch("/user-service/reset-password", { 94 | passwordToken, 95 | password, 96 | }); 97 | return response.data; 98 | }; 99 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | import { Provider } from "react-redux"; 3 | import store from "./store/configureStore"; 4 | import AppRouter from "./routers/AppRouter"; 5 | import "normalize.css/normalize.css"; 6 | import "./styles/styles.scss"; 7 | import "core-js/stable"; 8 | import "regenerator-runtime/runtime"; 9 | import "react-circular-progressbar/dist/styles.css"; 10 | import "react-toastify/dist/ReactToastify.css"; 11 | import { QueryClient, QueryClientProvider } from "react-query"; 12 | // import '../node_modules/@fortawesome/fontawesome-free/css/all.css'; 13 | // import '../node_modules/@fortawesome/fontawesome-free/js/all.js'; 14 | 15 | const queryClient = new QueryClient(); 16 | 17 | const jsxWrapper = ( 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | 25 | const root = ReactDOM.createRoot(document.getElementById("app")); 26 | root.render(jsxWrapper); 27 | -------------------------------------------------------------------------------- /src/axiosInterceptor/index.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import uuid from "uuid"; 3 | import getBackendURL from "../utils/getBackendURL"; 4 | 5 | let browserIDCheck = localStorage.getItem("browser-id"); 6 | 7 | const sleep = () => { 8 | return new Promise((resolve, reject) => { 9 | setTimeout(() => { 10 | resolve(true); 11 | }, 150); 12 | }); 13 | }; 14 | 15 | const axiosRetry = axios.create({ baseURL: getBackendURL() }); 16 | const axiosNoRetry = axios.create({ baseURL: getBackendURL() }); 17 | const axios3 = axios.create({ baseURL: getBackendURL() }); 18 | 19 | axiosRetry.interceptors.request.use( 20 | (config) => { 21 | if (!browserIDCheck) { 22 | browserIDCheck = uuid.v4(); 23 | localStorage.setItem("browser-id", browserIDCheck); 24 | } 25 | 26 | config.headers.uuid = browserIDCheck; 27 | 28 | return config; 29 | }, 30 | (error) => { 31 | return Promise.reject(error); 32 | } 33 | ); 34 | 35 | axiosRetry.interceptors.response.use( 36 | (response) => { 37 | //console.log("axios interceptor successful") 38 | return response; 39 | }, 40 | (error) => { 41 | return new Promise((resolve, reject) => { 42 | let originalRequest = error.config; 43 | 44 | if (error.response.status !== 401) { 45 | return reject(error); 46 | } 47 | 48 | if (originalRequest.ran === true) { 49 | //console.log("original request ran", error.config.url); 50 | return reject(error); 51 | } 52 | 53 | if (error.config.url === "/user-service/get-token") { 54 | //console.log("error url equal to refresh token route") 55 | return reject(); 56 | } 57 | 58 | if (!browserIDCheck) { 59 | browserIDCheck = uuid.v4(); 60 | localStorage.setItem("browser-id", browserIDCheck); 61 | } 62 | 63 | axiosNoRetry 64 | .post( 65 | "/user-service/get-token", 66 | {}, 67 | { 68 | headers: { 69 | uuid: browserIDCheck, 70 | }, 71 | } 72 | ) 73 | .then((cookieResponse) => { 74 | // We need to sleep before requesting again, if not I believe 75 | // The old request will still be open and it will not make a 76 | // Brand new request sometimes, so it will log users out 77 | // But adding a sleep function seems to fix this. 78 | return sleep(); 79 | }) 80 | .then((sleepres) => { 81 | return axios3(originalRequest); 82 | }) 83 | .then((response) => { 84 | resolve(response); 85 | }) 86 | .catch((e) => { 87 | //console.log("error"); 88 | return reject(error); 89 | }); 90 | }); 91 | } 92 | ); 93 | 94 | export default axiosRetry; 95 | -------------------------------------------------------------------------------- /src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "react-router-dom"; 2 | import SearchBar from "../SearchBar/SearchBar"; 3 | import MenuIcon from "../../icons/MenuIcon"; 4 | import { useAppDispatch, useAppSelector } from "../../hooks/store"; 5 | import { closeDrawer, openDrawer } from "../../reducers/leftSection"; 6 | import { useUtils } from "../../hooks/utils"; 7 | import ChevronOutline from "../../icons/ChevronOutline"; 8 | import SettingsIconSolid from "../../icons/SettingsIconSolid"; 9 | 10 | const Header = () => { 11 | const drawerOpen = useAppSelector((state) => state.leftSection.drawOpen); 12 | const { isSettings } = useUtils(); 13 | const dispatch = useAppDispatch(); 14 | const navigate = useNavigate(); 15 | 16 | const openDrawerClick = () => { 17 | dispatch(openDrawer()); 18 | }; 19 | 20 | const closeDrawerClick = () => { 21 | dispatch(closeDrawer()); 22 | }; 23 | 24 | return ( 25 | 72 | ); 73 | }; 74 | 75 | export default Header; 76 | -------------------------------------------------------------------------------- /src/components/Homepage/Homepage.tsx: -------------------------------------------------------------------------------- 1 | import Header from "../Header/Header"; 2 | import MainSection from "../MainSection/MainSection"; 3 | import Uploader from "../Uploader/Uploader"; 4 | import { useAppSelector } from "../../hooks/store"; 5 | import { ToastContainer } from "react-toastify"; 6 | 7 | const Homepage = () => { 8 | const showUploader = useAppSelector( 9 | (state) => state.uploader.uploads.length !== 0 10 | ); 11 | 12 | return ( 13 |
14 |
15 |
16 |
17 | 18 | {showUploader && } 19 |
20 |
21 | 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default Homepage; 28 | -------------------------------------------------------------------------------- /src/components/MainSection/MainSection.tsx: -------------------------------------------------------------------------------- 1 | import DataForm from "../Dataform/Dataform"; 2 | import RightSection from "../RightSection/RightSection"; 3 | import { memo, useRef } from "react"; 4 | import LeftSection from "../LeftSection/LeftSection"; 5 | import { useUtils } from "../../hooks/utils"; 6 | import Medias from "../Medias/Medias"; 7 | import { useAppSelector } from "../../hooks/store"; 8 | import PhotoViewerPopup from "../PhotoViewerPopup/PhotoViewerPopup"; 9 | import FileInfoPopup from "../FileInfoPopup/FileInfoPopup"; 10 | import SharePopup from "../SharePopup/SharePopup"; 11 | import MoverPopup from "../MoverPopup/MoverPopup"; 12 | 13 | const MainSection = memo(() => { 14 | const popupModalItem = useAppSelector( 15 | (state) => state.selected.popupModal.file 16 | ); 17 | const shareModalItem = useAppSelector( 18 | (state) => state.selected.shareModal.file 19 | ); 20 | const moveModalItemType = useAppSelector( 21 | (state) => state.selected.moveModal.type 22 | ); 23 | const scrollDivRef = useRef(null); 24 | 25 | const isMediaSelected = 26 | popupModalItem?.metadata.isVideo || popupModalItem?.metadata.hasThumbnail; 27 | const isFileInfoSelected = !isMediaSelected && popupModalItem; 28 | 29 | const { isMedia } = useUtils(); 30 | return ( 31 |
32 |
33 | {isMediaSelected && ( 34 | 35 | )} 36 | 37 | {isFileInfoSelected && } 38 | 39 | {shareModalItem && } 40 | 41 | {moveModalItemType && } 42 | 43 |
44 | 45 | 46 | {!isMedia ? ( 47 | 48 | ) : ( 49 | 50 | )} 51 | 52 | 53 |
54 |
55 |
56 | ); 57 | }); 58 | 59 | export default MainSection; 60 | -------------------------------------------------------------------------------- /src/components/QuickAccess/QuickAccess.tsx: -------------------------------------------------------------------------------- 1 | import QuickAccessItem from "../QuickAccessItem/QuickAccessItem"; 2 | import { memo, useState } from "react"; 3 | import { useQuickFiles } from "../../hooks/files"; 4 | import classNames from "classnames"; 5 | import { useUtils } from "../../hooks/utils"; 6 | import ChevronOutline from "../../icons/ChevronOutline"; 7 | 8 | const QuickAccess = memo(() => { 9 | const { data: quickfilesList } = useQuickFiles(false); 10 | const [quickAccessExpanded, setQuickAccessExpanded] = useState(false); 11 | const { isHome } = useUtils(); 12 | 13 | return ( 14 |
18 |
19 |

Quick Access

20 | setQuickAccessExpanded(!quickAccessExpanded)} 22 | className={classNames( 23 | "cursor-pointer animate-movement text-primary", 24 | { 25 | "rotate-180": quickAccessExpanded, 26 | } 27 | )} 28 | /> 29 |
30 | 31 |
44 | {quickfilesList?.map((file) => ( 45 | 46 | ))} 47 |
48 |
49 | ); 50 | }); 51 | 52 | export default QuickAccess; 53 | -------------------------------------------------------------------------------- /src/components/Spinner/Spinner.tsx: -------------------------------------------------------------------------------- 1 | export default () =>
; 2 | -------------------------------------------------------------------------------- /src/components/UploadItem/UploadItem.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import React, { memo } from "react"; 3 | import { getCancelToken } from "../../utils/cancelTokenManager"; 4 | import CloseIcon from "../../icons/CloseIcon"; 5 | import CheckCircleIcon from "../../icons/CheckCircleIcon"; 6 | import AlertIcon from "../../icons/AlertIcon"; 7 | import { UploadItemType } from "../../reducers/uploader"; 8 | 9 | const UploadItem: React.FC = (props) => { 10 | const { completed, canceled, progress, name, id, type } = props; 11 | const cancelToken = getCancelToken(id); 12 | 13 | const cancelUpload = () => { 14 | cancelToken.cancel(); 15 | }; 16 | 17 | const ProgressIcon = memo(() => { 18 | if (completed) { 19 | return ; 20 | } else if (canceled) { 21 | return ; 22 | } else { 23 | return ( 24 | 28 | ); 29 | } 30 | }); 31 | 32 | const ProgressBar = memo(() => { 33 | if (completed) { 34 | return
; 35 | } else if (canceled) { 36 | return
; 37 | } else if (type === "file") { 38 | return ( 39 | 40 | ); 41 | } else { 42 | return ; 43 | } 44 | }); 45 | 46 | return ( 47 |
48 |
49 |
50 |
51 |

52 | {name} 53 |

54 |
55 |
56 | 57 |
58 |
59 |
60 | 61 |
62 |
63 |
64 | ); 65 | }; 66 | 67 | export default connect()(UploadItem); 68 | -------------------------------------------------------------------------------- /src/components/Uploader/Uploader.tsx: -------------------------------------------------------------------------------- 1 | import { useAppDispatch, useAppSelector } from "../../hooks/store"; 2 | import CloseIcon from "../../icons/CloseIcon"; 3 | import MinimizeIcon from "../../icons/MinimizeIcon"; 4 | import { resetUploads } from "../../reducers/uploader"; 5 | import { cancelAllFileUploads } from "../../utils/cancelTokenManager"; 6 | import UploadItem from "../UploadItem/UploadItem"; 7 | import { memo, useMemo, useState } from "react"; 8 | 9 | const Uploader = memo(() => { 10 | const [minimized, setMinimized] = useState(false); 11 | const uploads = useAppSelector((state) => state.uploader.uploads); 12 | const dispatch = useAppDispatch(); 13 | 14 | const toggleMinimize = () => { 15 | setMinimized((val) => !val); 16 | }; 17 | 18 | const uploadTitle = useMemo(() => { 19 | const uploadedCount = uploads.filter((upload) => upload.completed).length; 20 | const currentlyUploadingCount = uploads.filter( 21 | (upload) => !upload.completed 22 | ).length; 23 | 24 | if (currentlyUploadingCount) { 25 | return `Uploading ${currentlyUploadingCount} file${ 26 | currentlyUploadingCount > 1 ? "s" : "" 27 | }`; 28 | } else { 29 | return `Uploaded ${uploadedCount} file${uploadedCount > 1 ? "s" : ""}`; 30 | } 31 | }, [uploads]); 32 | 33 | const closeUploader = () => { 34 | cancelAllFileUploads(); 35 | dispatch(resetUploads()); 36 | }; 37 | 38 | return ( 39 |
40 |
41 |

{uploadTitle}

42 | 50 |
51 |
52 | {!minimized && 53 | uploads.map((upload) => { 54 | return ; 55 | })} 56 |
57 |
58 | ); 59 | }); 60 | 61 | export default Uploader; 62 | -------------------------------------------------------------------------------- /src/components/VerifyEmailPage/VerifyEmailPage.tsx: -------------------------------------------------------------------------------- 1 | import { useParams } from "react-router-dom"; 2 | import { verifyEmailAPI } from "../../api/userAPI"; 3 | import { toast, ToastContainer } from "react-toastify"; 4 | import { useEffect } from "react"; 5 | 6 | const VerifyEmailPage = () => { 7 | const token = useParams().token!; 8 | 9 | const verifyEmail = async () => { 10 | try { 11 | await toast.promise(verifyEmailAPI(token), { 12 | pending: "Verifying email...", 13 | success: "Email Verified", 14 | error: "Error verifying email", 15 | }); 16 | 17 | setTimeout(() => { 18 | window.location.assign("/"); 19 | }, 1500); 20 | } catch (e) { 21 | console.log("Error verifying email", e); 22 | } 23 | }; 24 | 25 | useEffect(() => { 26 | verifyEmail(); 27 | }, []); 28 | 29 | return ( 30 |
31 |

Verifying email...

32 | 33 |
34 | ); 35 | }; 36 | 37 | export default VerifyEmailPage; 38 | -------------------------------------------------------------------------------- /src/config/.env.development.example: -------------------------------------------------------------------------------- 1 | # Optional, useful for development or if your BE is on a different domain 2 | # Example: http://localhost:5173/api 3 | VITE_BACKEND_URL= 4 | 5 | # Optional, this one is only used in development 6 | # /api will be appended to this URL, so for example 7 | # Here our BE is on localhost:3000, and we are telling 8 | # Vite to proxy all requests to /api to our BE 9 | # Proxy, and URL will become http://localhost:5173/api in this case. 10 | # Example: http://localhost:3000 11 | VITE_PROXY_URL= -------------------------------------------------------------------------------- /src/config/.env.production.example: -------------------------------------------------------------------------------- 1 | # NOTE: You most likely do not need these unless you running in development mode. 2 | 3 | # Either remove the .example from the end of this filename. 4 | # Or create a new file with the same name, but without the .example extension. 5 | 6 | # Optional, useful for development or if your BE is on a different domain 7 | # Example: http://localhost:5173/api 8 | VITE_BACKEND_URL= 9 | 10 | # Optional, this one is only used in development 11 | # /api will be appended to this URL, so for example 12 | # Here our BE is on localhost:3000, and we are telling 13 | # Vite to proxy all requests to /api to our BE 14 | # Proxy, and URL will become http://localhost:5173/api in this case. 15 | # Example: http://localhost:3000 16 | VITE_PROXY_URL= -------------------------------------------------------------------------------- /src/enviroment/envFrontEnd.js: -------------------------------------------------------------------------------- 1 | const env = { 2 | port: 5173, 3 | url: "http://localhost", 4 | // enableVideoTranscoding: process.env.ENABLE_VIDEO_TRANSCODING, 5 | // disableStorage: process.env.DISABLE_STORAGE, 6 | googleDriveEnabled: false, 7 | s3Enabled: false, 8 | activeSubscription: false, 9 | // commercialMode: process.env.COMMERCIAL_MODE, 10 | uploadMode: "", 11 | emailAddress: "", 12 | name: "", 13 | }; 14 | 15 | export default env; 16 | -------------------------------------------------------------------------------- /src/hooks/contextMenu.ts: -------------------------------------------------------------------------------- 1 | import { MouseEventHandler, useRef, useState } from "react"; 2 | 3 | export const useContextMenu = () => { 4 | const [contextData, setContextData] = useState({ 5 | selected: false, 6 | X: 0, 7 | Y: 0, 8 | }); 9 | const lastTouched = useRef(0); 10 | const timeoutRef = useRef(null); 11 | 12 | const onContextMenu = (e: any) => { 13 | if (e) e.stopPropagation(); 14 | if (e) e.preventDefault(); 15 | 16 | let X = e.clientX; 17 | let Y = e.clientY; 18 | 19 | setContextData({ 20 | ...contextData, 21 | selected: true, 22 | X, 23 | Y, 24 | }); 25 | }; 26 | 27 | const closeContextMenu = () => { 28 | setContextData({ 29 | ...contextData, 30 | selected: false, 31 | X: 0, 32 | Y: 0, 33 | }); 34 | }; 35 | 36 | const onTouchStart = (e: any) => { 37 | const touches = e.touches[0]; 38 | let X = e.clientX || touches.clientX; 39 | let Y = e.clientY || touches.clientY; 40 | 41 | if (contextData.selected) return; 42 | 43 | timeoutRef.current = setTimeout(() => { 44 | console.log("timeout"); 45 | setContextData({ 46 | ...contextData, 47 | selected: true, 48 | X, 49 | Y, 50 | }); 51 | }, 500); 52 | }; 53 | 54 | const onTouchMove = () => { 55 | if (timeoutRef.current) { 56 | clearTimeout(timeoutRef.current); 57 | } 58 | }; 59 | 60 | const onTouchEnd = () => { 61 | if (timeoutRef.current) { 62 | clearTimeout(timeoutRef.current); 63 | } 64 | }; 65 | 66 | const clickStopPropagation = (e: React.MouseEvent) => { 67 | e.stopPropagation(); 68 | }; 69 | 70 | return { 71 | ...contextData, 72 | onContextMenu, 73 | closeContextMenu, 74 | onTouchStart, 75 | onTouchMove, 76 | onTouchEnd, 77 | clickStopPropagation, 78 | }; 79 | }; 80 | -------------------------------------------------------------------------------- /src/hooks/folders.ts: -------------------------------------------------------------------------------- 1 | import { UseQueryResult, useQuery, useQueryClient } from "react-query"; 2 | import { useParams } from "react-router-dom"; 3 | import { 4 | getFolderInfoAPI, 5 | getFoldersListAPI, 6 | getMoveFolderListAPI, 7 | } from "../api/foldersAPI"; 8 | import { useUtils } from "./utils"; 9 | import { FolderInterface } from "../types/folders"; 10 | import { useAppSelector } from "./store"; 11 | 12 | export const useFolders = (enabled = true) => { 13 | const params = useParams(); 14 | const sortBy = useAppSelector((state) => state.filter.sortBy); 15 | const { isTrash } = useUtils(); 16 | const foldersReactQuery: UseQueryResult = useQuery( 17 | [ 18 | "folders", 19 | { 20 | parent: params.id || "/", 21 | search: params.query || "", 22 | sortBy, 23 | limit: undefined, 24 | trashMode: isTrash, 25 | }, 26 | ], 27 | getFoldersListAPI, 28 | { enabled, refetchOnWindowFocus: false, refetchOnReconnect: false } 29 | ); 30 | 31 | return { ...foldersReactQuery }; 32 | }; 33 | 34 | export const useFolder = (enabled = true) => { 35 | const params = useParams(); 36 | const folderQuery = useQuery( 37 | [ 38 | "folder", 39 | { 40 | id: params.id, 41 | }, 42 | ], 43 | getFolderInfoAPI, 44 | { enabled } 45 | ); 46 | 47 | return { ...folderQuery }; 48 | }; 49 | 50 | export const useMoveFolders = ( 51 | parent: string, 52 | search: string, 53 | folderIDs?: string[] 54 | ) => { 55 | const params = useParams(); 56 | const moveFoldersQuery = useQuery( 57 | [ 58 | "move-folder-list", 59 | { 60 | parent, 61 | search, 62 | folderIDs, 63 | currentParent: params.id || "/", 64 | }, 65 | ], 66 | getMoveFolderListAPI 67 | ); 68 | 69 | return { ...moveFoldersQuery }; 70 | }; 71 | -------------------------------------------------------------------------------- /src/hooks/infiniteScroll.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef, useState } from "react"; 2 | 3 | // TODO: Fix anys 4 | export const useInfiniteScroll = () => { 5 | const [reachedIntersect, setReachedIntersect] = useState(false); 6 | const observer = useRef() as any; 7 | const sentinelRef = useRef(); 8 | 9 | const handleObserver = useCallback( 10 | (entries: any) => { 11 | const target = entries[0]; 12 | if (target.isIntersecting) { 13 | setReachedIntersect(true); 14 | } else if (reachedIntersect) { 15 | setReachedIntersect(false); 16 | } 17 | }, 18 | [reachedIntersect] 19 | ); 20 | 21 | useEffect(() => { 22 | if (observer.current) { 23 | observer.current.disconnect(); 24 | } 25 | 26 | observer.current = new IntersectionObserver(handleObserver, { 27 | root: null, 28 | rootMargin: undefined, 29 | threshold: 0.1, 30 | }); 31 | if (sentinelRef.current) { 32 | observer.current.observe(sentinelRef.current); 33 | } 34 | 35 | return () => { 36 | if (sentinelRef.current) { 37 | observer.current.disconnect(); 38 | } 39 | }; 40 | }, [handleObserver]); 41 | 42 | return { reachedIntersect, sentinelRef, observer }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/hooks/preferenceSetter.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from "react"; 2 | import { useAppDispatch } from "./store"; 3 | import { setSortBy } from "../reducers/filter"; 4 | import { 5 | setSingleClickFolders, 6 | setListView, 7 | setLoadThumbnailsDisabled, 8 | } from "../reducers/general"; 9 | 10 | export const usePreferenceSetter = () => { 11 | const dispatch = useAppDispatch(); 12 | 13 | const setPreferences = useCallback(() => { 14 | const listModeLocalStorage = window.localStorage.getItem("list-mode"); 15 | const listModeEnabled = listModeLocalStorage === "true"; 16 | 17 | const sortByLocalStorage = window.localStorage.getItem("sort-name"); 18 | const sortByNameEnabled = sortByLocalStorage === "true"; 19 | 20 | const orderByLocalStorage = window.localStorage.getItem("order-asc"); 21 | const orderByAscendingEnabled = orderByLocalStorage === "true"; 22 | 23 | const singleClickFoldersLocalStorage = window.localStorage.getItem( 24 | "single-click-folders" 25 | ); 26 | const singleClickFoldersEnabled = singleClickFoldersLocalStorage === "true"; 27 | 28 | const loadThumbnailsLocalStorage = window.localStorage.getItem( 29 | "not-load-thumbnails" 30 | ); 31 | const loadThumbnailsDisabled = loadThumbnailsLocalStorage === "true"; 32 | 33 | let sortBy = ""; 34 | 35 | if (sortByNameEnabled) { 36 | sortBy = "alp_"; 37 | } else { 38 | sortBy = "date_"; 39 | } 40 | 41 | if (orderByAscendingEnabled) { 42 | sortBy += "asc"; 43 | } else { 44 | sortBy += "desc"; 45 | } 46 | 47 | dispatch(setListView(listModeEnabled)); 48 | dispatch(setSortBy(sortBy)); 49 | dispatch(setLoadThumbnailsDisabled(loadThumbnailsDisabled)); 50 | dispatch(setSingleClickFolders(singleClickFoldersEnabled)); 51 | }, []); 52 | 53 | useEffect(() => { 54 | setPreferences(); 55 | }, [setPreferences]); 56 | 57 | return { setPreferences }; 58 | }; 59 | -------------------------------------------------------------------------------- /src/hooks/store.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from "react-redux"; 2 | import { AppDispatch, RootState } from "../store/configureStore"; 3 | 4 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 5 | export const useAppDispatch = useDispatch.withTypes(); 6 | export const useAppSelector = useSelector.withTypes(); 7 | -------------------------------------------------------------------------------- /src/hooks/user.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from "react"; 2 | import { getAccessToken } from "../api/userAPI"; 3 | import uuid from "uuid"; 4 | 5 | const useAccessTokenHandler = () => { 6 | const refreshAccessToken = useCallback(async () => { 7 | try { 8 | const browserID = localStorage.getItem("browser-id") || uuid.v4(); 9 | if (!localStorage.getItem("browser-id")) { 10 | localStorage.setItem("browser-id", browserID); 11 | } 12 | await getAccessToken(browserID); 13 | } catch (e) { 14 | console.log("Error refreshing access token", e); 15 | } 16 | }, []); 17 | 18 | const visibilityChange = useCallback(() => { 19 | if (document.visibilityState === "visible") { 20 | refreshAccessToken(); 21 | } 22 | }, [refreshAccessToken]); 23 | 24 | useEffect(() => { 25 | refreshAccessToken(); 26 | 27 | const timer = setInterval(refreshAccessToken, 60 * 1000 * 20); 28 | document.addEventListener("visibilitychange", visibilityChange); 29 | 30 | return () => { 31 | clearInterval(timer); 32 | document.removeEventListener("visibilitychange", visibilityChange); 33 | }; 34 | }, [refreshAccessToken, visibilityChange]); 35 | }; 36 | 37 | export default useAccessTokenHandler; 38 | -------------------------------------------------------------------------------- /src/icons/AccountIcon.tsx: -------------------------------------------------------------------------------- 1 | type AccountIconType = React.SVGAttributes; 2 | 3 | const AccountIcon: React.FC = (props) => { 4 | return ( 5 | 6 | account-box 7 | 11 | 12 | ); 13 | }; 14 | 15 | export default AccountIcon; 16 | -------------------------------------------------------------------------------- /src/icons/ActionsIcon.tsx: -------------------------------------------------------------------------------- 1 | type ActionsIconType = React.SVGAttributes; 2 | 3 | const ActionsIcon: React.FC = (props) => { 4 | return ( 5 | 21 | ); 22 | }; 23 | 24 | export default ActionsIcon; 25 | -------------------------------------------------------------------------------- /src/icons/AlertIcon.tsx: -------------------------------------------------------------------------------- 1 | type AlertIconType = React.SVGAttributes; 2 | 3 | const AlertIcon: React.FC = (props) => { 4 | return ( 5 | 6 | alert-circle 7 | 11 | 12 | ); 13 | }; 14 | 15 | export default AlertIcon; 16 | -------------------------------------------------------------------------------- /src/icons/ArrowBackIcon.tsx: -------------------------------------------------------------------------------- 1 | type ArrowBackIconType = React.SVGAttributes; 2 | 3 | const ArrowBackIcon: React.FC = (props) => { 4 | return ( 5 | 6 | arrow-left 7 | 11 | 12 | ); 13 | }; 14 | 15 | export default ArrowBackIcon; 16 | -------------------------------------------------------------------------------- /src/icons/CalendarIcon.tsx: -------------------------------------------------------------------------------- 1 | type CalendarIconType = React.SVGAttributes; 2 | 3 | const CalendarIcon: React.FC = (props) => { 4 | return ( 5 | 11 | calendar-range 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default CalendarIcon; 18 | -------------------------------------------------------------------------------- /src/icons/CheckCircleIcon.tsx: -------------------------------------------------------------------------------- 1 | type CheckCircleIconType = React.SVGAttributes; 2 | 3 | const CheckCircleIcon: React.FC = (props) => { 4 | return ( 5 | 6 | check-circle 7 | 11 | 12 | ); 13 | }; 14 | 15 | export default CheckCircleIcon; 16 | -------------------------------------------------------------------------------- /src/icons/ChevronOutline.tsx: -------------------------------------------------------------------------------- 1 | type ChevronOutlineType = React.SVGAttributes; 2 | 3 | const ChevronOutline: React.FC = (props) => { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | }; 19 | 20 | export default ChevronOutline; 21 | -------------------------------------------------------------------------------- /src/icons/ChevronSolid.tsx: -------------------------------------------------------------------------------- 1 | type ChevronSolidType = React.SVGAttributes; 2 | 3 | const ChevronSolid: React.FC = (props) => { 4 | return ( 5 | 13 | 18 | 19 | ); 20 | }; 21 | 22 | export default ChevronSolid; 23 | -------------------------------------------------------------------------------- /src/icons/CircleLeftIcon.tsx: -------------------------------------------------------------------------------- 1 | type CircleLeftIconType = React.SVGAttributes; 2 | 3 | const CircleLeftIcon: React.FC = (props) => { 4 | return ( 5 | 6 | arrow-left-circle-outline 7 | 11 | 12 | ); 13 | }; 14 | 15 | export default CircleLeftIcon; 16 | -------------------------------------------------------------------------------- /src/icons/CircleRightIcon.tsx: -------------------------------------------------------------------------------- 1 | type CircleRightIconType = React.SVGAttributes; 2 | 3 | const CircleRightIcon: React.FC = (props) => { 4 | return ( 5 | 6 | arrow-right-circle-outline 7 | 11 | 12 | ); 13 | }; 14 | 15 | export default CircleRightIcon; 16 | -------------------------------------------------------------------------------- /src/icons/ClockIcon.tsx: -------------------------------------------------------------------------------- 1 | type ClockIconType = React.SVGAttributes; 2 | 3 | const ClockIcon: React.FC = (props) => { 4 | return ( 5 | 11 | clock-time-eight-outline 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default ClockIcon; 18 | -------------------------------------------------------------------------------- /src/icons/CloseIcon.tsx: -------------------------------------------------------------------------------- 1 | type CloseIconType = React.SVGAttributes; 2 | 3 | const CloseIcon: React.FC = (props) => { 4 | return ( 5 | 6 | window-close 7 | 11 | 12 | ); 13 | }; 14 | 15 | export default CloseIcon; 16 | -------------------------------------------------------------------------------- /src/icons/CreateFolderIcon.tsx: -------------------------------------------------------------------------------- 1 | type CreateFolderIconType = React.SVGAttributes; 2 | 3 | const CreateFolderIcon: React.FC = (props) => { 4 | return ( 5 | 11 | 18 | 19 | ); 20 | }; 21 | 22 | export default CreateFolderIcon; 23 | -------------------------------------------------------------------------------- /src/icons/DownloadIcon.tsx: -------------------------------------------------------------------------------- 1 | type DownloadIconType = React.SVGAttributes; 2 | 3 | const DownloadIcon: React.FC = (props) => { 4 | return ( 5 | 13 | 14 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default DownloadIcon; 27 | -------------------------------------------------------------------------------- /src/icons/FileDetailsIcon.tsx: -------------------------------------------------------------------------------- 1 | type FileDetailsIconType = React.SVGAttributes; 2 | 3 | const FileDetailsIcon: React.FC = (props) => { 4 | return ( 5 | 13 | 14 | 21 | 26 | 31 | 36 | 41 | 48 | 53 | 58 | 63 | 64 | 65 | ); 66 | }; 67 | 68 | export default FileDetailsIcon; 69 | -------------------------------------------------------------------------------- /src/icons/FolderIcon.tsx: -------------------------------------------------------------------------------- 1 | type FolderIconType = React.SVGAttributes; 2 | 3 | const FolderIcon: React.FC = (props) => { 4 | return ( 5 | 6 | 10 | 11 | ); 12 | }; 13 | 14 | export default FolderIcon; 15 | -------------------------------------------------------------------------------- /src/icons/FolderUploadIcon.tsx: -------------------------------------------------------------------------------- 1 | type FolderUploadIconType = React.SVGAttributes; 2 | 3 | const FolderUploadIcon: React.FC = (props) => { 4 | return ( 5 | 6 | folder-upload-outline 7 | 11 | 12 | ); 13 | }; 14 | 15 | export default FolderUploadIcon; 16 | -------------------------------------------------------------------------------- /src/icons/HomeIconOutline.tsx: -------------------------------------------------------------------------------- 1 | type HomeIconOutlineType = React.SVGAttributes; 2 | 3 | const HomeIconOutline: React.FC = (props) => { 4 | return ( 5 | 6 | home-variant-outline 7 | 11 | 12 | ); 13 | }; 14 | 15 | export default HomeIconOutline; 16 | -------------------------------------------------------------------------------- /src/icons/LockIcon.tsx: -------------------------------------------------------------------------------- 1 | type LockIconType = React.SVGAttributes; 2 | 3 | const LockIcon: React.FC = (props) => { 4 | return ( 5 | 11 | lock-outline 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default LockIcon; 18 | -------------------------------------------------------------------------------- /src/icons/MenuIcon.tsx: -------------------------------------------------------------------------------- 1 | type MenuIconType = React.SVGAttributes; 2 | 3 | const MenuIcon: React.FC = (props) => { 4 | return ( 5 | 6 | menu 7 | 11 | 12 | ); 13 | }; 14 | 15 | export default MenuIcon; 16 | -------------------------------------------------------------------------------- /src/icons/MinimizeIcon.tsx: -------------------------------------------------------------------------------- 1 | type MinimizeIconType = React.SVGAttributes; 2 | 3 | const MinimizeIcon: React.FC = (props) => { 4 | return ( 5 | 6 | minus 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default MinimizeIcon; 13 | -------------------------------------------------------------------------------- /src/icons/MoveIcon.tsx: -------------------------------------------------------------------------------- 1 | type MoveIconType = React.SVGAttributes; 2 | 3 | const Moveicon: React.FC = (props) => { 4 | return ( 5 | 13 | 20 | 21 | ); 22 | }; 23 | 24 | export default Moveicon; 25 | -------------------------------------------------------------------------------- /src/icons/MultiSelectIcon.tsx: -------------------------------------------------------------------------------- 1 | type MultiSelectIconType = React.SVGAttributes; 2 | 3 | const MultiSelectIcon: React.FC = (props) => { 4 | return ( 5 | 11 | 15 | 16 | ); 17 | }; 18 | 19 | export default MultiSelectIcon; 20 | -------------------------------------------------------------------------------- /src/icons/OneIcon.tsx: -------------------------------------------------------------------------------- 1 | const OneIcon: React.FC> = (props) => { 2 | return ( 3 | 9 | numeric-1-circle-outline 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default OneIcon; 16 | -------------------------------------------------------------------------------- /src/icons/PhotoIcon.tsx: -------------------------------------------------------------------------------- 1 | const PhotoIcon: React.FC> = (props) => { 2 | return ( 3 | 4 | image 5 | 9 | 10 | ); 11 | }; 12 | 13 | export default PhotoIcon; 14 | -------------------------------------------------------------------------------- /src/icons/PlayIcon.tsx: -------------------------------------------------------------------------------- 1 | type PlayIconType = React.SVGAttributes; 2 | 3 | const PlayIcon: React.FC = (props) => { 4 | return ( 5 | 6 | play 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default PlayIcon; 13 | -------------------------------------------------------------------------------- /src/icons/PublicIcon.tsx: -------------------------------------------------------------------------------- 1 | const PublicIcon: React.FC> = (props) => { 2 | return ( 3 | 9 | earth 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default PublicIcon; 16 | -------------------------------------------------------------------------------- /src/icons/RenameIcon.tsx: -------------------------------------------------------------------------------- 1 | type RenameIconType = React.SVGAttributes; 2 | 3 | const RenameIcon: React.FC = (props) => { 4 | return ( 5 | 13 | 20 | 21 | ); 22 | }; 23 | 24 | export default RenameIcon; 25 | -------------------------------------------------------------------------------- /src/icons/RestoreIcon.tsx: -------------------------------------------------------------------------------- 1 | type RestoreIconType = React.SVGAttributes; 2 | 3 | const RestoreIcon: React.FC = (props) => { 4 | return ( 5 | 11 | restore 12 | 16 | 17 | ); 18 | }; 19 | 20 | export default RestoreIcon; 21 | -------------------------------------------------------------------------------- /src/icons/SearchIcon.tsx: -------------------------------------------------------------------------------- 1 | type SearchIconType = React.SVGAttributes; 2 | 3 | const SearchIcon: React.FC = (props) => { 4 | return ( 5 | 12 | 13 | 20 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default SearchIcon; 31 | -------------------------------------------------------------------------------- /src/icons/SettingsIcon.tsx: -------------------------------------------------------------------------------- 1 | type SettingsIconType = React.SVGAttributes; 2 | 3 | const SettingsIcon: React.FC = (props) => { 4 | return ( 5 | 6 | cog-outline 7 | 11 | 12 | ); 13 | }; 14 | 15 | export default SettingsIcon; 16 | -------------------------------------------------------------------------------- /src/icons/SettingsIconSolid.tsx: -------------------------------------------------------------------------------- 1 | type SettingsIconSolidType = React.SVGAttributes; 2 | 3 | const SettingsIconSolid: React.FC = (props) => { 4 | return ( 5 | 13 | 20 | 21 | ); 22 | }; 23 | 24 | export default SettingsIconSolid; 25 | -------------------------------------------------------------------------------- /src/icons/ShareIcon.tsx: -------------------------------------------------------------------------------- 1 | type ShareIconType = React.SVGAttributes; 2 | 3 | const ShareIcon: React.FC = (props) => { 4 | return ( 5 | 13 | 14 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default ShareIcon; 27 | -------------------------------------------------------------------------------- /src/icons/SpacerIcon.tsx: -------------------------------------------------------------------------------- 1 | type SpacerIconType = React.SVGAttributes; 2 | 3 | const SpacerIcon: React.FC = (props) => { 4 | return ( 5 | 13 | 18 | 19 | ); 20 | }; 21 | 22 | export default SpacerIcon; 23 | -------------------------------------------------------------------------------- /src/icons/StorageIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type StorageIconType = React.SVGAttributes; 4 | 5 | const StorageIcon: React.FC = (props) => { 6 | return ( 7 | 13 | database-outline 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default StorageIcon; 20 | -------------------------------------------------------------------------------- /src/icons/TrashIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type TrashIconType = React.SVGAttributes; 4 | 5 | const TrashIcon: React.FC = (props) => { 6 | return ( 7 | 8 | trash-can-outline 9 | 13 | 14 | ); 15 | }; 16 | 17 | export default TrashIcon; 18 | -------------------------------------------------------------------------------- /src/icons/TuneIcon.tsx: -------------------------------------------------------------------------------- 1 | type TuneIconType = React.SVGAttributes; 2 | 3 | const TuneIcon: React.FC = (props) => { 4 | return ( 5 | 6 | tune 7 | 11 | 12 | ); 13 | }; 14 | 15 | export default TuneIcon; 16 | -------------------------------------------------------------------------------- /src/icons/UploadFileIcon.tsx: -------------------------------------------------------------------------------- 1 | type UploadFileIconType = React.SVGAttributes; 2 | 3 | const UploadFileIcon: React.FC = (props) => { 4 | return ( 5 | 6 | 13 | 14 | ); 15 | }; 16 | 17 | export default UploadFileIcon; 18 | -------------------------------------------------------------------------------- /src/popups/folder.ts: -------------------------------------------------------------------------------- 1 | import Swal from "sweetalert2"; 2 | 3 | export const renameFolderPopup = async (folderName: string) => { 4 | const result = await Swal.fire({ 5 | title: "Enter A folder Name", 6 | input: "text", 7 | inputValue: folderName, 8 | showCancelButton: true, 9 | inputValidator: (value) => { 10 | if (!value) { 11 | return "Please Enter a Name"; 12 | } 13 | }, 14 | }); 15 | return result.value; 16 | }; 17 | 18 | export const deleteFolderPopup = async () => { 19 | const result = await Swal.fire({ 20 | title: "Delete folder?", 21 | text: "You will not be able to recover this folder.", 22 | icon: "warning", 23 | showCancelButton: true, 24 | confirmButtonColor: "#3085d6", 25 | cancelButtonColor: "#d33", 26 | confirmButtonText: "Yes", 27 | }); 28 | return result.value; 29 | }; 30 | 31 | export const showCreateFolderPopup = async (defaultName = "") => { 32 | const { value: folderName } = await Swal.fire({ 33 | title: "Enter Folder Name", 34 | input: "text", 35 | inputValue: defaultName, 36 | showCancelButton: true, 37 | inputValidator: (value) => { 38 | if (!value) { 39 | return "Please Enter a Name"; 40 | } 41 | }, 42 | }); 43 | return folderName; 44 | }; 45 | -------------------------------------------------------------------------------- /src/popups/user.ts: -------------------------------------------------------------------------------- 1 | import Swal from "sweetalert2"; 2 | 3 | export const emailVerificationSentPopup = async () => { 4 | const result = await Swal.fire({ 5 | title: "Email verification sent", 6 | icon: "success", 7 | confirmButtonColor: "#3085d6", 8 | confirmButtonText: "Okay", 9 | }); 10 | return result.value; 11 | }; 12 | -------------------------------------------------------------------------------- /src/providers/AuthProvider.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState, useContext, useEffect } from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { useLocation, useNavigate } from "react-router-dom"; 4 | 5 | const AuthContext = createContext(); 6 | 7 | export const AuthProvider = ({ children }) => { 8 | const authenticated = useSelector((state) => !!state.auth.id); 9 | const location = useLocation(); 10 | const navigate = useNavigate(); 11 | console.log("isAuthenticated", isAuthenticated); 12 | if (!authenticated && location.pathname !== "/") { 13 | navigate("/"); 14 | } 15 | 16 | return {children}; 17 | }; 18 | 19 | export const useAuth = () => useContext(AuthContext); 20 | -------------------------------------------------------------------------------- /src/reducers/filter.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | sortBy: "date_desc", 5 | limit: 50, 6 | search: "", 7 | mediaFilter: "all", 8 | }; 9 | 10 | const filterSlice = createSlice({ 11 | name: "selected", 12 | initialState, 13 | reducers: { 14 | setSortBy: (state, action: PayloadAction) => { 15 | state.sortBy = action.payload; 16 | }, 17 | setMediaFilter: (state, action: PayloadAction) => { 18 | state.mediaFilter = action.payload; 19 | }, 20 | }, 21 | }); 22 | 23 | export const { setSortBy, setMediaFilter } = filterSlice.actions; 24 | 25 | export default filterSlice.reducer; 26 | -------------------------------------------------------------------------------- /src/reducers/general.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | listView: false, 5 | loadThumbnailsDisabled: false, 6 | singleClickFolders: false, 7 | }; 8 | 9 | const generalSlice = createSlice({ 10 | name: "general", 11 | initialState, 12 | reducers: { 13 | toggleListView: (state) => { 14 | state.listView = !state.listView; 15 | }, 16 | setListView: (state, action: PayloadAction) => { 17 | state.listView = action.payload; 18 | }, 19 | setLoadThumbnailsDisabled: (state, action: PayloadAction) => { 20 | state.loadThumbnailsDisabled = action.payload; 21 | }, 22 | setSingleClickFolders: (state, action: PayloadAction) => { 23 | state.singleClickFolders = action.payload; 24 | }, 25 | }, 26 | }); 27 | 28 | export const { 29 | toggleListView, 30 | setListView, 31 | setLoadThumbnailsDisabled, 32 | setSingleClickFolders, 33 | } = generalSlice.actions; 34 | 35 | export default generalSlice.reducer; 36 | -------------------------------------------------------------------------------- /src/reducers/leftSection.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | drawOpen: false, 5 | }; 6 | 7 | const leftSectionSlice = createSlice({ 8 | name: "selected", 9 | initialState, 10 | reducers: { 11 | toggleDrawer: (state) => { 12 | state.drawOpen = !state.drawOpen; 13 | }, 14 | closeDrawer: (state) => { 15 | state.drawOpen = false; 16 | }, 17 | openDrawer: (state) => { 18 | state.drawOpen = true; 19 | }, 20 | }, 21 | }); 22 | 23 | export const { toggleDrawer, closeDrawer, openDrawer } = 24 | leftSectionSlice.actions; 25 | 26 | export default leftSectionSlice.reducer; 27 | -------------------------------------------------------------------------------- /src/reducers/uploader.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | 3 | export interface UploadItemType { 4 | id: string; 5 | progress: number; 6 | name: string; 7 | completed: boolean; 8 | canceled: boolean; 9 | size: number; 10 | type: "file" | "folder"; 11 | } 12 | 13 | interface UploaderStateType { 14 | uploads: UploadItemType[]; 15 | } 16 | 17 | const initialState: UploaderStateType = { 18 | uploads: [], 19 | }; 20 | 21 | const uploaderSlice = createSlice({ 22 | name: "uploader", 23 | initialState, 24 | reducers: { 25 | addUpload(state, action: PayloadAction) { 26 | state.uploads.unshift(action.payload); 27 | }, 28 | editUpload( 29 | state, 30 | action: PayloadAction<{ 31 | id: string; 32 | updateData: { 33 | progress?: number; 34 | completed?: boolean; 35 | canceled?: boolean; 36 | }; 37 | }> 38 | ) { 39 | const uploads = state.uploads.map((upload) => { 40 | if (upload.id === action.payload.id) { 41 | return { 42 | ...upload, 43 | ...action.payload.updateData, 44 | }; 45 | } else { 46 | return upload; 47 | } 48 | }); 49 | 50 | state.uploads = uploads; 51 | }, 52 | resetUploads(state) { 53 | state.uploads = []; 54 | }, 55 | }, 56 | }); 57 | 58 | export const { addUpload, editUpload, resetUploads } = uploaderSlice.actions; 59 | 60 | export default uploaderSlice.reducer; 61 | -------------------------------------------------------------------------------- /src/reducers/user.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import { UserType } from "../types/user"; 3 | 4 | interface UserStateType { 5 | user?: null | UserType; 6 | loggedIn: boolean; 7 | lastRefreshed: number; 8 | } 9 | 10 | const initialState: UserStateType = { 11 | user: null, 12 | loggedIn: false, 13 | lastRefreshed: 0, 14 | }; 15 | 16 | const userSlice = createSlice({ 17 | name: "selected", 18 | initialState, 19 | reducers: { 20 | setUser: (state, action: PayloadAction) => { 21 | state.user = action.payload; 22 | state.loggedIn = true; 23 | }, 24 | setLastRefreshed: (state) => { 25 | state.lastRefreshed = Date.now(); 26 | }, 27 | }, 28 | }); 29 | 30 | export const { setUser, setLastRefreshed } = userSlice.actions; 31 | 32 | export default userSlice.reducer; 33 | -------------------------------------------------------------------------------- /src/routers/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect, useSelector } from "react-redux"; 3 | import { Route, Navigate, useLocation } from "react-router-dom"; 4 | import { useAppSelector } from "../hooks/store"; 5 | 6 | const PrivateRoute = ({ children }) => { 7 | const isAuthenticated = useAppSelector((state) => state.user.loggedIn); 8 | console.log("isAuthenticated", isAuthenticated); 9 | const location = useLocation(); 10 | if (!isAuthenticated) { 11 | return ; 12 | } 13 | 14 | return children; 15 | }; 16 | 17 | export default PrivateRoute; 18 | -------------------------------------------------------------------------------- /src/routers/PublicRoute.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import React from "react"; 3 | import { Route, Navigate } from "react-router-dom"; 4 | 5 | export const PublicRoute = ( 6 | { isAuthenticated, component: Component }, 7 | ...rest 8 | ) => ( 9 | 12 | isAuthenticated ? : 13 | } 14 | /> 15 | ); 16 | 17 | const connectStateToProps = (state) => ({ 18 | isAuthenticated: !!state.auth.id, 19 | }); 20 | 21 | export default connect(connectStateToProps)(PublicRoute); 22 | -------------------------------------------------------------------------------- /src/store/configureStore.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import filterReducer from "../reducers/filter"; 3 | import selectedReducer from "../reducers/selected"; 4 | import leftSectionReducer from "../reducers/leftSection"; 5 | import userReducer from "../reducers/user"; 6 | import uploaderReducer from "../reducers/uploader"; 7 | import generalReducer from "../reducers/general"; 8 | 9 | const store = configureStore({ 10 | reducer: { 11 | general: generalReducer, 12 | filter: filterReducer, 13 | selected: selectedReducer, 14 | leftSection: leftSectionReducer, 15 | user: userReducer, 16 | uploader: uploaderReducer, 17 | }, 18 | }); 19 | 20 | export type RootState = ReturnType; 21 | export type AppDispatch = typeof store.dispatch; 22 | 23 | export default store; 24 | -------------------------------------------------------------------------------- /src/styles/base/_base.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html { 6 | } 7 | 8 | input[type="text"], 9 | input[type="email"], 10 | input[type="password"], 11 | textarea { 12 | -webkit-appearance: none; 13 | } 14 | 15 | body { 16 | height: 100dvh; 17 | width: 100vw; 18 | overflow: hidden; 19 | } 20 | 21 | button { 22 | cursor: pointer; 23 | } 24 | 25 | button:disabled { 26 | cursor: default; 27 | } 28 | body, 29 | html { 30 | margin: 0px; 31 | padding: 0px; 32 | & input[type="text"], 33 | & input[type="submit"] { 34 | outline: none; 35 | } 36 | } 37 | body { 38 | & * { 39 | box-sizing: border-box; 40 | font-family: sans-serif; 41 | } 42 | } 43 | 44 | .dynamic-height { 45 | height: 100dvh; 46 | } 47 | -------------------------------------------------------------------------------- /src/styles/components/_Swal.scss: -------------------------------------------------------------------------------- 1 | .swal2-popup { 2 | font-size: 10px !important; 3 | 4 | @media (max-width: 480px) { 5 | font-size: 13px !important; 6 | } 7 | } 8 | 9 | .swal2-confirm { 10 | background-color: #3c85ee !important; 11 | } 12 | 13 | .swal2-cancel { 14 | background-color: red !important; 15 | } 16 | -------------------------------------------------------------------------------- /src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import "./base/base"; 6 | @import "./components/Spinner"; 7 | @import "./components/Swal"; 8 | 9 | .animate { 10 | transition: 0.2s ease all; 11 | } 12 | 13 | .animate-easy { 14 | transition: 0.3s ease all; 15 | } 16 | 17 | .animate-movement { 18 | transition: 0.4s ease all; 19 | } 20 | 21 | .animate-medium { 22 | transition: 0.6s ease all; 23 | } 24 | 25 | .animate-very-long { 26 | transition: 6s ease all; 27 | } 28 | 29 | .Toastify__progress-bar--success { 30 | background-color: #3c85ee !important; 31 | } 32 | 33 | .--toastify-color-transparent .Toastify__toast-icon { 34 | background-color: #3c85ee !important; 35 | } 36 | 37 | .Toastify__toast--success .Toastify__icon svg path { 38 | fill: #3c85ee !important; 39 | } 40 | 41 | :root { 42 | --toastify-icon-color-success: #3c85ee !important; 43 | } 44 | 45 | .disable-force-touch { 46 | -webkit-touch-callout: none; 47 | } 48 | -------------------------------------------------------------------------------- /src/types/file.ts: -------------------------------------------------------------------------------- 1 | export interface FileInterface { 2 | _id: string; 3 | length: number; 4 | chunkSize: number; 5 | uploadDate: string; 6 | filename: string; 7 | metadata: { 8 | owner: string; 9 | parent: string; 10 | parentList: string; 11 | hasThumbnail: boolean; 12 | isVideo: boolean; 13 | thumbnailID?: string; 14 | size: number; 15 | IV: Buffer; 16 | linkType?: "one" | "public"; 17 | link?: string; 18 | filePath?: string; 19 | s3ID?: string; 20 | personalFile?: boolean; 21 | trashed?: boolean; 22 | }; 23 | } 24 | interface example { 25 | selectedRightSectionItem: { 26 | folder?: { 27 | id: string; 28 | name: string; 29 | }; 30 | file?: { 31 | id: string; 32 | filename: string; 33 | }; 34 | }; 35 | popupFile: { 36 | id: string; 37 | filename: string; 38 | }; 39 | selectedItem: { 40 | type: string; 41 | id: string; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/types/folders.ts: -------------------------------------------------------------------------------- 1 | export interface FolderInterface { 2 | _id: string; 3 | name: string; 4 | parent: string; 5 | owner: string; 6 | createdAt: Date; 7 | updatedAt: Date; 8 | parentList: string[]; 9 | personalFolder?: boolean; 10 | trashed?: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /src/types/user.ts: -------------------------------------------------------------------------------- 1 | export interface UserType { 2 | name: string; 3 | email: string; 4 | emailVerified?: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/InternalServerError.js: -------------------------------------------------------------------------------- 1 | class InternalServerError extends Error { 2 | 3 | constructor(args) { 4 | super(args); 5 | 6 | this.code = 500; 7 | } 8 | } 9 | 10 | module.exports = InternalServerError; -------------------------------------------------------------------------------- /src/utils/NotAuthorizedError.js: -------------------------------------------------------------------------------- 1 | class NotAuthorizedError extends Error { 2 | 3 | constructor(args) { 4 | super(args); 5 | 6 | this.code = 401; 7 | } 8 | } 9 | 10 | module.exports = NotAuthorizedError; -------------------------------------------------------------------------------- /src/utils/NotFoundError.js: -------------------------------------------------------------------------------- 1 | class NotFoundError extends Error { 2 | 3 | constructor(args) { 4 | super(args); 5 | 6 | this.code = 404; 7 | } 8 | } 9 | 10 | module.exports = NotFoundError; -------------------------------------------------------------------------------- /src/utils/PWAUtils.ts: -------------------------------------------------------------------------------- 1 | export const isPwa = () => { 2 | return ["fullscreen", "standalone", "minimal-ui"].some( 3 | (displayMode) => 4 | window.matchMedia("(display-mode: " + displayMode + ")").matches 5 | ); 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/cancelTokenManager.ts: -------------------------------------------------------------------------------- 1 | interface MapType { 2 | [key: string]: { 3 | token: any; 4 | cancel: () => {}; 5 | }; 6 | } 7 | 8 | const cancelTokens: MapType = {}; 9 | 10 | // TODO: Fix any 11 | export const addFileUploadCancelToken = (id: string, cancelToken: any) => { 12 | cancelTokens[id] = cancelToken; 13 | }; 14 | 15 | export const removeFileUploadCancelToken = (id: string) => { 16 | delete cancelTokens[id]; 17 | }; 18 | 19 | export const getCancelToken = (id: string) => { 20 | return cancelTokens[id]; 21 | }; 22 | 23 | export const cancelAllFileUploads = () => { 24 | for (const key in cancelTokens) { 25 | cancelTokens[key].cancel(); 26 | delete cancelTokens[key]; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/utils/capitalize.ts: -------------------------------------------------------------------------------- 1 | const capitalize = (name: string) => { 2 | if (name.length <= 1) { 3 | return name.toUpperCase(); 4 | } else { 5 | return name.substring(0, 1).toUpperCase() + name.substring(1); 6 | } 7 | }; 8 | 9 | export default capitalize; 10 | -------------------------------------------------------------------------------- /src/utils/convertDriveListToMongoList.js: -------------------------------------------------------------------------------- 1 | import convertDriveToMongo from "./convertDriveToMongo"; 2 | 3 | const convertDriveListToMongoList = (driveObjs, ownerID) => { 4 | 5 | let convertedObjs = []; 6 | 7 | for (let currentObj of driveObjs) { 8 | convertedObjs.push(convertDriveToMongo(currentObj, ownerID)); 9 | } 10 | 11 | return convertedObjs; 12 | } 13 | 14 | export default convertDriveListToMongoList; -------------------------------------------------------------------------------- /src/utils/convertDriveToMongo.js: -------------------------------------------------------------------------------- 1 | const convertDriveToMongo = (driveObj, ownerID) => { 2 | 3 | let convertedObj = {}; 4 | convertedObj._id = driveObj.id; 5 | convertedObj.filename = driveObj.name; 6 | convertedObj.length = driveObj.size; 7 | convertedObj.uploadDate = driveObj.modifiedTime; 8 | convertedObj.metadata = { 9 | IV: "", 10 | hasThumbnail: driveObj.hasThumbnail, 11 | isVideo: false, 12 | owner: ownerID, 13 | parent: driveObj.parents[driveObj.parents.length - 1] === "root" ? "/" : driveObj.parents[driveObj.parents.length - 1], 14 | parentList: driveObj.parents, 15 | size: driveObj.length, 16 | drive: true, 17 | thumbnailID: driveObj.thumbnailLink 18 | } 19 | 20 | return convertedObj; 21 | } 22 | 23 | export default convertDriveToMongo; -------------------------------------------------------------------------------- /src/utils/createError.js: -------------------------------------------------------------------------------- 1 | const createError = (message, code, exception) => { 2 | 3 | let error = new Error(message); 4 | error.message = message 5 | error.code = code 6 | error.exception = exception 7 | 8 | throw error; 9 | 10 | } 11 | 12 | module.exports = createError; -------------------------------------------------------------------------------- /src/utils/createQuery.js: -------------------------------------------------------------------------------- 1 | const createQuery = (owner, parent,sortBy, startAt, startAtDate,searchQuery, startAtName) => { 2 | 3 | let query = {"metadata.owner": owner} 4 | 5 | if (searchQuery !== "") { 6 | 7 | searchQuery = new RegExp(searchQuery, 'i') 8 | 9 | query = {...query, filename: searchQuery} 10 | 11 | } else { 12 | 13 | query = {...query, "metadata.parent": parent} 14 | } 15 | 16 | if (startAt) { 17 | 18 | if (sortBy === "date_desc" || sortBy === "DEFAULT") { 19 | 20 | query = {...query, "uploadDate": {$lt: new Date(startAtDate)}} 21 | 22 | } else if (sortBy === "date_asc") { 23 | 24 | query = {...query, "uploadDate": {$gt: new Date(startAtDate)}} 25 | 26 | } else if (sortBy === "alp_desc") { 27 | 28 | query = {...query, "filename": {$lt: startAtName}} 29 | 30 | } else { 31 | 32 | query = {...query, "filename": {$gt: startAtName}} 33 | } 34 | } 35 | 36 | 37 | return query; 38 | 39 | } 40 | 41 | module.exports = createQuery -------------------------------------------------------------------------------- /src/utils/files.ts: -------------------------------------------------------------------------------- 1 | export const getFileExtension = (filename: string, length = 4) => { 2 | const filenameSplit = filename.split("."); 3 | 4 | if (filenameSplit.length > 1) { 5 | let extension = filenameSplit[filenameSplit.length - 1]; 6 | 7 | if (extension.length > length) 8 | extension = 9 | extension.substring(0, length - 1) + 10 | extension.substring(extension.length - 1, extension.length); 11 | 12 | return extension.toUpperCase(); 13 | } else { 14 | return "UNK"; 15 | } 16 | }; 17 | 18 | type ColorMap = { 19 | [key: string]: string; 20 | }; 21 | 22 | export const getFileColor = (filename: string) => { 23 | const letter = getFileExtension(filename).substring(0, 1).toUpperCase(); 24 | 25 | const colorMap: ColorMap = { 26 | A: "#e53935", 27 | B: "#d81b60", 28 | C: "#8e24aa", 29 | D: "#5e35b1", 30 | E: "#3949ab", 31 | F: "#1e88e5", 32 | G: "#039be5", 33 | H: "#00acc1", 34 | I: "#00897b", 35 | J: "#43a047", 36 | K: "#fdd835", 37 | L: "#ffb300", 38 | M: "#fb8c00", 39 | N: "#f4511e", 40 | O: "#d32f2f", 41 | P: "#c2185b", 42 | Q: "#7b1fa2", 43 | R: "#512da8", 44 | S: "#303f9f", 45 | T: "#1976d2", 46 | U: "#0288d1", 47 | V: "#0097a7", 48 | W: "#0097a7", 49 | X: "#00796b", 50 | Y: "#388e3c", 51 | Z: "#fbc02d", 52 | }; 53 | 54 | if (colorMap[letter]) { 55 | return colorMap[letter]; 56 | } else { 57 | return "#03a9f4"; 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/utils/getBackendURL.ts: -------------------------------------------------------------------------------- 1 | const getBackendURL = () => { 2 | // @ts-ignore 3 | const envURL = import.meta.env.VITE_BACKEND_URL; 4 | 5 | if (envURL) { 6 | return envURL; 7 | } 8 | 9 | const mode = process.env.NODE_ENV; 10 | 11 | if (mode === "development") { 12 | return "http://localhost:5173/api"; 13 | } 14 | 15 | return window.location.origin; 16 | }; 17 | 18 | export default getBackendURL; 19 | -------------------------------------------------------------------------------- /src/utils/imageChecker.js: -------------------------------------------------------------------------------- 1 | const videoExtList = [ 2 | "jpeg", 3 | "jpg", 4 | "png", 5 | "gif", 6 | "svg", 7 | "tiff", 8 | "bmp" 9 | ] 10 | 11 | const videoChecker = (filename) => { 12 | 13 | if (filename.length < 1 || !filename.includes(".")) { 14 | 15 | return false; 16 | } 17 | 18 | const extSplit = filename.split("."); 19 | 20 | if (extSplit < 1) { 21 | 22 | return false; 23 | } 24 | 25 | const ext = extSplit[extSplit.length - 1]; 26 | 27 | return videoExtList.includes(ext.toLowerCase()); 28 | 29 | } 30 | 31 | module.exports = videoChecker; -------------------------------------------------------------------------------- /src/utils/mobileCheck.ts: -------------------------------------------------------------------------------- 1 | const mobilecheck = () => { 2 | var check = false; 3 | (function (a) { 4 | if ( 5 | /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( 6 | a 7 | ) || 8 | /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( 9 | a.substr(0, 4) 10 | ) 11 | ) 12 | check = true; 13 | // @ts-ignore 14 | })(navigator.userAgent || navigator.vendor || window?.opera); 15 | return check; 16 | }; 17 | export default mobilecheck; 18 | -------------------------------------------------------------------------------- /src/utils/reduceQuickItemList.js: -------------------------------------------------------------------------------- 1 | import mobilecheck from "./mobileCheck"; 2 | 3 | const reduceQuickItemList = (quickItemList) => { 4 | const isMobile = mobilecheck(); 5 | 6 | if (quickItemList.length > 10 && !isMobile) { 7 | quickItemList = quickItemList.slice(0, 10); 8 | } else if (quickItemList.length > 2 && isMobile) { 9 | quickItemList = quickItemList.slice(0, 2); 10 | } 11 | 12 | return quickItemList; 13 | }; 14 | 15 | export default reduceQuickItemList; 16 | -------------------------------------------------------------------------------- /src/utils/sortBySwitch.js: -------------------------------------------------------------------------------- 1 | const sortBySwitch = (sortBy) => { 2 | switch(sortBy) { 3 | 4 | case "alp_asc": 5 | return {filename: 1} 6 | case "alp_desc": 7 | return {filename: -1} 8 | case "date_asc": 9 | return {uploadDate: 1} 10 | default: 11 | return {uploadDate: -1} 12 | } 13 | } 14 | 15 | module.exports = sortBySwitch; -------------------------------------------------------------------------------- /src/utils/sortBySwitchFolder.js: -------------------------------------------------------------------------------- 1 | const sortBySwitch = (sortBy) => { 2 | switch(sortBy) { 3 | 4 | case "alp_asc": 5 | return {name: 1} 6 | case "alp_desc": 7 | return {name: -1} 8 | case "date_asc": 9 | return {createdAt: 1} 10 | default: 11 | return {createdAt: -1} 12 | } 13 | } 14 | 15 | module.exports = sortBySwitch; -------------------------------------------------------------------------------- /src/utils/updateSettings.js: -------------------------------------------------------------------------------- 1 | let updateSettingsID = ""; 2 | 3 | export const getUpdateSettingsID = () => { 4 | return updateSettingsID; 5 | } 6 | 7 | export const setUpdateSettingsID = (id) => { 8 | updateSettingsID = id; 9 | } -------------------------------------------------------------------------------- /src/utils/videoChecker.js: -------------------------------------------------------------------------------- 1 | const videoExtList = [ 2 | "3g2", 3 | "3gp", 4 | "aaf", 5 | "asf", 6 | "avchd", 7 | "avi", 8 | "drc", 9 | "flv", 10 | "m2v", 11 | "m4p", 12 | "m4v", 13 | "mkv", 14 | "mng", 15 | "mov", 16 | "mp2", 17 | "mp4", 18 | "mpe", 19 | "mpeg", 20 | "mpg", 21 | "mpv", 22 | "mxf", 23 | "nsv", 24 | "ogg", 25 | "ogv", 26 | "qt", 27 | "rm", 28 | "rmvb", 29 | "roq", 30 | "svi", 31 | "vob", 32 | "webm", 33 | "wmv", 34 | "yuv" 35 | ] 36 | 37 | const videoChecker = (filename) => { 38 | 39 | if (filename.length < 1 || !filename.includes(".")) { 40 | 41 | return false; 42 | } 43 | 44 | const extSplit = filename.split("."); 45 | 46 | if (extSplit < 1) { 47 | 48 | return false; 49 | } 50 | 51 | const ext = extSplit[extSplit.length - 1]; 52 | 53 | return videoExtList.includes(ext.toLowerCase()); 54 | 55 | } 56 | 57 | module.exports = videoChecker; -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | "./public/index.html", 7 | ], 8 | theme: { 9 | extend: { 10 | colors: { 11 | primary: "#3c85ee", 12 | "primary-hover": "#326bcc", 13 | "white-hover": "#f6f5fd", 14 | "gray-primary": "#637381", 15 | "gray-secondary": "#e8eef2", 16 | "gray-third": "#ebe9f9", 17 | "light-primary": "rgba(60, 133, 238, 0.4)", 18 | }, 19 | }, 20 | screens: { 21 | quickAccessOne: "1000px", 22 | quickAccessTwo: "1210px", 23 | quickAccessThree: "1420px", 24 | quickAccessFour: "1600px", 25 | xxs: "360px", 26 | xs: "480px", 27 | sm: "640px", 28 | md: "768px", 29 | lg: "1024px", 30 | xl: "1280px", 31 | xxl: "1536px", 32 | fileTextXL: "1600px", 33 | fileTextLG: "1400px", 34 | fileTextMD: "1200px", 35 | fileTextSM: "1000px", 36 | fileTextXSM: "900px", 37 | fileListShowDetails: "680px", 38 | desktopMode: "1100px", 39 | }, 40 | }, 41 | plugins: [], 42 | }; 43 | -------------------------------------------------------------------------------- /tests/utils/db-setup.js: -------------------------------------------------------------------------------- 1 | const createTestData = (mongoose) => { 2 | const file = new mongoose.model("fs.files"); 3 | 4 | file.create({ 5 | name: "test.txt", 6 | type: "text/plain", 7 | size: 10, 8 | userId: "5f7e5d8d1f962d5a0f5e8a9e", 9 | }); 10 | }; 11 | 12 | const envFileFix = (env) => { 13 | env.key = process.env.KEY; 14 | env.newKey = process.env.NEW_KEY; 15 | env.passwordAccess = process.env.PASSWORD_ACCESS; 16 | env.passwordRefresh = process.env.PASSWORD_REFRESH; 17 | env.passwordCookie = process.env.PASSWORD_COOKIE; 18 | env.createAcctBlocked = process.env.BLOCK_CREATE_ACCOUNT; 19 | env.root = process.env.ROOT; 20 | env.url = process.env.URL; 21 | env.mongoURL = process.env.MONGODB_URL; 22 | env.dbType = process.env.DB_TYPE; 23 | env.fsDirectory = process.env.FS_DIRECTORY; 24 | env.s3ID = process.env.S3_ID; 25 | env.s3Key = process.env.S3_KEY; 26 | env.s3Bucket = process.env.S3_BUCKET; 27 | env.useDocumentDB = process.env.USE_DOCUMENT_DB; 28 | env.documentDBBundle = process.env.DOCUMENT_DB_BUNDLE; 29 | env.sendgridKey = process.env.SENDGRID_KEY; 30 | env.sendgridEmail = process.env.SENDGRID_EMAIL; 31 | env.remoteURL = process.env.REMOTE_URL; 32 | env.secureCookies = process.env.SECURE_COOKIES; 33 | env.tempDirectory = process.env.TEMP_DIRECTORY; 34 | env.emailVerification = process.env.EMAIL_VERIFICATION; 35 | env.emailDomain = process.env.EMAIL_DOMAIN; 36 | env.emailAPIKey = process.env.EMAIL_API_KEY; 37 | env.emailHost = process.env.EMAIL_HOST; 38 | env.emailPort = process.env.EMAIL_PORT; 39 | env.emailAddress = process.env.EMAIL_ADDRESS; 40 | env.videoThumbnailsEnabled = process.env.VIDEO_THUMBNAILS_ENABLED === "true"; 41 | env.tempVideoThumbnailLimit = process.env.TEMP_VIDEO_THUMBNAIL_LIMIT 42 | ? +process.env.TEMP_VIDEO_THUMBNAIL_LIMIT 43 | : 0; 44 | env.docker = process.env.DOCKER === "true"; 45 | }; 46 | 47 | module.exports = { 48 | envFileFix, 49 | }; 50 | -------------------------------------------------------------------------------- /tests/utils/express-app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | // const requestIp = require("request-ip"); 3 | const bodyParser = require("body-parser"); 4 | const cookieParser = require("cookie-parser"); 5 | const helmet = require("helmet"); 6 | const compression = require("compression"); 7 | const busboy = require("connect-busboy"); 8 | const userRouter = 9 | require("../../dist-backend/express-routers/user-router").default; 10 | const fileRouter = 11 | require("../../dist-backend/express-routers/file-router").default; 12 | const folderRouter = 13 | require("../../dist-backend/express-routers/folder-router").default; 14 | const env = require("../../dist-backend/enviroment/env"); 15 | const middlewareErrorHandler = 16 | require("../../dist-backend/middleware/utils/middleware-utils").middlewareErrorHandler; 17 | const getEnviromentVariables = require("../../dist-backend/enviroment/get-env-variables"); 18 | 19 | process.env.NODE_ENV = "test"; 20 | 21 | getEnviromentVariables(); 22 | 23 | const app = express(); 24 | 25 | app.use(cookieParser(env.passwordCookie)); 26 | app.use(helmet()); 27 | app.use(compression()); 28 | app.use(express.json()); 29 | app.use(bodyParser.json({ limit: "50mb" })); 30 | app.use( 31 | bodyParser.urlencoded({ 32 | limit: "50mb", 33 | extended: true, 34 | parameterLimit: 50000, 35 | }) 36 | ); 37 | // app.use(requestIp.mw()); 38 | 39 | app.use( 40 | busboy({ 41 | highWaterMark: 2 * 1024 * 1024, 42 | }) 43 | ); 44 | 45 | app.use(userRouter, fileRouter, folderRouter); 46 | 47 | app.use(middlewareErrorHandler); 48 | 49 | module.exports = app; 50 | -------------------------------------------------------------------------------- /tests/utils/fileUtils.js: -------------------------------------------------------------------------------- 1 | // const createFolderTree = async (request, app, token) => { 2 | // const rootFolderResponse = await request(app) 3 | // .post("/folder-service/create") 4 | // .set("Cookie", authToken) 5 | // .send({ 6 | // name: "test", 7 | // parent: "/", 8 | // }); 9 | 10 | // expect(rootFolderResponse.status).toBe(201); 11 | 12 | // const rootFolderId = rootFolderResponse.body._id; 13 | 14 | // const folder2Response = 15 | // }; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { visualizer } from "rollup-plugin-visualizer"; 4 | import { VitePWA } from "vite-plugin-pwa"; 5 | 6 | export default defineConfig(({ mode }) => { 7 | const env = loadEnv(mode, "./src/config/"); 8 | 9 | const proxyURL = env.VITE_PROXY_URL || "http://localhost:3000"; 10 | 11 | console.log(`\nBackend Development Proxy URL: ${proxyURL}/api\n`); 12 | 13 | return { 14 | plugins: [ 15 | react(), 16 | visualizer(), 17 | VitePWA({ 18 | registerType: "autoUpdate", 19 | workbox: { 20 | globPatterns: ["**/*.{js,css,html,ico,png,svg}"], 21 | navigateFallbackDenylist: [ 22 | /^\/file-service\/download\//, // Matches any path starting with /file-service/download/ 23 | /^\/file-service\/public\/download\/[^\/]+\/[^\/]+/, // Matches /file-service/public/download/:id/:tempToken 24 | /^\/folder-service\/download-zip/, // Matches /folder-service/download-zip 25 | /^\/file-service\/stream-video\/[^\/]+/, // Matches /file-service/stream-video/:id 26 | /^\/file-service\/download\/[^\/]+/, // Matches /file-service/download/:id 27 | ], 28 | runtimeCaching: [ 29 | { 30 | // Matches any URL that follows the pattern /file-service/thumbnail/{id} 31 | urlPattern: /\/file-service\/thumbnail\/[a-zA-Z0-9_-]+$/, 32 | handler: "CacheFirst", 33 | options: { 34 | cacheName: "dynamic-thumbnails", 35 | expiration: { 36 | maxEntries: 1000, 37 | maxAgeSeconds: 60 * 60 * 24 * 7, // Cache for 1 week 38 | }, 39 | cacheableResponse: { 40 | statuses: [0, 200], 41 | }, 42 | }, 43 | }, 44 | ], 45 | }, 46 | }), 47 | ], 48 | build: { 49 | outDir: "dist-frontend", 50 | }, 51 | resolve: { 52 | extensions: [".js", ".jsx", ".ts", ".tsx"], // Include these extensions 53 | }, 54 | envDir: "./src/config/", 55 | server: { 56 | proxy: proxyURL 57 | ? { 58 | "/api": { 59 | target: proxyURL, // The port where your backend is running 60 | changeOrigin: true, 61 | rewrite: (path) => path.replace(/^\/api/, ""), 62 | }, 63 | } 64 | : undefined, 65 | host: proxyURL ? true : undefined, 66 | }, 67 | }; 68 | }); 69 | --------------------------------------------------------------------------------