added upload changes

This commit is contained in:
subnub
2024-07-22 00:43:04 -04:00
parent 73caa872c9
commit db925a69d2
17 changed files with 425 additions and 255 deletions
+3 -1
View File
@@ -274,7 +274,9 @@ class StorageService {
throw new NotAuthorizedError("File Not Public");
}
await dbUtilsFile.removeOneTimePublicLink(fileID);
if (file.metadata.linkType === "one") {
await dbUtilsFile.removeOneTimePublicLink(fileID);
}
const user = (await User.findById(file.metadata.owner)) as UserInterface;
+35
View File
@@ -2,6 +2,7 @@ import { QueryFunctionContext } from "react-query";
import axios from "../axiosInterceptor";
import { getUserToken } from "./user";
import { FileInterface } from "../types/file";
import { AxiosRequestConfig } from "axios";
interface QueryKeyParams {
parent: string;
@@ -124,6 +125,33 @@ export const getSuggestedListAPI = async ({
return response.data;
};
export const getPublicFileInfoAPI = async (
fileID: string,
tempToken: string
) => {
const response = await axios.get(
`/file-service/public/info/${fileID}/${tempToken}`
);
return response.data;
};
export const downloadPublicFileAPI = async (
fileID: string,
tempToken: string
) => {
await getUserToken();
// TODO: Change this
const url = `http://localhost:5173/api/file-service/public/download/${fileID}/${tempToken}`;
const link = document.createElement("a");
document.body.appendChild(link);
link.href = url;
link.setAttribute("type", "hidden");
link.setAttribute("download", "true");
link.click();
};
// PATCH
export const trashFileAPI = async (fileID: string) => {
@@ -202,3 +230,10 @@ export const deleteVideoTokenAPI = async () => {
);
return response.data;
};
// POST
export const uploadFileAPI = async (data: FormData, config: any) => {
const url = "/file-service/upload";
const response = await axios.post(url, data, config);
return response.data;
};
+10 -6
View File
@@ -5,7 +5,10 @@ import { useClickOutOfBounds } from "../../hooks/utils";
import { showCreateFolderPopup } from "../../popups/folder";
import { useAppDispatch } from "../../hooks/store";
import { startAddFile } from "../../actions/files";
import { useRef } from "react";
import { RefObject, useRef } from "react";
import axiosNonInterceptor from "axios";
import uuid from "uuid";
import { useUploader } from "../../hooks/files";
interface AddNewDropdownProps {
closeDropdown: () => void;
@@ -15,7 +18,8 @@ const AddNewDropdown: React.FC<AddNewDropdownProps> = (props) => {
const params = useParams();
const { invalidateFoldersCache } = useFoldersClient();
const { wrapperRef } = useClickOutOfBounds(props.closeDropdown);
const uploadRef = useRef("");
const uploadRef: RefObject<HTMLInputElement> = useRef(null);
const { uploadFiles } = useUploader();
const dispatch = useAppDispatch();
const createFolder = async () => {
@@ -35,10 +39,10 @@ const AddNewDropdown: React.FC<AddNewDropdownProps> = (props) => {
props.closeDropdown();
console.log("handle upload");
dispatch(startAddFile(uploadRef.current, params.id));
if (uploadRef && uploadRef.current) {
uploadRef.current = "";
}
const files = uploadRef.current?.files;
if (!files) return;
uploadFiles(files);
};
return (
@@ -1,33 +0,0 @@
import Spinner from "../SpinnerLogin";
import React from "react";
const DownloadPage = ({ download, state }) => {
if (state.size === "" && !state.error) {
return (
<div className="downloadpage__box">
<Spinner />
</div>
);
}
return (
<div className="downloadpage">
{!state.error ? (
<div className="downloadpage__box">
<p className="downloadpage__box__title">{state.title}</p>
<p className="downloadpage__box__subtitle">Type: {state.type}</p>
<p className="downloadpage__box__subtitle">Size: {state.size}</p>
<button className="button popup-window__button" onClick={download}>
Download
</button>
</div>
) : (
<div className="downloadpage__box">
<p>Unauthorized/Not Found Download</p>
</div>
)}
</div>
);
};
export default DownloadPage;
-85
View File
@@ -1,85 +0,0 @@
import DownloadPage from "./DownloadPage";
import capitalize from "../../utils/capitalize";
import axios from "../../axiosInterceptor";
import bytes from "bytes";
import React from "react";
class DownloadPageContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
error: false,
title: "",
type: "",
size: "",
}
this.isPersonalFile = false;
}
getFileExtension = (filename) => {
const filenameSplit = filename.split(".");
if (filenameSplit.length > 1) {
const extension = filenameSplit[filenameSplit.length - 1]
return extension.toUpperCase();
} else {
return "Unknown"
}
}
componentDidMount = () => {
const _id = this.props.match.params.id;
const tempToken = this.props.match.params.tempToken
axios.get(`/file-service/public/info/${_id}/${tempToken}`).then((results) => {
const data = results.data;
const title = capitalize(data.filename);
const size = bytes(data.length);
const type = this.getFileExtension(title);
this.isPersonalFile = results.data.metadata.personalFile;
this.setState(() => ({
...this.state,
title,
type,
size
}))
}).catch((err) => {
console.log(err)
this.setState(() => ({...this.state, error: true}))
})
}
download = () => {
const _id = this.props.match.params.id;
const tempToken = this.props.match.params.tempToken
const finalUrl = !this.isPersonalFile ? `/file-service/public/download/${_id}/${tempToken}` : `/file-service-personal/public/download/${_id}/${tempToken}`;
const link = document.createElement('a');
document.body.appendChild(link);
link.href = finalUrl;
link.setAttribute('type', 'hidden');
link.click();
}
render() {
return <DownloadPage state={this.state} download={this.download} {...this.props}/>
}
}
export default DownloadPageContainer;
+129
View File
@@ -0,0 +1,129 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useParams } from "react-router-dom";
import axios from "../../axiosInterceptor";
import {
downloadPublicFileAPI,
getPublicFileInfoAPI,
} from "../../api/filesAPI";
import { toast, ToastContainer } from "react-toastify";
import SpinnerPage from "../SpinnerPage";
import moment from "moment";
import { getFileColor, getFileExtension } from "../../utils/files";
import { FileInterface } from "../../types/file";
import bytes from "bytes";
const PublicDownloadPage = () => {
const [file, setFile] = useState<FileInterface | null>(null);
const params = useParams();
const getFile = useCallback(async () => {
try {
const id = params.id!;
const tempToken = params.tempToken!;
const fileResponse = await getPublicFileInfoAPI(id, tempToken);
setFile(fileResponse);
} catch (e) {
console.log("Error getting publicfile info", e);
toast.error("Error getting public file");
}
}, [params.id, params.tempToken, getPublicFileInfoAPI]);
const downloadItem = useCallback(() => {
const id = params.id!;
const tempToken = params.tempToken!;
downloadPublicFileAPI(id, tempToken);
}, [params.id, params.tempToken, downloadPublicFileAPI]);
useEffect(() => {
getFile();
}, [getFile]);
if (!file) {
return (
<div className="w-screen h-screen flex justify-center items-center">
<SpinnerPage />
<ToastContainer position="bottom-right" />
</div>
);
}
const fileExtension = getFileExtension(file.filename, 3);
const imageColor = getFileColor(file.filename);
const formattedDate = moment(file.uploadDate).format("L");
const fileSize = bytes(file.metadata.size);
return (
<div>
<div className="flex justify-center items-center w-screen h-screen bg-black bg-opacity-90">
<div
className="absolute top-[20px] flex justify-between w-full"
id="actions-wrapper"
>
<div className="ml-4 flex items-center">
<span className="inline-flex items-center mr-[15px] max-w-[27px] min-w-[27px] min-h-[27px] max-h-[27px]">
<div
className="h-[27px] w-[27px] bg-red-500 rounded-[3px] flex flex-row justify-center items-center"
style={{ background: imageColor }}
>
<span className="font-semibold text-[9.5px] text-white">
{fileExtension}
</span>
</div>
</span>
<p className="text-md text-white text-ellipsis overflow-hidden max-w-[200px] md:max-w-[600px] whitespace-nowrap">
{file.filename}
</p>
</div>
</div>
<div className="w-[300px] p-4 bg-white rounded-md border shadow-lg">
<div className="mt-2 flex justify-between">
<span className="text-[#637381] text-[13px] font-normal leading-[20px] min-w-[50px]">
Type
</span>
<span className="text-[#212b36] text-[13px] font-normal leading-[20px]">
{fileExtension}
</span>
</div>
<div className="mt-2 flex justify-between">
<span className="text-[#637381] text-[13px] font-normal leading-[20px] min-w-[50px]">
Size
</span>
<span className="text-[#212b36] text-[13px] font-normal leading-[20px]">
{fileSize}
</span>
</div>
<div className="mt-2 flex justify-between">
<span className="text-[#637381] text-[13px] font-normal leading-[20px] min-w-[50px]">
Created
</span>
<span className="text-[#212b36] text-[13px] font-normal leading-[20px]">
{formattedDate}
</span>
</div>
<div className="mt-2 flex justify-between">
<span className="text-[#637381] text-[13px] font-normal leading-[20px] min-w-[50px]">
Access
</span>
<span className="text-[#212b36] text-[13px] font-normal leading-[20px]">
{file.metadata.link ? "Public" : "Private"}
</span>
</div>
<div className="mt-[15px] flex justify-center">
<a
onClick={downloadItem}
className="w-[80px] h-[40px] inline-flex items-center justify-center border border-[#3c85ee] rounded-[4px] text-[#3c85ee] text-[15px] font-medium no-underline animate mr-4 cursor-pointer hover:bg-[#f6f5fd]"
>
Download
</a>
</div>
</div>
</div>
<ToastContainer position="bottom-right" />
</div>
);
};
export default PublicDownloadPage;
+5 -1
View File
@@ -9,6 +9,10 @@ import { useAppSelector } from "../../hooks/store";
import { ToastContainer } from "react-toastify";
const Homepage = () => {
const showUploader = useAppSelector(
(state) => state.uploader.uploads.length !== 0
);
return (
<div>
<HomepageSpinner />
@@ -17,7 +21,7 @@ const Homepage = () => {
<Header />
<div className="flex space-between">
<MainSection />
<Uploader />
{showUploader && <Uploader />}
</div>
</div>
+4 -1
View File
@@ -298,7 +298,10 @@ const PhotoViewerPopup = memo(() => {
className="max-w-[80vw] max-h-[80vh] flex justify-center items-center"
>
{!file.metadata.isVideo && (
<img src={image} className="max-w-full max-h-full object-contain" />
<img
src={image}
className="max-w-full max-h-full object-contain select-none"
/>
)}
{file.metadata.isVideo && (
<video
+1 -1
View File
@@ -151,7 +151,7 @@ const SharePopup = memo(() => {
useEffect(() => {
if (!file.metadata.link) return;
const url = `${window.location.origin}/download-page/${file._id}/${shareLink}`;
const url = `${window.location.origin}/public-download/${file._id}/${file.metadata.link}`;
console.log("url", url);
setShareLink(url);
}, [file.metadata.link]);
+44 -92
View File
@@ -1,18 +1,18 @@
import { connect } from "react-redux";
import React, { useEffect, useRef } from "react";
import bytes from "bytes";
import { useFilesClient, useQuickFilesClient } from "../../hooks/files";
import { getCancelToken } from "../../utils/cancelTokenManager";
import CloseIcon from "../../icons/CloseIcon";
import CheckCircleIcon from "../../icons/CheckCircleIcon";
import AlertIcon from "../../icons/AlertIcon";
const UploadItem = (props) => {
const filesRefreshed = useRef(false);
const { invalidateFilesCache } = useFilesClient();
const { invalidateQuickFilesCache } = useQuickFilesClient();
const completed = props.completed;
const uploadImage = props.getUploadImage();
const canceled = props.canceled;
const { completed, canceled, progress, name, id } = props;
const cancelToken = getCancelToken(id);
// TODO: Add ability to cancel indivdual uploads
useEffect(() => {
if (completed && !filesRefreshed.current) {
invalidateFilesCache();
@@ -21,97 +21,49 @@ const UploadItem = (props) => {
}
}, [completed]);
if (!completed && !canceled) {
return (
<div className="elem__upload uploading__now">
<div className="upload__elem--status">
<span>
<img src="/assets/upload_now.svg" alt="upload" />
</span>
</div>
<div className="upload__info">
<div className="top__upload">
<div className="upload__text">
<p>{props.name}</p>
</div>
<div className="upload__size">
<div className="stop__download">
<span>
<img
onClick={props.cancelUploadEvent}
src="/assets/cancel.svg"
alt="cancel"
/>
</span>
</div>
<span>{bytes(props.size)}</span>
</div>
const cancelUpload = () => {
cancelToken.cancel();
};
const ProgressIcon = () => {
if (completed) {
return <CheckCircleIcon className="w-[20px] h-[20px] text-green-600" />;
} else if (canceled) {
return <AlertIcon className="w-[20px] h-[20px] text-red-600" />;
} else {
return (
<CloseIcon
className="w-[20px] h-[20px] cursor-pointer"
onClick={cancelUpload}
/>
);
}
};
return (
<div className="relative p-[20px] flex justify-between items-start hover:bg-[#f6f5fd]">
<div className="w-full">
<div className="flex justify-between items-center mb-[15px]">
<div className="mr-[30px]">
<p className="text-[15px] leading-[18px] font-medium max-w-[160px] overflow-hidden whitespace-nowrap text-ellipsis">
{name}
</p>
</div>
<div className="bottom__upload">
<div className="progress__upload">
<div
className="active__progress"
style={{ width: `${props.progress}%` }}
></div>
</div>
<div>
<ProgressIcon />
</div>
</div>
<div>
<div className="w-full bg-[#e0dcf3] rounded-[1.5px] h-[3px] relative">
<div
className="h-[3px] bg-[#3c85ee] rounded-[1.5px]"
style={{ width: `${progress}%` }}
></div>
</div>
</div>
</div>
);
} else if (completed) {
return (
<div className="elem__upload uploaded__already">
<div className="upload__elem--status">
<span>
<img src="/assets/uploaded__success.svg" alt="upload" />
</span>
</div>
<div className="upload__info">
<div className="top__upload">
<div className="upload__text">
<p>{props.name}</p>
</div>
<div className="upload__size">
<span>{bytes(props.size)}</span>
</div>
</div>
<div className="bottom__upload">
<div className="progress__upload">
<div
className="active__progress"
style={{ width: `${props.progress}%` }}
></div>
</div>
</div>
</div>
</div>
);
} else {
return (
<div className="elem__upload uploaded__cancelled">
<div className="upload__elem--status">
<span>
<img src="/assets/uploaded__failed.svg" alt="upload" />
</span>
</div>
<div className="upload__info">
<div className="top__upload">
<div className="upload__text">
<p>{props.name}</p>
</div>
<div className="retry__download">
<a href="#">Retry</a>
</div>
</div>
<div className="bottom__upload">
<div className="failed__info">
<span>Upload failed</span>
</div>
</div>
</div>
</div>
);
}
</div>
);
};
export default connect()(UploadItem);
+31 -32
View File
@@ -1,39 +1,38 @@
import { useAppSelector } from "../../hooks/store";
import UploadItem from "../UploadItem";
import React from "react";
const Uploader = React.forwardRef((props, ref) => (
<div
className="upload__status"
style={
props.uploads.length !== 0 ? { display: "block" } : { display: "none" }
}
>
<div className="head__upload">
<p>
Uploading {props.uploads.length}{" "}
{props.uploads.length === 1 ? "file" : "files"}
</p>
<div className="hide__upload">
<a onClick={() => props.minimizeUploader()}>
<img src="/assets/upload_hide.svg" alt="upload__hide" />
</a>
<a onClick={() => props.cancelAllUploadsEvent()}>
<img
src="/assets/close-white.svg"
style={{ height: "24px" }}
alt="upload__hide"
/>
</a>
const Uploader = React.forwardRef((props, ref) => {
const uploads = useAppSelector((state) => state.uploader.uploads);
return (
<div className="upload__status">
<div className="head__upload">
<p>
Uploading {uploads.length} {uploads.length === 1 ? "file" : "files"}
</p>
<div className="hide__upload">
<a onClick={() => props.minimizeUploader()}>
<img src="/assets/upload_hide.svg" alt="upload__hide" />
</a>
<a onClick={() => props.cancelAllUploadsEvent()}>
<img
src="/assets/close-white.svg"
style={{ height: "24px" }}
alt="upload__hide"
/>
</a>
</div>
</div>
<div className="content__upload">
{props.uploaderShow
? uploads.map((upload) => {
return <UploadItem key={upload.id} {...upload} />;
})
: undefined}
</div>
</div>
<div className="content__upload">
{props.uploaderShow
? props.uploads.map((upload) => {
return <UploadItem key={upload.id} {...upload} />;
})
: undefined}
</div>
</div>
));
);
});
export default Uploader;
+89 -1
View File
@@ -12,12 +12,18 @@ import {
getFilesListAPI,
getQuickFilesListAPI,
getSuggestedListAPI,
uploadFileAPI,
} from "../api/filesAPI";
import { useCallback, useEffect, useRef, useState } from "react";
import { useSelector } from "react-redux";
import { useAppSelector } from "./store";
import { useAppDispatch, useAppSelector } from "./store";
import { useUtils } from "./utils";
import { FileInterface } from "../types/file";
import { v4 as uuid } from "uuid";
import axiosNonInterceptor from "axios";
import { addFileUploadCancelToken } from "../utils/cancelTokenManager";
import { debounce } from "lodash";
import { addUpload, editUpload } from "../reducers/uploader";
export const useFiles = (enabled = true) => {
const params = useParams();
@@ -196,3 +202,85 @@ export const useSearchSuggestions = (searchText: string) => {
return { ...searchQuery };
};
export const useUploader = () => {
const dispatch = useAppDispatch();
const params = useParams();
const debounceDispatch = debounce(dispatch, 200);
const uploadFiles = async (files: FileList) => {
for (let i = 0; i < files.length; i++) {
const parent = params.id || "/";
const currentFile = files[i];
const currentID = uuid();
const CancelToken = axiosNonInterceptor.CancelToken;
const source = CancelToken.source();
addFileUploadCancelToken(currentID, source);
const config = {
headers: {
"Content-Type": "multipart/form-data",
"Transfere-Encoding": "chunked",
},
onUploadProgress: (progressEvent: ProgressEvent<EventTarget>) => {
const currentProgress = Math.round(
(progressEvent.loaded / progressEvent.total) * 100
);
if (currentProgress !== 100) {
debounceDispatch(
editUpload({
id: currentID,
updateData: { progress: currentProgress },
})
);
}
},
cancelToken: source.token,
};
dispatch(
addUpload({
id: currentID,
progress: 0,
name: currentFile.name,
completed: false,
canceled: false,
size: currentFile.size,
})
);
const data = new FormData();
data.append("filename", currentFile.name);
data.append("parent", parent);
data.append("currentID", currentID);
data.append("size", currentFile.size.toString());
data.append("file", currentFile);
try {
await uploadFileAPI(data, config);
dispatch(
editUpload({
id: currentID,
updateData: { completed: true, progress: 100 },
})
);
} catch (e) {
console.log("Error uploading file", e);
dispatch(
editUpload({
id: currentID,
updateData: { canceled: true },
})
);
}
}
};
return { uploadFiles };
};
+15
View File
@@ -0,0 +1,15 @@
type CheckCircleIconType = React.SVGAttributes<SVGSVGElement>;
const CheckCircleIcon: React.FC<CheckCircleIconType> = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<title>check-circle</title>
<path
d="M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M10 17L5 12L6.41 10.59L10 14.17L17.59 6.58L19 8L10 17Z"
fill="currentColor"
/>
</svg>
);
};
export default CheckCircleIcon;
+56
View File
@@ -0,0 +1,56 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface UploadItemType {
id: string;
progress: number;
name: string;
completed: boolean;
canceled: boolean;
size: number;
}
interface UploaderStateType {
uploads: UploadItemType[];
}
const initialState: UploaderStateType = {
uploads: [],
};
const uploaderSlice = createSlice({
name: "uploader",
initialState,
reducers: {
addUpload(state, action: PayloadAction<UploadItemType>) {
state.uploads.push(action.payload);
},
editUpload(
state,
action: PayloadAction<{
id: string;
updateData: {
progress?: number;
completed?: boolean;
canceled?: boolean;
};
}>
) {
const uploads = state.uploads.map((upload) => {
if (upload.id === action.payload.id) {
return {
...upload,
...action.payload.updateData,
};
} else {
return upload;
}
});
state.uploads = uploads;
},
},
});
export const { addUpload, editUpload } = uploaderSlice.actions;
export default uploaderSlice.reducer;
+1 -1
View File
@@ -34,7 +34,7 @@ const AppRouter = () => {
}
/>
<Route
path="/download-page/:id/:tempToken"
path="/public-download/:id/:tempToken"
element={<DownloadPage />}
/>
<Route
+2
View File
@@ -21,6 +21,7 @@ import mobileContextMenuReducer from "../reducers/mobileContextMenu";
import selectedReducer from "../reducers/selected";
import leftSectionReducer from "../reducers/leftSection";
import userReducer from "../reducers/user";
import uploaderReducer from "../reducers/uploader";
//const composeEnchancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
@@ -36,6 +37,7 @@ const store = configureStore({
// selectedItem: selectedItemReducer,
uploads: uploadsReducer,
user: userReducer,
uploader: uploaderReducer,
// storage: storageReducer,
// quickFiles: quickFilesReducer,
// popupFile: popupFilesReducer,
-1
View File
@@ -15,7 +15,6 @@ export const addFileUploadCancelToken = (id: string, cancelToken: any) => {
};
export const getCancelToken = (id: string) => {
console.log("cancel tokens", cancelTokens);
return cancelTokens[id];
};