Merge pull request #5 from ente-io/file-upload

File upload
This commit is contained in:
Pushkar Anand 2021-02-07 21:07:09 +05:30 committed by GitHub
commit 9f6388b8ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1997 additions and 348 deletions

View file

@ -12,6 +12,7 @@
"axios": "^0.20.0",
"bootstrap": "^4.5.2",
"comlink": "^4.3.0",
"exif-js": "^2.3.0",
"formik": "^2.1.5",
"http-proxy-middleware": "^1.0.5",
"libsodium-wrappers": "^0.7.8",
@ -21,6 +22,7 @@
"react": "16.13.1",
"react-bootstrap": "^1.3.0",
"react-dom": "16.13.1",
"react-dropzone": "^11.2.4",
"react-photoswipe": "^1.3.0",
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.6",
@ -42,7 +44,7 @@
"@types/yup": "^0.29.7",
"babel-plugin-styled-components": "^1.11.1",
"next-on-netlify": "^2.4.0",
"typescript": "^4.0.2",
"typescript": "^4.1.3",
"worker-plugin": "^5.0.0"
},
"standard": {

BIN
public/fav-button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/plus-sign.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1,23 @@
import React from "react";
import styled from "styled-components";
const HeartUI = styled.button<{
isClick: boolean,
size: number,
}>`
width: ${props => props.size}px;
height: ${props => props.size}px;
float:right;
background: url("/fav-button.png") no-repeat;
cursor: pointer;
background-size: cover;
border: none;
${({ isClick, size }) => isClick && `background-position: -${28 * size}px;transition: background 1s steps(28);`}
`;
export default function FavButton({ isClick, onClick, size }) {
return (
<HeartUI isClick={isClick} onClick={onClick} size={size}/>
);
}

View file

@ -0,0 +1,41 @@
import React, { useRef } from 'react';
import styled from 'styled-components';
const DropDiv = styled.div`
flex: 1;
display: flex;
flex-direction: column;
`;
type Props = React.PropsWithChildren<{
showModal: () => void;
closeModal: () => void;
}>;
export default function FullScreenDropZone({ children, showModal, closeModal }: Props) {
const closeTimer = useRef<number>();
const clearTimer = () => {
if (closeTimer.current) {
clearTimeout(closeTimer.current);
}
}
const onDragOver = (e) => {
e.preventDefault();
clearTimer();
showModal();
}
const onDragLeave = (e) => {
e.preventDefault();
clearTimer();
closeTimer.current = setTimeout(closeModal, 1000);
}
return (
<DropDiv onDragOver={onDragOver} onDragLeave={onDragLeave}>
{children}
</DropDiv>
);
};

View file

@ -0,0 +1,196 @@
import React, { useEffect, useState } from 'react';
import Photoswipe from 'photoswipe';
import PhotoswipeUIDefault from 'photoswipe/dist/photoswipe-ui-default';
import classnames from 'classnames';
import events from './events';
import FavButton from 'components/FavButton';
import { addToFavorites, removeFromFavorites } from 'services/collectionService';
import { file } from 'services/fileService';
interface Iprops {
isOpen: boolean
items: any[];
options?: Object;
onClose?: () => void;
gettingData?: (instance: any, index: number, item: file) => void;
id?: string;
className?: string;
favItemIds: Set<number>;
setFavItemIds: (favItemIds: Set<number>) => void;
};
function PhotoSwipe(props: Iprops) {
let pswpElement;
const [photoSwipe, setPhotoSwipe] = useState<Photoswipe<any>>();
const { isOpen } = props;
const [isFav, setIsFav] = useState(false)
useEffect(() => {
if (!pswpElement)
return;
if (isOpen)
openPhotoSwipe();
}, [pswpElement]);
useEffect(() => {
if (!pswpElement)
return;
if (isOpen) {
openPhotoSwipe();
}
if (!isOpen) {
closePhotoSwipe();
}
return () => {
closePhotoSwipe();
}
}, [isOpen]);
function updateFavButton() {
console.log(this.currItem.id, props.favItemIds)
setIsFav(isInFav(this?.currItem));
}
const openPhotoSwipe = () => {
const { items, options } = props;
let photoSwipe = new Photoswipe(pswpElement, PhotoswipeUIDefault, items, options);
events.forEach((event) => {
const callback = props[event];
if (callback || event === 'destroy') {
photoSwipe.listen(event, function (...args) {
if (callback) {
args.unshift(this);
callback(...args);
}
if (event === 'destroy') {
handleClose();
}
});
}
});
photoSwipe.listen('beforeChange', updateFavButton);
photoSwipe.init();
setPhotoSwipe(photoSwipe);
};
const updateItems = (items = []) => {
photoSwipe.items = [];
items.forEach((item) => {
photoSwipe.items.push(item);
});
photoSwipe.invalidateCurrItems();
photoSwipe.updateSize(true);
};
const closePhotoSwipe = () => {
if (photoSwipe)
photoSwipe.close();
};
const handleClose = () => {
const { onClose } = props;
if (onClose) {
onClose();
}
};
const isInFav = (file) => {
const { favItemIds } = props;
if (favItemIds && file) {
return favItemIds.has(file.id);
}
else
return false;
}
const onFavClick = async (file) => {
const { favItemIds, setFavItemIds } = props;
if (!isInFav(file)) {
favItemIds.add(file.id);
await addToFavorites(file);
console.log("added to Favorites");
setIsFav(true);
setFavItemIds(favItemIds);
}
else {
favItemIds.delete(file.id);
await removeFromFavorites(file)
console.log("removed from Favorites");
setIsFav(false);
setFavItemIds(favItemIds);
}
}
const { id } = props;
let { className } = props;
className = classnames(['pswp', className]).trim();
return (
<div
id={id}
className={className}
tabIndex={Number("-1")}
role="dialog"
aria-hidden="true"
ref={(node) => {
pswpElement = node;
}}
>
<div className="pswp__bg" />
<div className="pswp__scroll-wrap">
<div className="pswp__container">
<div className="pswp__item" />
<div className="pswp__item" />
<div className="pswp__item" />
</div>
<div className="pswp__ui pswp__ui--hidden">
<div className="pswp__top-bar">
<div className="pswp__counter" />
<button
className="pswp__button pswp__button--close"
title="Share"
/>
<button
className="pswp__button pswp__button--share"
title="Share"
/>
<button
className="pswp__button pswp__button--fs"
title="Toggle fullscreen"
/>
<button className="pswp__button pswp__button--zoom" title="Zoom in/out" />
<FavButton size={44} isClick={isFav} onClick={() => { onFavClick(photoSwipe?.currItem) }} />
<div className="pswp__preloader">
<div className="pswp__preloader__icn">
<div className="pswp__preloader__cut">
<div className="pswp__preloader__donut" />
</div>
</div>
</div>
</div>
<div className="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
<div className="pswp__share-tooltip" />
</div>
<button
className="pswp__button pswp__button--arrow--left"
title="Previous (arrow left)"
/>
<button
className="pswp__button pswp__button--arrow--right"
title="Next (arrow right)"
/>
<div className="pswp__caption">
<div className="pswp__caption__center" />
</div>
</div>
</div>
</div>
);
}
export default PhotoSwipe;

View file

@ -0,0 +1,19 @@
export default [
'beforeChange',
'afterChange',
'imageLoadComplete',
'resize',
'gettingData',
'mouseUsed',
'initialZoomIn',
'initialZoomInEnd',
'initialZoomOut',
'initialZoomOutEnd',
'parseVerticalMargin',
'close',
'unbindEvents',
'destroy',
'updateScrollOffset',
'preventDragEvent',
'shareLinkClick'
];

View file

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import styled, {createGlobalStyle } from 'styled-components';
import styled, { createGlobalStyle } from 'styled-components';
import Navbar from 'components/Navbar';
import constants from 'utils/strings/constants';
import Button from 'react-bootstrap/Button';
@ -13,6 +13,8 @@ import Head from 'next/head';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'react-photoswipe/lib/photoswipe.css';
import localForage from 'localforage';
import UploadButton from 'pages/gallery/components/UploadButton';
import FullScreenDropZone from 'components/FullScreenDropZone';
localForage.config({
driver: localForage.INDEXEDDB,
@ -78,26 +80,59 @@ const GlobalStyles = createGlobalStyle`
.pswp__img {
object-fit: contain;
}
.modal-90w{
width:90vw;
max-width:880px!important;
}
.modal .modal-header, .modal .modal-footer {
border-color: #444 !important;
}
.modal .modal-header .close {
color: #aaa;
text-shadow: none;
}
.modal .card {
background-color: #303030;
border: none;
color: #aaa;
}
.modal .card > div {
border-radius: 30px;
overflow: hidden;
margin: 0 0 5px 0;
}
.modal-content{
background-color:#303030 !important;
color:#aaa;
}
`;
const Image = styled.img`
max-height: 28px;
margin-right: 5px;
max-height: 28px;
margin-right: 5px;
`;
const FlexContainer = styled.div`
flex: 1;
flex: 1;
`;
export default function App({ Component, pageProps }) {
const router = useRouter();
const [user, setUser] = useState();
const [loading, setLoading] = useState(false);
const [uploadButtonView, setUploadButtonView] = useState(false);
const [uploadModalView, setUploadModalView] = useState(false);
const closeUploadModal = () => setUploadModalView(false);
const showUploadModal = () => setUploadModalView(true);
useEffect(() => {
const user = getData(LS_KEYS.USER);
setUser(user);
console.log(`%c${constants.CONSOLE_WARNING_STOP}`, 'color: red; font-size: 52px;');
console.log(
`%c${constants.CONSOLE_WARNING_STOP}`,
'color: red; font-size: 52px;'
);
console.log(`%c${constants.CONSOLE_WARNING_DESC}`, 'font-size: 20px;');
router.events.on('routeChangeStart', (url: string) => {
@ -116,34 +151,47 @@ export default function App({ Component, pageProps }) {
const logout = async () => {
clearKeys();
clearData();
setUploadButtonView(false);
localForage.clear();
const cache = await caches.delete('thumbs');
router.push("/");
}
router.push('/');
};
return (
<>
<FullScreenDropZone
closeModal={closeUploadModal}
showModal={showUploadModal}
>
<Head>
<title>ente.io | Privacy friendly alternative to Google Photos</title>
</Head>
<GlobalStyles />
<Navbar>
<FlexContainer>
<Image alt='logo' src="/icon.png" />
<Image alt='logo' src='/icon.png' />
{constants.COMPANY_NAME}
</FlexContainer>
{user && <Button variant='link' onClick={logout}>
<PowerSettings />
</Button>}
{uploadButtonView && <UploadButton showModal={showUploadModal} />}
{user &&
<Button variant='link' onClick={logout}>
<PowerSettings />
</Button>
}
</Navbar>
{loading
? <Container>
<Spinner animation="border" role="status" variant="primary">
<span className="sr-only">Loading...</span>
{loading ? (
<Container>
<Spinner animation='border' role='status' variant='primary'>
<span className='sr-only'>Loading...</span>
</Spinner>
</Container>
: <Component />
}
</>
) : (
<Component
uploadModalView={uploadModalView}
showUploadModal={showUploadModal}
closeUploadModal={closeUploadModal}
setUploadButtonView={setUploadButtonView}
/>
)}
</FullScreenDropZone>
);
}
}

View file

@ -52,18 +52,14 @@ export default function Credentials() {
try {
const cryptoWorker = await new CryptoWorker();
const { passphrase } = values;
const kek = await cryptoWorker.deriveKey(await cryptoWorker.fromString(passphrase),
await cryptoWorker.fromB64(keyAttributes.kekSalt));
const kek: string = await cryptoWorker.deriveKey(passphrase, keyAttributes.kekSalt);
if (await cryptoWorker.verifyHash(keyAttributes.kekHash, kek)) {
const key = await cryptoWorker.decrypt(
await cryptoWorker.fromB64(keyAttributes.encryptedKey),
await cryptoWorker.fromB64(keyAttributes.keyDecryptionNonce),
kek);
const sessionKeyAttributes = await cryptoWorker.encrypt(key);
const sessionKey = await cryptoWorker.toB64(sessionKeyAttributes.key);
const sessionNonce = await cryptoWorker.toB64(sessionKeyAttributes.nonce);
const encryptionKey = await cryptoWorker.toB64(sessionKeyAttributes.encryptedData);
const key: string = await cryptoWorker.decryptB64(keyAttributes.encryptedKey, keyAttributes.keyDecryptionNonce, kek);
const sessionKeyAttributes = await cryptoWorker.encryptToB64(key);
const sessionKey = sessionKeyAttributes.key;
const sessionNonce = sessionKeyAttributes.nonce;
const encryptionKey = sessionKeyAttributes.encryptedData;
setKey(SESSION_KEYS.ENCRYPTION_KEY, { encryptionKey });
setData(LS_KEYS.SESSION, { sessionKey, sessionNonce });
router.push('/gallery');

View file

@ -0,0 +1,55 @@
import React, { useState } from "react";
import { Card } from "react-bootstrap";
import styled from "styled-components";
import CreateCollection from "./CreateCollection";
import DropzoneWrapper from "./DropzoneWrapper";
const ImageContainer = styled.div`
min-height: 192px;
max-width: 192px;
border: 1px solid #555;
display: flex;
align-items: center;
justify-content: center;
font-size: 42px;
`;
const StyledCard = styled(Card)`
cursor: pointer;
`;
export default function AddCollection(props) {
const [acceptedFiles, setAcceptedFiles] = useState<File[]>();
const [createCollectionView, setCreateCollectionView] = useState(false);
const { closeUploadModal, showUploadModal, ...rest } = props;
const createCollection = (acceptedFiles) => {
setAcceptedFiles(acceptedFiles);
setCreateCollectionView(true);
};
const children = (
<StyledCard>
<ImageContainer>+</ImageContainer>
<Card.Text style={{ textAlign: "center" }}>Create New Album</Card.Text>
</StyledCard>
);
return (
<>
<DropzoneWrapper
onDropAccepted={createCollection}
onDropRejected={closeUploadModal}
onDragOver={showUploadModal}
children={children}
/>
<CreateCollection
{...rest}
modalView={createCollectionView}
closeUploadModal={closeUploadModal}
closeModal={() => setCreateCollectionView(false)}
acceptedFiles={acceptedFiles}
/>
</>
)
}

View file

@ -0,0 +1,38 @@
import React from 'react';
import UploadService from 'services/uploadService';
import { getToken } from 'utils/common/key';
import DropzoneWrapper from './DropzoneWrapper';
function CollectionDropZone({
children,
closeModal,
showModal,
refetchData,
collectionLatestFile,
setProgressView,
progressBarProps
}) {
const upload = async (acceptedFiles) => {
const token = getToken();
closeModal();
progressBarProps.setPercentComplete(0);
setProgressView(true);
await UploadService.uploadFiles(acceptedFiles, collectionLatestFile, token, progressBarProps);
refetchData();
setProgressView(false);
}
return (
<DropzoneWrapper
children={children}
onDropAccepted={upload}
onDragOver={showModal}
onDropRejected={closeModal}
/>
);
};
export default CollectionDropZone;

View file

@ -0,0 +1,56 @@
import React, { useEffect, useState } from 'react';
import { Button, Card, Modal } from 'react-bootstrap';
import CollectionDropZone from './CollectionDropZone';
import AddCollection from './AddCollection';
import PreviewCard from './PreviewCard';
import constants from 'utils/strings/constants';
function CollectionSelector(props) {
const {
uploadModalView,
closeUploadModal,
showUploadModal,
collectionLatestFile,
...rest
} = props;
const CollectionIcons = collectionLatestFile?.map((item) => (
<CollectionDropZone key={item.collection.id}
{...rest}
closeModal={closeUploadModal}
showModal={showUploadModal}
collectionLatestFile={item}
>
<Card>
<PreviewCard data={item.file} updateUrl={() => { }} forcedEnable />
<Card.Text className="text-center">{item.collection.name}</Card.Text>
</Card>
</CollectionDropZone>
));
return (
<Modal
show={uploadModalView}
onHide={closeUploadModal}
dialogClassName="modal-90w"
>
<Modal.Header closeButton>
<Modal.Title >
{constants.SELECT_COLLECTION}
</Modal.Title>
</Modal.Header>
<Modal.Body style={{ display: "flex", justifyContent: "flex-start", flexWrap: "wrap" }}>
<AddCollection
{...rest}
showUploadModal={showUploadModal}
closeUploadModal={closeUploadModal}
/>
{CollectionIcons}
</Modal.Body>
</Modal>
);
}
export default CollectionSelector;

View file

@ -1,5 +1,5 @@
import React from 'react';
import { collection } from 'services/fileService';
import { collection } from 'services/collectionService';
import styled from 'styled-components';
interface CollectionProps {
@ -57,6 +57,7 @@ export default function Collections(props: CollectionProps) {
<Wrapper>
<Chip active={!selected} onClick={clickHandler()}>All</Chip>
{collections?.map(item => <Chip
key={item.id}
active={selected === item.id.toString()}
onClick={clickHandler(item.id)}
>{item.name}</Chip>)}

View file

@ -0,0 +1,72 @@
import React, { useEffect, useState } from 'react';
import { Button, Form, Modal } from 'react-bootstrap';
import { createAlbum } from 'services/collectionService';
import UploadService from 'services/uploadService';
import { collectionLatestFile } from 'services/collectionService'
import { getToken } from 'utils/common/key';
export default function CreateCollection(props) {
const { acceptedFiles, setProgressView, progressBarProps, refetchData, modalView, closeModal, closeUploadModal } = props;
const [albumName, setAlbumName] = useState("");
const handleChange = (event) => { setAlbumName(event.target.value); }
useEffect(() => {
if (acceptedFiles == null)
return;
let commonPathPrefix: string = (() => {
const paths: string[] = acceptedFiles.map(files => files.path);
paths.sort();
let firstPath = paths[0], lastPath = paths[paths.length - 1], L = firstPath.length, i = 0;
while (i < L && firstPath.charAt(i) === lastPath.charAt(i)) i++;
return firstPath.substring(0, i);
})();
if (commonPathPrefix)
commonPathPrefix = commonPathPrefix.substr(1, commonPathPrefix.lastIndexOf('/') - 1);
setAlbumName(commonPathPrefix);
}, [acceptedFiles]);
const handleSubmit = async (event) => {
const token = getToken();
event.preventDefault();
closeModal();
closeUploadModal();
const collection = await createAlbum(albumName);
const collectionLatestFile: collectionLatestFile = { collection, file: null }
progressBarProps.setPercentComplete(0);
setProgressView(true);
await UploadService.uploadFiles(acceptedFiles, collectionLatestFile, token, progressBarProps);
refetchData();
setProgressView(false);
}
return (
<Modal
show={modalView}
onHide={closeModal}
centered
backdrop="static"
>
<Modal.Header closeButton>
<Modal.Title>
Create Collection
</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form onSubmit={handleSubmit}>
<Form.Group controlId="formBasicEmail">
<Form.Label>Album Name:</Form.Label>
<Form.Control type="text" placeholder="Enter Album Name" value={albumName} onChange={handleChange} />
</Form.Group>
<Button variant="primary" type="submit" style={{ width: "100%" }}>
Submit
</Button>
</Form>
</Modal.Body>
</Modal>
);
}

View file

@ -0,0 +1,64 @@
import React from 'react';
import Dropzone from 'react-dropzone';
import styled from 'styled-components';
export const getColor = (props) => {
if (props.isDragAccept) {
return '#00e676';
}
if (props.isDragReject) {
return '#ff1744';
}
if (props.isDragActive) {
return '#2196f3';
}
};
export const enableBorder = (props) => (props.isDragActive ? 'dashed' : 'none');
export const DropDiv = styled.div`
width:200px;
margin:5px;
height:230px;
color:black;
border-width: 2px;
border-radius: 2px;
border-color: ${(props) => getColor(props)};
border-style: ${(props) => enableBorder(props)};
outline: none;
transition: border 0.24s ease-in-out;
`;
export function DropzoneWrapper(props) {
const { children, ...callbackProps } = props
return (
<Dropzone
noDragEventsBubbling
accept="image/*, video/*, application/json, "
{...callbackProps}
>
{({
getRootProps,
getInputProps,
isDragActive,
isDragAccept,
isDragReject,
}) => {
return (
<DropDiv
{...getRootProps({
isDragActive,
isDragAccept,
isDragReject,
})}
>
<input {...getInputProps()} />
{children}
</DropDiv>
);
}}
</Dropzone>
);
};
export default DropzoneWrapper;

View file

@ -7,7 +7,8 @@ import PlayCircleOutline from 'components/PlayCircleOutline';
interface IProps {
data: file,
updateUrl: (url: string) => void,
onClick: () => void,
onClick?: () => void,
forcedEnable?: boolean,
}
const Cont = styled.div<{ disabled: boolean }>`
@ -41,7 +42,7 @@ const Cont = styled.div<{ disabled: boolean }>`
export default function PreviewCard(props: IProps) {
const [imgSrc, setImgSrc] = useState<string>();
const { data, onClick, updateUrl } = props;
const { data, onClick, updateUrl, forcedEnable } = props;
useEffect(() => {
if (data && !data.msrc) {
@ -57,12 +58,12 @@ export default function PreviewCard(props: IProps) {
}, [data]);
const handleClick = () => {
if (data.msrc || imgSrc) {
onClick();
if (data?.msrc || imgSrc) {
onClick?.();
}
}
return <Cont onClick={handleClick} disabled={!data?.msrc && !imgSrc}>
return <Cont onClick={handleClick} disabled={!forcedEnable && !data?.msrc && !imgSrc}>
<img src={data?.msrc || imgSrc} />
{data?.metadata.fileType === 1 && <PlayCircleOutline />}
</Cont>;

View file

@ -0,0 +1,32 @@
import React, { useState } from "react"
import { UPLOAD_STAGES } from "services/uploadService";
import CollectionSelector from "./CollectionSelector"
import UploadProgress from "./UploadProgress"
export default function Upload(props) {
const [progressView, setProgressView] = useState(false);
const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>(UPLOAD_STAGES.START);
const [fileCounter, setFileCounter] = useState({ current: 0, total: 0 });
const [percentComplete, setPercentComplete] = useState(0);
const init = () => {
setProgressView(false);
setUploadStage(UPLOAD_STAGES.START);
setFileCounter({ current: 0, total: 0 });
setPercentComplete(0);
}
return (<>
<CollectionSelector
{...props}
setProgressView={setProgressView}
progressBarProps={{ setPercentComplete, setFileCounter, setUploadStage }}
/>
<UploadProgress
now={percentComplete}
fileCounter={fileCounter}
uploadStage={uploadStage}
show={progressView}
onHide={init}
/>
</>
)
}

View file

@ -0,0 +1,12 @@
import React from 'react';
import { Button } from 'react-bootstrap';
function UploadButton({ showModal }) {
return (
<Button variant='primary' onClick={showModal}>
Upload New Photos
</Button>
);
};
export default UploadButton;

View file

@ -0,0 +1,31 @@
import React from 'react';
import { Alert, Modal, ProgressBar } from 'react-bootstrap';
import constants from 'utils/strings/constants'
export default function UploadProgress({ fileCounter, uploadStage, now, ...props }) {
return (
<Modal
{...props}
size='lg'
aria-labelledby='contained-modal-title-vcenter'
centered
backdrop="static"
>
<Modal.Header>
<Modal.Title id='contained-modal-title-vcenter'>
Uploading Files
</Modal.Title>
</Modal.Header>
<Modal.Body>
{now === 100 ? (
<Alert variant='success'>{constants.UPLOAD[3]}</Alert>
) : (
<>
<Alert variant='info'>{constants.UPLOAD[uploadStage]} {fileCounter?.total != 0 ? `${fileCounter?.current} ${constants.OF} ${fileCounter?.total}` : ''}</Alert>
<ProgressBar animated now={now} />
</>
)}
</Modal.Body>
</Modal>
);
}

View file

@ -2,28 +2,41 @@ import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import Spinner from 'react-bootstrap/Spinner';
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
import { collection, fetchCollections, file, getFile, getFiles, getPreview } from 'services/fileService';
import {
file,
getFile,
getPreview,
fetchData,
} from 'services/fileService';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import PreviewCard from './components/PreviewCard';
import { getActualKey } from 'utils/common/key';
import styled from 'styled-components';
import { PhotoSwipe } from 'react-photoswipe';
import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe';
import { Options } from 'photoswipe';
import AutoSizer from 'react-virtualized-auto-sizer';
import { VariableSizeList as List } from 'react-window';
import Collections from './components/Collections';
import SadFace from 'components/SadFace';
import Upload from './components/Upload';
import { collection, fetchCollections, collectionLatestFile, getCollectionLatestFile, getFavItemIds } from 'services/collectionService';
import constants from 'utils/strings/constants';
enum ITEM_TYPE {
TIME='TIME',
TILE='TILE'
TIME = 'TIME',
TILE = 'TILE',
}
export enum FILE_TYPE {
IMAGE,
VIDEO,
OTHERS
}
interface TimeStampListItem {
itemType: ITEM_TYPE,
items?: file[],
itemStartIndex?: number,
date?: string,
itemType: ITEM_TYPE;
items?: file[];
itemStartIndex?: number;
date?: string;
}
const Container = styled.div`
@ -76,47 +89,57 @@ const DateContainer = styled.div`
padding: 0 4px;
`;
const PAGE_SIZE = 12;
const COLUMNS = 3;
export default function Gallery() {
export default function Gallery(props) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [collections, setCollections] = useState<collection[]>([])
const [reload, setReload] = useState(0);
const [collections, setCollections] = useState<collection[]>([]);
const [collectionLatestFile, setCollectionLatestFile] = useState<
collectionLatestFile[]
>([]);
const [data, setData] = useState<file[]>();
const [favItemIds, setFavItemIds] = useState<Set<number>>();
const [open, setOpen] = useState(false);
const [options, setOptions] = useState<Options>({
history: false,
maxSpreadZoom: 5,
});
const fetching: { [k: number]: boolean } = {};
const fetching: { [k: number]: boolean } = {};
useEffect(() => {
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
const token = getData(LS_KEYS.USER).token;
if (!key) {
router.push("/");
router.push('/');
}
const main = async () => {
setLoading(true);
const encryptionKey = await getActualKey();
const collections = await fetchCollections(token, encryptionKey);
const resp = await getFiles("0", token, "100", encryptionKey, collections);
await syncWithRemote();
setLoading(false);
setCollections(collections);
setData(resp.map(item => ({
...item,
w: window.innerWidth,
h: window.innerHeight,
})));
};
main();
main();
props.setUploadButtonView(true);
}, []);
const syncWithRemote = async () => {
const token = getData(LS_KEYS.USER).token;
const encryptionKey = await getActualKey();
const collections = await fetchCollections(token, encryptionKey);
const data = await fetchData(token, collections);
const collectionLatestFile = await getCollectionLatestFile(collections, data);
const favItemIds = await getFavItemIds(data);
setCollections(collections);
setData(data);
setCollectionLatestFile(collectionLatestFile);
setFavItemIds(favItemIds);
}
if (!data || loading) {
return <div className="text-center">
<Spinner animation="border" variant="primary" />
</div>
return (
<div className='text-center'>
<Spinner animation='border' variant='primary' />
</div>
);
}
const updateUrl = (index: number) => (url: string) => {
@ -125,8 +148,8 @@ export default function Gallery() {
msrc: url,
w: window.innerWidth,
h: window.innerHeight,
}
if (data[index].metadata.fileType === 1 && !data[index].html) {
};
if (data[index].metadata.fileType === FILE_TYPE.VIDEO && !data[index].html) {
data[index].html = `
<div class="video-loading">
<img src="${url}" />
@ -137,11 +160,11 @@ export default function Gallery() {
`;
delete data[index].src;
}
if (data[index].metadata.fileType === 0 && !data[index].src) {
if (data[index].metadata.fileType === FILE_TYPE.IMAGE && !data[index].src) {
data[index].src = url;
}
setData(data);
}
};
const updateSrcUrl = (index: number, url: string) => {
data[index] = {
@ -149,8 +172,8 @@ export default function Gallery() {
src: url,
w: window.innerWidth,
h: window.innerHeight,
}
if (data[index].metadata.fileType === 1) {
};
if (data[index].metadata.fileType === FILE_TYPE.VIDEO) {
data[index].html = `
<video controls>
<source src="${url}" />
@ -160,11 +183,12 @@ export default function Gallery() {
delete data[index].src;
}
setData(data);
}
};
const handleClose = () => {
setOpen(false);
}
// syncWithRemote();
};
const onThumbnailClick = (index: number) => () => {
setOptions({
@ -172,16 +196,18 @@ export default function Gallery() {
index,
});
setOpen(true);
}
};
const getThumbnail = (file: file[], index: number) => {
return (<PreviewCard
key={`tile-${file[index].id}`}
data={file[index]}
updateUrl={updateUrl(file[index].dataIndex)}
onClick={onThumbnailClick(index)}
/>);
}
return (
<PreviewCard
key={`tile-${file[index].id}`}
data={file[index]}
updateUrl={updateUrl(file[index].dataIndex)}
onClick={onThumbnailClick(index)}
/>
);
};
const getSlideData = async (instance: any, index: number, item: file) => {
const token = getData(LS_KEYS.USER).token;
@ -205,7 +231,7 @@ export default function Gallery() {
fetching[item.dataIndex] = true;
const url = await getFile(token, item);
updateSrcUrl(item.dataIndex, url);
if (item.metadata.fileType === 1) {
if (item.metadata.fileType === FILE_TYPE.VIDEO) {
item.html = `
<video width="320" height="240" controls>
<source src="${url}" />
@ -225,43 +251,58 @@ export default function Gallery() {
// ignore
}
}
}
};
const selectCollection = (id?: string) => {
const href = `/gallery?collection=${id || ''}`;
router.push(href, undefined, { shallow: true });
}
};
const idSet = new Set();
const filteredData = data.map((item, index) => ({
...item,
dataIndex: index,
})).filter(item => {
if (!idSet.has(item.id)) {
if (!router.query.collection || router.query.collection === item.collectionID.toString()) {
idSet.add(item.id);
return true;
let idSet = new Set();
const filteredData = data
.map((item, index) => ({
...item,
dataIndex: index,
}))
.filter((item) => {
if (!idSet.has(item.id)) {
if (
!router.query.collection ||
router.query.collection === item.collectionID.toString()
) {
idSet.add(item.id);
return true;
}
return false;
}
return false;
}
return false;
});
});
const isSameDay = (first, second) => {
return first.getFullYear() === second.getFullYear() &&
return (
first.getFullYear() === second.getFullYear() &&
first.getMonth() === second.getMonth() &&
first.getDate() === second.getDate();
}
first.getDate() === second.getDate()
);
};
return (<>
<Collections
collections={collections}
selected={router.query.collection?.toString()}
selectCollection={selectCollection}
/>
{
filteredData.length
? <Container>
return (
<>
<Collections
collections={collections}
selected={router.query.collection?.toString()}
selectCollection={selectCollection}
/>
<Upload
uploadModalView={props.uploadModalView}
closeUploadModal={props.closeUploadModal}
showUploadModal={props.showUploadModal}
collectionLatestFile={collectionLatestFile}
refetchData={syncWithRemote}
/>
{filteredData.length ? (
<Container>
<AutoSizer>
{({ height, width }) => {
let columns;
@ -279,13 +320,18 @@ export default function Gallery() {
let listItemIndex = 0;
let currentDate = -1;
filteredData.forEach((item, index) => {
if (!isSameDay(new Date(item.metadata.creationTime/1000), new Date(currentDate))) {
currentDate = item.metadata.creationTime/1000;
if (
!isSameDay(
new Date(item.metadata.creationTime / 1000),
new Date(currentDate)
)
) {
currentDate = item.metadata.creationTime / 1000;
const dateTimeFormat = new Intl.DateTimeFormat('en-IN', {
weekday: 'short',
year: 'numeric',
month: 'long',
day: 'numeric'
day: 'numeric',
});
timeStampList.push({
itemType: ITEM_TYPE.TIME,
@ -307,34 +353,46 @@ export default function Gallery() {
itemType: ITEM_TYPE.TILE,
items: [item],
itemStartIndex: index,
})
});
}
}
});
return (
<List
itemSize={(index) => timeStampList[index].itemType === ITEM_TYPE.TIME ? 30 : 200}
itemSize={(index) =>
timeStampList[index].itemType === ITEM_TYPE.TIME
? 30
: 200
}
height={height}
width={width}
itemCount={timeStampList.length}
key={`${router.query.collection}-${columns}`}
>
{({ index, style }) => {
return (<ListItem style={style}>
<ListContainer>
{
timeStampList[index].itemType === ITEM_TYPE.TIME
? <DateContainer>{timeStampList[index].date}</DateContainer>
: timeStampList[index].items.map((item, idx) =>{
return getThumbnail(filteredData, timeStampList[index].itemStartIndex + idx);
})
}
</ListContainer>
</ListItem>);
return (
<ListItem style={style}>
<ListContainer>
{timeStampList[index].itemType ===
ITEM_TYPE.TIME ? (
<DateContainer>
{timeStampList[index].date}
</DateContainer>
) : (
timeStampList[index].items.map((item, idx) => {
return getThumbnail(
filteredData,
timeStampList[index].itemStartIndex + idx
);
})
)}
</ListContainer>
</ListItem>
);
}}
</List>
)
);
}}
</AutoSizer>
<PhotoSwipe
@ -343,12 +401,15 @@ export default function Gallery() {
options={options}
onClose={handleClose}
gettingData={getSlideData}
favItemIds={favItemIds}
setFavItemIds={setFavItemIds}
/>
</Container>
: <DeadCenter>
<SadFace height={100} width={100} />
<div>No content found!</div>
</DeadCenter>
}
</>);
) : (
<DeadCenter>
<div>{constants.NOTHING_HERE}</div>
</DeadCenter>
)}
</>
);
}

View file

@ -12,6 +12,7 @@ import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import { useRouter } from 'next/router';
import { getKey, SESSION_KEYS, setKey } from 'utils/storage/sessionStorage';
import * as Comlink from "comlink";
import { keyEncryptionResult } from 'services/uploadService';
const CryptoWorker: any = typeof window !== 'undefined'
&& Comlink.wrap(new Worker("worker/crypto.worker.js", { type: 'module' }));
@ -51,30 +52,30 @@ export default function Generate() {
const { passphrase, confirm } = values;
if (passphrase === confirm) {
const cryptoWorker = await new CryptoWorker();
const key = await cryptoWorker.generateMasterKey();
const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
const kek = await cryptoWorker.deriveKey(
await cryptoWorker.fromString(passphrase), kekSalt);
const kekHash = await cryptoWorker.hash(kek);
const encryptedKeyAttributes = await cryptoWorker.encrypt(key, kek);
const key: string = await cryptoWorker.generateMasterKey();
const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey();
const kek: string = await cryptoWorker.deriveKey(passphrase, kekSalt);
const kekHash: string = await cryptoWorker.hash(kek);
const encryptedKeyAttributes: keyEncryptionResult = await cryptoWorker.encryptToB64(key, kek);
const keyPair = await cryptoWorker.generateKeyPair();
const encryptedKeyPairAttributes = await cryptoWorker.encrypt(keyPair.privateKey, key);
const encryptedKeyPairAttributes: keyEncryptionResult = await cryptoWorker.encryptToB64(keyPair.privateKey, key);
const keyAttributes = {
kekSalt: await cryptoWorker.toB64(kekSalt),
kekSalt,
kekHash: kekHash,
encryptedKey: await cryptoWorker.toB64(encryptedKeyAttributes.encryptedData),
keyDecryptionNonce: await cryptoWorker.toB64(encryptedKeyAttributes.nonce),
publicKey: await cryptoWorker.toB64(keyPair.publicKey),
encryptedSecretKey: await cryptoWorker.toB64(encryptedKeyPairAttributes.encryptedData),
secretKeyDecryptionNonce: await cryptoWorker.toB64(encryptedKeyPairAttributes.nonce)
encryptedKey: encryptedKeyAttributes.encryptedData,
keyDecryptionNonce: encryptedKeyAttributes.nonce,
publicKey: keyPair.publicKey,
encryptedSecretKey: encryptedKeyPairAttributes.encryptedData,
secretKeyDecryptionNonce: encryptedKeyPairAttributes.nonce
};
await putAttributes(token, getData(LS_KEYS.USER).name, keyAttributes);
setData(LS_KEYS.KEY_ATTRIBUTES, keyAttributes);
const sessionKeyAttributes = await cryptoWorker.encrypt(key);
const sessionKey = await cryptoWorker.toB64(sessionKeyAttributes.key);
const sessionNonce = await cryptoWorker.toB64(sessionKeyAttributes.nonce);
const encryptionKey = await cryptoWorker.toB64(sessionKeyAttributes.encryptedData);
const sessionKeyAttributes = await cryptoWorker.encryptToB64(key);
const sessionKey = sessionKeyAttributes.key;
const sessionNonce = sessionKeyAttributes.nonce;
const encryptionKey = sessionKeyAttributes.encryptedData;
setKey(SESSION_KEYS.ENCRYPTION_KEY, { encryptionKey });
setData(LS_KEYS.SESSION, { sessionKey, sessionNonce });
router.push('/gallery');

View file

@ -0,0 +1,235 @@
import { getEndpoint } from "utils/common/apiUtil";
import { getData, LS_KEYS } from "utils/storage/localStorage";
import { file, user, getFiles } from "./fileService";
import localForage from 'localforage';
import HTTPService from "./HTTPService";
import * as Comlink from 'comlink';
import { keyEncryptionResult } from "./uploadService";
import { getActualKey, getToken } from "utils/common/key";
const CryptoWorker: any =
typeof window !== 'undefined' &&
Comlink.wrap(new Worker('worker/crypto.worker.js', { type: 'module' }));
const ENDPOINT = getEndpoint();
enum CollectionType {
folder = "folder",
favorites = "favorites",
album = "album",
}
export interface collection {
id: string;
owner: user;
key?: string;
name?: string;
encryptedName?: string;
nameDecryptionNonce?: string;
type: string;
attributes: collectionAttributes
sharees: user[];
updationTime: number;
encryptedKey: string;
keyDecryptionNonce: string;
isDeleted: boolean;
}
interface collectionAttributes {
encryptedPath?: string;
pathDecryptionNonce?: string
};
export interface collectionLatestFile {
collection: collection
file: file;
}
const getCollectionSecrets = async (collection: collection, masterKey: string) => {
const worker = await new CryptoWorker();
const userID = getData(LS_KEYS.USER).id;
let decryptedKey: string;
if (collection.owner.id == userID) {
decryptedKey = await worker.decryptB64(
collection.encryptedKey,
collection.keyDecryptionNonce,
masterKey
);
} else {
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
const secretKey = await worker.decryptB64(
keyAttributes.encryptedSecretKey,
keyAttributes.secretKeyDecryptionNonce,
masterKey
);
decryptedKey = await worker.boxSealOpen(
collection.encryptedKey,
keyAttributes.publicKey,
secretKey
);
}
collection.name = collection.name || await worker.decryptString(
collection.encryptedName,
collection.nameDecryptionNonce,
decryptedKey);
return {
...collection,
key: decryptedKey,
};
};
const getCollections = async (
token: string,
sinceTime: string,
key: string
): Promise<collection[]> => {
try {
const resp = await HTTPService.get(`${ENDPOINT}/collections`, {
sinceTime: sinceTime,
}, { 'X-Auth-Token': token, });
const promises: Promise<collection>[] = resp.data.collections.map(
(collection: collection) => getCollectionSecrets(collection, key)
);
return await Promise.all(promises);
}
catch (e) {
console.log("getCollections falied- " + e);
}
};
export const fetchCollections = async (token: string, key: string) => {
const collections = await getCollections(token, '0', key);
const favCollection = collections.filter(collection => collection.type === CollectionType.favorites);
await localForage.setItem('fav-collection', favCollection);
return collections;
};
export const getCollectionLatestFile = (
collections: collection[],
files: file[]
): collectionLatestFile[] => {
const latestFile = new Map<number, file>();
const collectionMap = new Map<number, collection>();
collections.forEach(collection => collectionMap.set(Number(collection.id), collection));
files.forEach(file => {
if (!latestFile.has(file.collectionID)) {
latestFile.set(file.collectionID, file)
}
});
let allCollectionLatestFile: collectionLatestFile[] = [];
for (const [collectionID, file] of latestFile) {
allCollectionLatestFile.push({ collection: collectionMap.get(collectionID), file });
}
return allCollectionLatestFile;
}
export const getFavItemIds = async (files: file[]): Promise<Set<number>> => {
let favCollection: collection = (await localForage.getItem<collection>('fav-collection'))[0];
if (!favCollection)
return new Set();
return new Set(files.filter(file => file.collectionID === Number(favCollection.id)).map((file): number => file.id));
}
export const createAlbum = async (albumName: string) => {
return AddCollection(albumName, CollectionType.album);
}
export const AddCollection = async (collectionName: string, type: CollectionType) => {
const worker = await new CryptoWorker();
const encryptionKey = await getActualKey();
const token = getToken();
const collectionKey: string = await worker.generateMasterKey();
const { encryptedData: encryptedKey, nonce: keyDecryptionNonce }: keyEncryptionResult = await worker.encryptToB64(collectionKey, encryptionKey);
const { encryptedData: encryptedName, nonce: nameDecryptionNonce }: keyEncryptionResult = await worker.encryptToB64(collectionName, collectionKey);
const newCollection: collection = {
id: null,
owner: null,
encryptedKey,
keyDecryptionNonce,
encryptedName,
nameDecryptionNonce,
type,
attributes: {},
sharees: null,
updationTime: null,
isDeleted: false
};
let createdCollection: collection = await createCollection(newCollection, token);
createdCollection = await getCollectionSecrets(createdCollection, encryptionKey);
return createdCollection;
}
const createCollection = async (collectionData: collection, token: string): Promise<collection> => {
try {
const response = await HTTPService.post(`${ENDPOINT}/collections`, collectionData, null, { 'X-Auth-Token': token });
return response.data.collection;
} catch (e) {
console.log("create Collection failed " + e);
}
}
export const addToFavorites = async (file: file) => {
let favCollection: collection = (await localForage.getItem<collection>('fav-collection'))[0];
if (!favCollection) {
favCollection = await AddCollection("Favorites", CollectionType.favorites);
await localForage.setItem('fav-collection', favCollection);
}
await addtoCollection(favCollection, [file])
}
export const removeFromFavorites = async (file: file) => {
let favCollection: collection = (await localForage.getItem<collection>('fav-collection'))[0];
await removeFromCollection(favCollection, [file])
}
const addtoCollection = async (collection: collection, files: file[]) => {
try {
const params = new Object();
const worker = await new CryptoWorker();
const token = getToken();
params["collectionID"] = collection.id;
await Promise.all(files.map(async file => {
file.collectionID = Number(collection.id);
const newEncryptedKey: keyEncryptionResult = await worker.encryptToB64(file.key, collection.key);
file.encryptedKey = newEncryptedKey.encryptedData;
file.keyDecryptionNonce = newEncryptedKey.nonce;
if (params["files"] == undefined) {
params["files"] = [];
}
params["files"].push({
id: file.id,
encryptedKey: file.encryptedKey,
keyDecryptionNonce: file.keyDecryptionNonce
})
return file;
}));
await HTTPService.post(`${ENDPOINT}/collections/add-files`, params, null, { 'X-Auth-Token': token });
} catch (e) {
console.log("Add to collection Failed " + e);
}
}
const removeFromCollection = async (collection: collection, files: file[]) => {
try {
const params = new Object();
const token = getToken();
params["collectionID"] = collection.id;
await Promise.all(files.map(async file => {
if (params["fileIDs"] == undefined) {
params["fileIDs"] = [];
}
params["fileIDs"].push(file.id);
}));
await HTTPService.post(`${ENDPOINT}/collections/remove-files`, params, null, { 'X-Auth-Token': token });
} catch (e) {
console.log("remove from collection failed " + e);
}
}

View file

@ -1,11 +1,12 @@
import { getEndpoint } from "utils/common/apiUtil";
import HTTPService from "./HTTPService";
import * as Comlink from "comlink";
import { getData, LS_KEYS } from "utils/storage/localStorage";
import localForage from "localforage";
import { getEndpoint } from 'utils/common/apiUtil';
import HTTPService from './HTTPService';
import * as Comlink from 'comlink';
import localForage from 'localforage';
import { collection } from './collectionService';
const CryptoWorker: any = typeof window !== 'undefined'
&& Comlink.wrap(new Worker("worker/crypto.worker.js", { type: 'module' }));
const CryptoWorker: any =
typeof window !== 'undefined' &&
Comlink.wrap(new Worker('worker/crypto.worker.js', { type: 'module' }));
const ENDPOINT = getEndpoint();
localForage.config({
@ -16,11 +17,11 @@ localForage.config({
});
export interface fileAttribute {
encryptedData: string;
encryptedData: Uint8Array | string;
decryptionHeader: string;
creationTime: number;
fileType: number;
};
}
export interface user {
id: number;
@ -28,17 +29,6 @@ export interface user {
email: string;
}
export interface collection {
id: string;
owner: user;
key: string;
name: string;
type: string;
creationTime: number;
encryptedKey: string;
keyDecryptionNonce: string;
isDeleted: boolean;
}
export interface file {
id: number;
@ -48,7 +38,7 @@ export interface file {
metadata: fileAttribute;
encryptedKey: string;
keyDecryptionNonce: string;
key: Uint8Array;
key: string;
src: string;
msrc: string;
html: string;
@ -56,119 +46,149 @@ export interface file {
h: number;
isDeleted: boolean;
dataIndex: number;
};
const getCollectionKey = async (collection: collection, key: Uint8Array) => {
const worker = await new CryptoWorker();
const userID = getData(LS_KEYS.USER).id;
var decryptedKey;
if (collection.owner.id == userID) {
decryptedKey = await worker.decrypt(
await worker.fromB64(collection.encryptedKey),
await worker.fromB64(collection.keyDecryptionNonce),
key);
} else {
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
const secretKey = await worker.decrypt(
await worker.fromB64(keyAttributes.encryptedSecretKey),
await worker.fromB64(keyAttributes.secretKeyDecryptionNonce),
key);
decryptedKey = await worker.boxSealOpen(
await worker.fromB64(collection.encryptedKey),
await worker.fromB64(keyAttributes.publicKey),
secretKey);
}
return {
...collection,
key: decryptedKey
};
updationTime: number;
}
const getCollections = async (token: string, sinceTime: string, key: Uint8Array): Promise<collection[]> => {
const resp = await HTTPService.get(`${ENDPOINT}/collections`, {
'token': token,
'sinceTime': sinceTime,
});
const promises: Promise<collection>[] = resp.data.collections.map(
(collection: collection) => getCollectionKey(collection, key));
return await Promise.all(promises);
export const fetchData = async (token, collections) => {
const resp = await fetchFiles(
token,
collections
);
return (
resp.map((item) => ({
...item,
w: window.innerWidth,
h: window.innerHeight,
}))
);
}
export const fetchCollections = async (token: string, key: string) => {
const worker = await new CryptoWorker();
return getCollections(token, "0", await worker.fromB64(key));
}
export const fetchFiles = async (
token: string,
collections: collection[]
) => {
let files: Array<file> = (await localForage.getItem<file[]>('files')) || [];
const fetchedFiles = await getFiles(collections, null, "100", token);
export const getFiles = async (sinceTime: string, token: string, limit: string, key: string, collections: collection[]) => {
const worker = await new CryptoWorker();
let files: Array<file> = await localForage.getItem<file[]>('files') || [];
for (const index in collections) {
const collection = collections[index];
if (collection.isDeleted) {
// TODO: Remove files in this collection from localForage and cache
continue;
files.push(...fetchedFiles);
var latestFiles = new Map<string, file>();
files.forEach((file) => {
let uid = `${file.collectionID}-${file.id}`;
if (!latestFiles.has(uid) || latestFiles.get(uid).updationTime < file.updationTime) {
latestFiles.set(uid, file);
}
let time = await localForage.getItem<string>(`${collection.id}-time`) || sinceTime;
let resp;
do {
resp = await HTTPService.get(`${ENDPOINT}/collections/diff`, {
'collectionID': collection.id, sinceTime: time, token, limit,
});
const promises: Promise<file>[] = resp.data.diff.map(
async (file: file) => {
file.key = await worker.decrypt(
await worker.fromB64(file.encryptedKey),
await worker.fromB64(file.keyDecryptionNonce),
collection.key);
file.metadata = await worker.decryptMetadata(file);
return file;
});
files.push(...await Promise.all(promises));
files = files.sort((a, b) => b.metadata.creationTime - a.metadata.creationTime);
if (resp.data.diff.length) {
time = (resp.data.diff.slice(-1)[0].updationTime).toString();
}
} while (resp.data.diff.length);
await localForage.setItem(`${collection.id}-time`, time);
});
files = [];
for (const [_, file] of latestFiles.entries()) {
if (!file.isDeleted)
files.push(file);
}
files = files.filter(item => !item.isDeleted)
files = files.sort(
(a, b) => b.metadata.creationTime - a.metadata.creationTime
);
await localForage.setItem('files', files);
return files;
}
};
export const getPreview = async (token: string, file: file) => {
const cache = await caches.open('thumbs');
const cacheResp: Response = await cache.match(file.id.toString());
if (cacheResp) {
return URL.createObjectURL(await cacheResp.blob());
}
const resp = await HTTPService.get(
`${ENDPOINT}/files/preview/${file.id}`,
{ token }, null, { responseType: 'arraybuffer' },
);
const worker = await new CryptoWorker();
const decrypted: any = await worker.decryptThumbnail(
new Uint8Array(resp.data),
await worker.fromB64(file.thumbnail.decryptionHeader),
file.key);
export const getFiles = async (collections: collection[], sinceTime: string, limit: string, token: string): Promise<file[]> => {
try {
await cache.put(file.id.toString(), new Response(new Blob([decrypted])));
const worker = await new CryptoWorker();
let promises: Promise<file>[] = [];
for (const index in collections) {
const collection = collections[index];
if (collection.isDeleted) {
// TODO: Remove files in this collection from localForage and cache
continue;
}
let time =
sinceTime || (await localForage.getItem<string>(`${collection.id}-time`)) || "0";
let resp;
do {
resp = await HTTPService.get(`${ENDPOINT}/collections/diff`, {
collectionID: collection.id,
sinceTime: time,
limit,
},
{
'X-Auth-Token': token
});
promises.push(...resp.data.diff.map(
async (file: file) => {
if (!file.isDeleted) {
file.key = await worker.decryptB64(
file.encryptedKey,
file.keyDecryptionNonce,
collection.key
);
file.metadata = await worker.decryptMetadata(file);
}
return file;
}
));
if (resp.data.diff.length) {
time = resp.data.diff.slice(-1)[0].updationTime.toString();
}
} while (resp.data.diff.length);
await localForage.setItem(`${collection.id}-time`, time);
}
return Promise.all(promises);
} catch (e) {
// TODO: handle storage full exception.
console.log("Get files failed" + e);
}
return URL.createObjectURL(new Blob([decrypted]));
}
export const getPreview = async (token: string, file: file) => {
try {
const cache = await caches.open('thumbs');
const cacheResp: Response = await cache.match(file.id.toString());
if (cacheResp) {
return URL.createObjectURL(await cacheResp.blob());
}
const resp = await HTTPService.get(
`${ENDPOINT}/files/preview/${file.id}`,
null,
{ 'X-Auth-Token': token },
{ responseType: 'arraybuffer' }
);
const worker = await new CryptoWorker();
const decrypted: any = await worker.decryptThumbnail(
new Uint8Array(resp.data),
await worker.fromB64(file.thumbnail.decryptionHeader),
file.key
);
try {
await cache.put(file.id.toString(), new Response(new Blob([decrypted])));
} catch (e) {
// TODO: handle storage full exception.
}
return URL.createObjectURL(new Blob([decrypted]));
} catch (e) {
console.log("get preview Failed" + e);
}
};
export const getFile = async (token: string, file: file) => {
const resp = await HTTPService.get(
`${ENDPOINT}/files/download/${file.id}`,
{ token }, null, { responseType: 'arraybuffer' },
);
const worker = await new CryptoWorker();
const decrypted: any = await worker.decryptFile(
new Uint8Array(resp.data),
await worker.fromB64(file.file.decryptionHeader),
file.key);
return URL.createObjectURL(new Blob([decrypted]));
}
try {
const resp = await HTTPService.get(
`${ENDPOINT}/files/download/${file.id}`,
null,
{ 'X-Auth-Token': token },
{ responseType: 'arraybuffer' }
);
const worker = await new CryptoWorker();
const decrypted: any = await worker.decryptFile(
new Uint8Array(resp.data),
await worker.fromB64(file.file.decryptionHeader),
file.key
);
return URL.createObjectURL(new Blob([decrypted]));
}
catch (e) {
console.log("get file failed " + e);
}
};

View file

@ -0,0 +1,495 @@
import { getEndpoint } from 'utils/common/apiUtil';
import HTTPService from './HTTPService';
import * as Comlink from 'comlink';
import EXIF from "exif-js";
import { fileAttribute } from './fileService';
import { collection, collectionLatestFile } from "./collectionService"
import { FILE_TYPE } from 'pages/gallery';
const CryptoWorker: any =
typeof window !== 'undefined' &&
Comlink.wrap(new Worker('worker/crypto.worker.js', { type: 'module' }));
const ENDPOINT = getEndpoint();
interface encryptionResult {
file: fileAttribute,
key: string
}
export interface keyEncryptionResult {
encryptedData: string,
key: string,
nonce: string,
}
interface uploadURL {
url: string,
objectKey: string
}
interface FileinMemory {
filedata: Uint8Array,
thumbnail: Uint8Array,
filename: string
}
interface encryptedFile {
filedata: fileAttribute;
thumbnail: fileAttribute;
fileKey: keyEncryptionResult;
}
interface objectKey {
objectKey: string,
decryptionHeader: string
}
interface objectKeys {
file: objectKey
thumbnail: objectKey
}
interface uploadFile extends objectKeys {
collectionID: string,
encryptedKey: string;
keyDecryptionNonce: string;
metadata?: {
encryptedData: string | Uint8Array,
decryptionHeader: string
}
}
interface UploadFileWithoutMetaData {
tempUploadFile: uploadFile,
encryptedFileKey: keyEncryptionResult,
fileName: string
}
export enum UPLOAD_STAGES {
START,
ENCRYPTION,
UPLOAD,
FINISH
}
class UploadService {
private uploadURLs: uploadURL[];
private uploadURLFetchInProgress: Promise<any>;
private perStepProgress: number
private stepsCompleted: number
private totalFilesCount: number
private metadataMap: Map<string, Object>;
public async uploadFiles(recievedFiles: File[], collectionLatestFile: collectionLatestFile, token: string, progressBarProps) {
try {
const worker = await new CryptoWorker();
this.stepsCompleted = 0;
this.metadataMap = new Map<string, object>();
this.uploadURLs = [];
this.uploadURLFetchInProgress = null;
let metadataFiles: File[] = [];
let actualFiles: File[] = [];
recievedFiles.forEach(file => {
if (file.type.substr(0, 5) === "image" || file.type.substr(0, 5) === "video")
actualFiles.push(file);
if (file.name.slice(-4) == "json")
metadataFiles.push(file);
});
this.totalFilesCount = actualFiles.length;
this.perStepProgress = 100 / (3 * actualFiles.length);
progressBarProps.setUploadStage(UPLOAD_STAGES.START);
this.changeProgressBarProps(progressBarProps);
const uploadFilesWithoutMetaData: UploadFileWithoutMetaData[] = [];
while (actualFiles.length > 0) {
var promises = [];
for (var i = 0; i < 5 && actualFiles.length > 0; i++)
promises.push(this.uploadHelper(progressBarProps, actualFiles.pop(), collectionLatestFile.collection, token));
uploadFilesWithoutMetaData.push(...await Promise.all(promises));
}
for await (const rawFile of metadataFiles) {
await this.updateMetadata(rawFile)
};
progressBarProps.setUploadStage(UPLOAD_STAGES.ENCRYPTION);
const completeUploadFiles: uploadFile[] = await Promise.all(uploadFilesWithoutMetaData.map(async (file: UploadFileWithoutMetaData) => {
const { file: encryptedMetaData } = await this.encryptMetadata(worker, file.fileName, file.encryptedFileKey);
const completeUploadFile = {
...file.tempUploadFile,
metadata: {
encryptedData: encryptedMetaData.encryptedData,
decryptionHeader: encryptedMetaData.decryptionHeader
}
}
this.changeProgressBarProps(progressBarProps);
return completeUploadFile;
}));
progressBarProps.setUploadStage(UPLOAD_STAGES.UPLOAD);
await Promise.all(completeUploadFiles.map(async (uploadFile: uploadFile) => {
await this.uploadFile(uploadFile, token);
this.changeProgressBarProps(progressBarProps);
}));
progressBarProps.setUploadStage(UPLOAD_STAGES.FINISH);
progressBarProps.setPercentComplete(100);
} catch (e) {
console.log(e);
}
}
private async uploadHelper(progressBarProps, rawFile, collection, token) {
try {
const worker = await new CryptoWorker();
let file: FileinMemory = await this.readFile(rawFile);
let encryptedFile: encryptedFile = await this.encryptFile(worker, file, collection.key);
let objectKeys = await this.uploadtoBucket(encryptedFile, token, 2 * this.totalFilesCount);
let uploadFileWithoutMetaData: uploadFile = this.getuploadFile(collection, encryptedFile.fileKey, objectKeys);
this.changeProgressBarProps(progressBarProps);
return {
tempUploadFile: uploadFileWithoutMetaData,
encryptedFileKey: encryptedFile.fileKey,
fileName: file.filename
};
}
catch (e) {
console.log(e);
}
}
private changeProgressBarProps({ setPercentComplete, setFileCounter }) {
this.stepsCompleted++;
const fileCompleted = this.stepsCompleted % this.totalFilesCount;
setFileCounter({ current: fileCompleted, total: this.totalFilesCount });
setPercentComplete(this.perStepProgress * this.stepsCompleted);
}
private async readFile(recievedFile: File) {
try {
const filedata: Uint8Array = await this.getUint8ArrayView(recievedFile);
let fileType;
switch (recievedFile.type.split('/')[0]) {
case "image":
fileType = FILE_TYPE.IMAGE;
break;
case "video":
fileType = FILE_TYPE.VIDEO;
break;
default:
fileType = FILE_TYPE.OTHERS;
}
const { location, creationTime } = await this.getExifData(recievedFile);
this.metadataMap.set(recievedFile.name, {
title: recievedFile.name,
creationTime: creationTime || (recievedFile.lastModified) * 1000,
modificationTime: (recievedFile.lastModified) * 1000,
latitude: location?.latitude,
longitude: location?.latitude,
fileType,
});
return {
filedata,
filename: recievedFile.name,
thumbnail: await this.generateThumbnail(recievedFile)
}
} catch (e) {
console.log("error reading files " + e);
}
}
private async encryptFile(worker, file: FileinMemory, encryptionKey: string): Promise<encryptedFile> {
try {
const { key: fileKey, file: encryptedFiledata }: encryptionResult = await worker.encryptFile(file.filedata);
const { file: encryptedThumbnail }: encryptionResult = await worker.encryptThumbnail(file.thumbnail, fileKey);
const encryptedKey: keyEncryptionResult = await worker.encryptToB64(fileKey, encryptionKey);
const result: encryptedFile = {
filedata: encryptedFiledata,
thumbnail: encryptedThumbnail,
fileKey: encryptedKey
};
return result;
}
catch (e) {
console.log("Error encrypting files " + e);
}
}
private async encryptMetadata(worker: any, fileName: string, encryptedFileKey: keyEncryptionResult) {
const metaData = this.metadataMap.get(fileName);
const fileKey = await worker.decryptB64(encryptedFileKey.encryptedData, encryptedFileKey.nonce, encryptedFileKey.key);
const encryptedMetaData = await worker.encryptMetadata(metaData, fileKey);
return encryptedMetaData;
}
private async uploadtoBucket(file: encryptedFile, token, count: number): Promise<objectKeys> {
try {
const fileUploadURL = await this.getUploadURL(token, count);
const fileObjectKey = await this.putFile(fileUploadURL, file.filedata.encryptedData)
const thumbnailUploadURL = await this.getUploadURL(token, count);
const thumbnailObjectKey = await this.putFile(thumbnailUploadURL, file.thumbnail.encryptedData)
return {
file: { objectKey: fileObjectKey, decryptionHeader: file.filedata.decryptionHeader },
thumbnail: { objectKey: thumbnailObjectKey, decryptionHeader: file.thumbnail.decryptionHeader }
};
} catch (e) {
console.log("error uploading to bucket " + e);
}
}
private getuploadFile(collection: collection, encryptedKey: keyEncryptionResult, objectKeys: objectKeys): uploadFile {
const uploadFile: uploadFile = {
collectionID: collection.id,
encryptedKey: encryptedKey.encryptedData,
keyDecryptionNonce: encryptedKey.nonce,
...objectKeys
}
return uploadFile;
}
private async uploadFile(uploadFile: uploadFile, token) {
try {
const response = await HTTPService.post(`${ENDPOINT}/files`, uploadFile, null, { 'X-Auth-Token': token });
return response.data;
} catch (e) {
console.log("upload Files Failed " + e);
}
}
private async updateMetadata(recievedFile: File) {
try {
const metadataJSON: object = await new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
var result = typeof reader.result !== "string" ? new TextDecoder().decode(reader.result) : reader.result
resolve(JSON.parse(result));
}
reader.readAsText(recievedFile)
});
if (!this.metadataMap.has(metadataJSON['title']))
return;
const metaDataObject = this.metadataMap.get(metadataJSON['title']);
metaDataObject['creationTime'] = metadataJSON['photoTakenTime']['timestamp'] * 1000000;
metaDataObject['modificationTime'] = metadataJSON['modificationTime']['timestamp'] * 1000000;
if (metaDataObject['latitude'] == null || (metaDataObject['latitude'] == 0.0 && metaDataObject['longitude'] == 0.0)) {
var locationData = null;
if (metadataJSON['geoData']['latitude'] != 0.0 || metadataJSON['geoData']['longitude'] != 0.0) {
locationData = metadataJSON['geoData'];
}
else if (metadataJSON['geoDataExif']['latitude'] != 0.0 || metadataJSON['geoDataExif']['longitude'] != 0.0) {
locationData = metadataJSON['geoDataExif'];
}
if (locationData != null) {
metaDataObject['latitude'] = locationData['latitide'];
metaDataObject['longitude'] = locationData['longitude'];
}
}
} catch (e) {
console.log("error reading metaData Files " + e);
}
}
private async generateThumbnail(file: File): Promise<Uint8Array> {
try {
let canvas = document.createElement("canvas");
let canvas_CTX = canvas.getContext("2d");
let imageURL = null;
if (file.type.match("image")) {
let image = new Image();
imageURL = URL.createObjectURL(file);
image.setAttribute("src", imageURL);
await new Promise((resolve) => {
image.onload = () => {
canvas.width = image.width;
canvas.height = image.height;
canvas_CTX.drawImage(image, 0, 0, image.width, image.height);
image = undefined;
resolve(null);
}
});
}
else {
await new Promise(async (resolve) => {
let video = document.createElement('video');
imageURL = URL.createObjectURL(file);
var timeupdate = function () {
if (snapImage()) {
video.removeEventListener('timeupdate', timeupdate);
video.pause();
resolve(null);
}
};
video.addEventListener('loadeddata', function () {
if (snapImage()) {
video.removeEventListener('timeupdate', timeupdate);
resolve(null);
}
});
var snapImage = function () {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas_CTX.drawImage(video, 0, 0, canvas.width, canvas.height);
var image = canvas.toDataURL();
var success = image.length > 100000;
return success;
};
video.addEventListener('timeupdate', timeupdate);
video.preload = 'metadata';
video.src = imageURL;
// Load video in Safari / IE11
video.muted = true;
video.playsInline = true;
video.play();
});
}
URL.revokeObjectURL(imageURL);
var thumbnailBlob = await new Promise(resolve => {
canvas.toBlob(function (blob) {
resolve(blob);
}), 'image/jpeg', 0.4
});
console.log(URL.createObjectURL(thumbnailBlob));
const thumbnail = this.getUint8ArrayView(thumbnailBlob);
return thumbnail;
} catch (e) {
console.log("Error generatin thumbnail " + e);
}
}
private async getUint8ArrayView(file): Promise<Uint8Array> {
try {
return await new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onabort = () => reject('file reading was aborted')
reader.onerror = () => reject('file reading has failed')
reader.onload = () => {
// Do whatever you want with the file contents
const result = typeof reader.result === "string" ? new TextEncoder().encode(reader.result) : new Uint8Array(reader.result);
resolve(result);
}
reader.readAsArrayBuffer(file)
});
} catch (e) {
console.log("error readinf file to bytearray " + e);
throw e;
}
}
private async getUploadURL(token: string, count: number) {
if (this.uploadURLs.length == 0) {
await this.fetchUploadURLs(token, count);
}
return this.uploadURLs.pop();
}
private async fetchUploadURLs(token: string, count: number): Promise<void> {
try {
if (!this.uploadURLFetchInProgress) {
this.uploadURLFetchInProgress = HTTPService.get(`${ENDPOINT}/files/upload-urls`,
{
count: Math.min(50, count).toString() //m4gic number
}, { 'X-Auth-Token': token })
const response = await this.uploadURLFetchInProgress;
this.uploadURLFetchInProgress = null;
this.uploadURLs.push(...response.data["urls"]);
}
return this.uploadURLFetchInProgress;
} catch (e) {
console.log("fetch upload-url failed " + e);
throw e;
}
}
private async putFile(fileUploadURL: uploadURL, file: Uint8Array | string): Promise<string> {
try {
const fileSize = file.length.toString();
await HTTPService.put(fileUploadURL.url, file, null, { contentLengthHeader: fileSize })
return fileUploadURL.objectKey;
} catch (e) {
console.log('putFile to dataStore failed ' + e);
throw e;
}
}
private async getExifData(recievedFile) {
try {
const exifData: any = await new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
resolve(EXIF.readFromBinaryFile(reader.result));
}
reader.readAsArrayBuffer(recievedFile)
});
if (!exifData)
return { location: null, creationTime: null };
return {
location: this.getLocation(exifData),
creationTime: this.getUNIXTime(exifData)
};
} catch (e) {
console.log("error reading exif data");
}
}
private getUNIXTime(exifData: any) {
if (!exifData.DateTimeOriginal)
return null;
let dateString: string = exifData.DateTimeOriginal;
var parts = dateString.split(' ')[0].split(":");
var date = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
return date.getTime() * 1000;
}
private getLocation(exifData) {
if (!exifData.GPSLatitude)
return null;
var latDegree = exifData.GPSLatitude[0].numerator;
var latMinute = exifData.GPSLatitude[1].numerator;
var latSecond = exifData.GPSLatitude[2].numerator;
var latDirection = exifData.GPSLatitudeRef;
var latFinal = this.convertDMSToDD(latDegree, latMinute, latSecond, latDirection);
// Calculate longitude decimal
var lonDegree = exifData.GPSLongitude[0].numerator;
var lonMinute = exifData.GPSLongitude[1].numerator;
var lonSecond = exifData.GPSLongitude[2].numerator;
var lonDirection = exifData.GPSLongitudeRef;
var lonFinal = this.convertDMSToDD(lonDegree, lonMinute, lonSecond, lonDirection);
return { latitude: latFinal * 1.0, longitude: lonFinal * 1.0 };
}
private convertDMSToDD(degrees, minutes, seconds, direction) {
var dd = degrees + (minutes / 60) + (seconds / 3600);
if (direction == "S" || direction == "W") {
dd = dd * -1;
}
return dd;
}
}
export default new UploadService();

View file

@ -1,3 +1,5 @@
export const getEndpoint = () => {
return process.env.NEXT_PUBLIC_ENTE_ENDPOINT || "https://api.ente.io";
const endPoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT ?? "https://api.ente.io";
console.log(endPoint);
return endPoint;
}

View file

@ -6,9 +6,15 @@ const CryptoWorker: any = typeof window !== 'undefined'
&& Comlink.wrap(new Worker("worker/crypto.worker.js", { type: 'module' }));
export const getActualKey = async () => {
const cryptoWorker = await new CryptoWorker();
const encryptedKey = getKey(SESSION_KEYS.ENCRYPTION_KEY).encryptionKey;
const session = getData(LS_KEYS.SESSION);
const key = await cryptoWorker.decryptToB64(encryptedKey, session.sessionNonce, session.sessionKey);
if (session == null)
return;
const cryptoWorker = await new CryptoWorker();
const encryptedKey = getKey(SESSION_KEYS.ENCRYPTION_KEY)?.encryptionKey;
const key: string = await cryptoWorker.decryptB64(encryptedKey, session.sessionNonce, session.sessionKey);
return key;
}
export const getToken = () => {
return getData(LS_KEYS.USER)?.token;
}

View file

@ -2,16 +2,16 @@ import sodium from 'libsodium-wrappers';
const encryptionChunkSize = 4 * 1024 * 1024;
export async function decryptChaChaOneShot(data: Uint8Array, header: Uint8Array, key: Uint8Array) {
export async function decryptChaChaOneShot(data: Uint8Array, header: Uint8Array, key: string) {
await sodium.ready;
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(header, key);
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(header, await fromB64(key));
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(pullState, data, null);
return pullResult.message;
}
export async function decryptChaCha(data: Uint8Array, header: Uint8Array, key: Uint8Array) {
export async function decryptChaCha(data: Uint8Array, header: Uint8Array, key: string) {
await sodium.ready;
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(header, key);
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(header, await fromB64(key));
const decryptionChunkSize =
encryptionChunkSize + sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
var bytesRead = 0;
@ -33,41 +33,97 @@ export async function decryptChaCha(data: Uint8Array, header: Uint8Array, key: U
return Uint8Array.from(decryptedData);
}
export async function encryptToB64(data: string, key: string) {
export async function encryptChaChaOneShot(data: Uint8Array, key?: string) {
await sodium.ready;
var bKey: Uint8Array;
if (key == null) {
bKey = sodium.crypto_secretbox_keygen();
} else {
bKey = await fromB64(key)
}
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
const encryptedData = sodium.crypto_secretbox_easy(data, nonce, bKey);
const uintkey: Uint8Array = key ? await fromB64(key) : sodium.crypto_secretstream_xchacha20poly1305_keygen();
let initPushResult = sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
let [pushState, header] = [initPushResult.state, initPushResult.header];
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(pushState, data, null, sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL);
return {
encryptedData: await toB64(encryptedData),
key: await toB64(bKey),
nonce: await toB64(nonce),
key: await toB64(uintkey), file: {
encryptedData: pushResult,
decryptionHeader: await toB64(header),
creationTime: Date.now(),
fileType: 0
}
}
}
export async function decryptToB64(data: string, nonce: string, key: string) {
export async function encryptChaCha(data: Uint8Array, key?: string) {
await sodium.ready;
const uintkey: Uint8Array = key ? await fromB64(key) : sodium.crypto_secretstream_xchacha20poly1305_keygen();
let initPushResult = sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
let [pushState, header] = [initPushResult.state, initPushResult.header];
let bytesRead = 0;
let tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
let encryptedData = [];
while (tag !== sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
let chunkSize = encryptionChunkSize;
if (bytesRead + chunkSize >= data.length) {
chunkSize = data.length - bytesRead;
tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL;
}
const buffer = data.slice(bytesRead, bytesRead + chunkSize);
bytesRead += chunkSize;
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(pushState, buffer, null, tag);
for (var index = 0; index < pushResult.length; index++) {
encryptedData.push(pushResult[index]);
}
}
return {
key: await toB64(uintkey), file: {
encryptedData: new Uint8Array(encryptedData),
decryptionHeader: await toB64(header),
creationTime: Date.now(),
fileType: 0
}
}
}
export async function encryptToB64(data: string, key?: string) {
await sodium.ready;
const encrypted = await encrypt(await fromB64(data), (key ? await fromB64(key) : null));
return {
encryptedData: await toB64(encrypted.encryptedData),
key: await toB64(encrypted.key),
nonce: await toB64(encrypted.nonce),
}
}
export async function decryptB64(data: string, nonce: string, key: string) {
await sodium.ready;
const decrypted = await decrypt(await fromB64(data),
await fromB64(nonce),
await fromB64(key))
await fromB64(key));
return await toB64(decrypted);
}
export async function decryptString(data: string, nonce: string, key: string) {
await sodium.ready;
const decrypted = await decrypt(await fromB64(data),
await fromB64(nonce),
await fromB64(key));
return sodium.to_string(decrypted);
}
export async function encrypt(data: Uint8Array, key?: Uint8Array) {
await sodium.ready;
if (key == null) {
key = sodium.crypto_secretbox_keygen();
}
const uintkey: Uint8Array = key ? key : sodium.crypto_secretbox_keygen();
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
const encryptedData = sodium.crypto_secretbox_easy(data, nonce, key);
const encryptedData = sodium.crypto_secretbox_easy(data, nonce, uintkey);
return {
encryptedData: encryptedData,
key: key,
key: uintkey,
nonce: nonce,
}
}
@ -77,55 +133,65 @@ export async function decrypt(data: Uint8Array, nonce: Uint8Array, key: Uint8Arr
return sodium.crypto_secretbox_open_easy(data, nonce, key);
}
export async function verifyHash(hash: string, input: Uint8Array) {
export async function verifyHash(hash: string, input: string) {
await sodium.ready;
return sodium.crypto_pwhash_str_verify(hash, input);
return sodium.crypto_pwhash_str_verify(hash, await fromB64(input));
}
export async function hash(input: string | Uint8Array) {
export async function hash(input: string) {
await sodium.ready;
return sodium.crypto_pwhash_str(
input,
await fromB64(input),
sodium.crypto_pwhash_OPSLIMIT_SENSITIVE,
sodium.crypto_pwhash_MEMLIMIT_MODERATE,
);
}
export async function deriveKey(passphrase: Uint8Array, salt: Uint8Array) {
export async function deriveKey(passphrase: string, salt: string) {
await sodium.ready;
return sodium.crypto_pwhash(
return await toB64(sodium.crypto_pwhash(
sodium.crypto_secretbox_KEYBYTES,
passphrase,
salt,
await fromString(passphrase),
await fromB64(salt),
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
sodium.crypto_pwhash_ALG_DEFAULT,
);
));
}
export async function generateMasterKey() {
await sodium.ready;
return sodium.crypto_kdf_keygen();
return await toB64(sodium.crypto_kdf_keygen());
}
export async function generateSaltToDeriveKey() {
await sodium.ready;
return sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
return await toB64(sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES));
}
export async function generateKeyPair() {
await sodium.ready;
return sodium.crypto_box_keypair();
const keyPair: sodium.KeyPair = sodium.crypto_box_keypair();
return { privateKey: await toB64(keyPair.privateKey), publicKey: await toB64(keyPair.publicKey) }
}
export async function boxSealOpen(input: Uint8Array, publicKey: Uint8Array, secretKey: Uint8Array) {
export async function boxSealOpen(input: string, publicKey: string, secretKey: string) {
await sodium.ready;
return sodium.crypto_box_seal_open(input, publicKey, secretKey);
return await toB64(sodium.crypto_box_seal_open(await fromB64(input), await fromB64(publicKey), await fromB64(secretKey)));
}
export async function fromB64(input: string) {
await sodium.ready;
return sodium.from_base64(input, sodium.base64_variants.ORIGINAL);
let result;
try {
result = sodium.from_base64(input, sodium.base64_variants.ORIGINAL);
} catch (e) {
result = await fromB64(await toB64(await fromString(input)));
}
finally {
return result;
}
}
export async function toB64(input: Uint8Array) {

View file

@ -41,7 +41,17 @@ const englishConstants = {
PASSPHRASE_CONFIRM: 'Please repeat it once more',
PASSPHRASE_MATCH_ERROR: `Passphrase didn't match`,
CONSOLE_WARNING_STOP: 'STOP!',
CONSOLE_WARNING_DESC: `This is a browser feature intended for developers. If someone told you to copy-paste something here to enable a feature or "hack" someone's account, it is a scam and will give them access to your account.`
CONSOLE_WARNING_DESC: `This is a browser feature intended for developers. If someone told you to copy-paste something here to enable a feature or "hack" someone's account, it is a scam and will give them access to your account.`,
SELECT_COLLECTION: `Select/Click on Collection to upload`,
CLOSE: 'Close',
NOTHING_HERE: `nothing to see here! 👀`,
UPLOAD: {
0: "Preparing to upload",
1: "Encryting your files",
2: "Uploading your Files",
3: "Files Uploaded Successfully !!!"
},
OF: 'of'
};
export default englishConstants;

View file

@ -24,6 +24,32 @@ export class Crypto {
key);
}
async encryptMetadata(metadata, key) {
const encodedMetadata = new TextEncoder().encode(JSON.stringify(metadata));
const { file: encryptedMetadata } = await libsodium.encryptChaChaOneShot(
encodedMetadata,
key);
const { encryptedData, ...other } = encryptedMetadata
return {
file: {
encryptedData: await libsodium.toB64(encryptedData),
...other
},
key
};
}
async encryptThumbnail(fileData, key) {
return libsodium.encryptChaChaOneShot(
fileData,
key);
}
async encryptFile(fileData, key) {
return libsodium.encryptChaCha(fileData, key);
}
async encrypt(data, key) {
return libsodium.encrypt(data, key);
}
@ -44,8 +70,16 @@ export class Crypto {
return libsodium.deriveKey(passphrase, salt);
}
async decryptToB64(encryptedKey, sessionNonce, sessionKey) {
return libsodium.decryptToB64(encryptedKey, sessionNonce, sessionKey)
async decryptB64(data, nonce, key) {
return libsodium.decryptB64(data, nonce, key)
}
async decryptString(data, nonce, key) {
return libsodium.decryptString(data, nonce, key)
}
async encryptToB64(data, key) {
return libsodium.encryptToB64(data, key);
}
async generateMasterKey() {

View file

@ -1,32 +1,33 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext",
"webworker"
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext",
"webworker"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": "./src",
"downlevelIteration": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"src/pages/index.tsx"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": "./src",
"downlevelIteration": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx", "src/pages/index.tsx"
],
"exclude": [
"node_modules"
]
}
"exclude": [
"node_modules"
]
}

View file

@ -1673,6 +1673,11 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
attr-accept@^2.2.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
axios@^0.20.0:
version "0.20.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.20.0.tgz#057ba30f04884694993a8cd07fa394cff11c50bd"
@ -2848,6 +2853,11 @@ execa@^4.0.2:
signal-exit "^3.0.2"
strip-final-newline "^2.0.0"
exif-js@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/exif-js/-/exif-js-2.3.0.tgz#9d10819bf571f873813e7640241255ab9ce1a814"
integrity sha1-nRCBm/Vx+HOBPnZAJBJVq5zhqBQ=
expand-brackets@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
@ -2948,6 +2958,13 @@ figgy-pudding@^3.5.1:
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
file-selector@^0.2.2:
version "0.2.4"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.2.4.tgz#7b98286f9dbb9925f420130ea5ed0a69238d4d80"
integrity sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==
dependencies:
tslib "^2.0.3"
file-uri-to-path@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@ -4923,6 +4940,15 @@ react-dom@16.13.1:
prop-types "^15.6.2"
scheduler "^0.19.1"
react-dropzone@^11.2.4:
version "11.3.0"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.3.0.tgz#516561c5003e0c0f7d63bd5621f410b1b3496ab3"
integrity sha512-5ffIOi5Uf1X52m4fN8QdcRuAX88nQPfmx6HTTIfF9I3W9Ss1SvRDl/ruZmFf53K7+g3TSaIgVw6a9EK7XoDwHw==
dependencies:
attr-accept "^2.2.1"
file-selector "^0.2.2"
prop-types "^15.7.2"
react-fast-compare@^2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
@ -5851,6 +5877,11 @@ tslib@^1.10.0, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
tslib@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
@ -5884,10 +5915,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2"
integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==
typescript@^4.1.3:
version "4.1.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7"
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
uncontrollable@^7.0.0:
version "7.1.1"