Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[PB-897]: reature/download public shared items #840

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"@iconscout/react-unicons": "^1.1.6",
"@internxt/inxt-js": "=1.2.21",
"@internxt/lib": "^1.2.0",
"@internxt/sdk": "1.4.52",
"@internxt/sdk": "1.4.53",
"@phosphor-icons/react": "^2.0.10",
"@popperjs/core": "^2.11.6",
"@reduxjs/toolkit": "^1.6.0",
Expand Down
2 changes: 1 addition & 1 deletion src/app/core/config/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@
{
"id": "share-token",
"layout": "share",
"path": "/sh/file/:token([a-z0-9]{20})/:code?",
"path": "/sh/file/:token/:code?",
"exact": false
},
{
Expand Down
56 changes: 48 additions & 8 deletions src/app/drive/components/ShareDialog/ShareDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { ArrowLeft, CaretDown, Check, CheckCircle, Globe, Link, UserPlus, Users,
import Avatar from 'app/shared/components/Avatar';
import Spinner from 'app/shared/components/Spinner/Spinner';
import { sharedThunks } from '../../../store/slices/sharedLinks';
import { DriveItemData } from '../../types';
import './ShareDialog.scss';
import shareService, { getSharingRoles } from '../../../share/services/share.service';
import errorService from '../../../core/services/error.service';
Expand All @@ -21,6 +20,9 @@ import notificationsService, { ToastType } from '../../../notifications/services
import { Role } from 'app/store/slices/sharedLinks/types';
import copy from 'copy-to-clipboard';

import crypto from 'crypto';
import { aes } from '@internxt/lib';

type AccessMode = 'public' | 'restricted';
type UserRole = 'owner' | 'editor' | 'reader';
type Views = 'general' | 'invite' | 'requests';
Expand Down Expand Up @@ -213,17 +215,55 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => {
dispatch(uiActions.setIsShareDialogOpen(false));
};

const onCopyLink = (): void => {
if (accessMode === 'restricted') {
const getPublicShareLink = async (uuid: string, itemType: 'folder' | 'file') => {
const user = props.user;
const { mnemonic } = user;
const code = crypto.randomBytes(32).toString('hex');

const encryptedMnemonic = aes.encrypt(mnemonic, code);

try {
const publicSharingItemData = await shareService.createPublicSharingItem({
encryptionAlgorithm: 'inxt-v2',
encryptionKey: encryptedMnemonic,
itemType,
itemId: uuid,
});
const { id: sharingId } = publicSharingItemData;

copy(`${process.env.REACT_APP_HOSTNAME}/sh/${itemType}/${sharingId}/${code}`);
notificationsService.show({ text: translate('shared-links.toast.copy-to-clipboard'), type: ToastType.Success });
} catch (error) {
notificationsService.show({
text: translate('modals.shareModal.errors.copy-to-clipboard'),
type: ToastType.Error,
});
}
};

const getPrivateShareLink = () => {
try {
copy(`${process.env.REACT_APP_HOSTNAME}/app/shared/?folderuuid=${itemToShare?.item.uuid}`);
notificationsService.show({ text: translate('shared-links.toast.copy-to-clipboard'), type: ToastType.Success });
} catch (error) {
notificationsService.show({
text: translate('modals.shareModal.errors.copy-to-clipboard'),
type: ToastType.Error,
});
}
};

const onCopyLink = (): void => {
if (accessMode === 'restricted') {
getPrivateShareLink();
closeSelectedUserPopover();
return;
}

dispatch(sharedThunks.getSharedLinkThunk({ item: itemToShare?.item as DriveItemData }));

closeSelectedUserPopover();
if (itemToShare?.item.uuid) {
getPublicShareLink(itemToShare?.item.uuid, itemToShare.item.isFolder ? 'folder' : 'file');
closeSelectedUserPopover();
}
};

const onInviteUser = () => {
Expand Down Expand Up @@ -450,7 +490,7 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => {
{({ close }) => (
<>
{/* Public */}
{/* <button
<button
className="flex h-16 w-full cursor-pointer items-center justify-start space-x-3 rounded-lg px-3 hover:bg-gray-5"
onClick={() => changeAccess('public')}
>
Expand All @@ -472,7 +512,7 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => {
)
) : null}
</div>
</button> */}
</button>
{/* Restricted */}
<button
className="flex h-16 w-full cursor-pointer items-center justify-start space-x-3 rounded-lg px-3 hover:bg-gray-5"
Expand Down
3 changes: 2 additions & 1 deletion src/app/i18n/locales/cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,8 @@
}
},
"errors": {
"updatingRole": "无法更新角色,请稍后重试."
"updatingRole": "无法更新角色,请稍后重试.",
"copy-to-clipboard": "分享项目时出错,请稍后再."
},
"general": {
"generalAccess": "一般访问",
Expand Down
3 changes: 2 additions & 1 deletion src/app/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,8 @@
}
},
"errors": {
"updatingRole": "The role could not be updated, please try again later."
"updatingRole": "The role could not be updated, please try again later.",
"copy-to-clipboard": "Error sharing item, try again later."
},
"general": {
"generalAccess": "General access",
Expand Down
3 changes: 2 additions & 1 deletion src/app/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,8 @@
}
},
"errors": {
"updatingRole": "El rol no pudo ser actualizado, por favor inténtalo nuevamente más tarde."
"updatingRole": "El rol no pudo ser actualizado, por favor inténtalo nuevamente más tarde.",
"copy-to-clipboard": "Error al compartir el elemento, inténtalo de nuevo más tarde."
},
"general": {
"generalAccess": "Acceso general",
Expand Down
3 changes: 2 additions & 1 deletion src/app/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,8 @@
}
},
"errors": {
"updatingRole": "Le rôle n'a pas pu être mis à jour, veuillez réessayer plus tard."
"updatingRole": "Le rôle n'a pas pu être mis à jour, veuillez réessayer plus tard.",
"copy-to-clipboard": "Erreur lors du partage de l'élément, réessayez plus tard."
},
"general": {
"generalAccess": "Accès général",
Expand Down
3 changes: 2 additions & 1 deletion src/app/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,8 @@
}
},
"errors": {
"updatingRole": "Impossibile aggiornare il ruolo, per favore riprova più tardi."
"updatingRole": "Impossibile aggiornare il ruolo, per favore riprova più tardi.",
"copy-to-clipboard": "Errore durante la condivisione dell'elemento, riprova più tardi."
},
"general": {
"generalAccess": "Accesso generale",
Expand Down
3 changes: 2 additions & 1 deletion src/app/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,8 @@
}
},
"errors": {
"updatingRole": "Роль не может быть обновлена, пожалуйста, попробуйте позже."
"updatingRole": "Роль не может быть обновлена, пожалуйста, попробуйте позже.",
"copy-to-clipboard": "Ошибка при попытке поделиться элементом, попробуйте позже."
},
"general": {
"generalAccess": "Общий доступ",
Expand Down
55 changes: 50 additions & 5 deletions src/app/share/services/share.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ import {
SharedFolders,
SharedFiles,
SharedFoldersInvitationsAsInvitedUserResponse,
CreateSharingPayload,
SharingMeta,
} from '@internxt/sdk/dist/drive/share/types';
import { domainManager } from './DomainManager';
import _ from 'lodash';
import { binaryStreamToBlob } from '../../core/services/stream.service';
import downloadService from '../../drive/services/download.service';
import network from '../../network';
import { decryptMessageWithPrivateKey } from '../../crypto/services/pgp.service';
import localStorageService from '../../core/services/local-storage.service';
import {
Expand Down Expand Up @@ -160,10 +159,40 @@ export function deleteShareLink(shareId: string): Promise<{ deleted: boolean; sh
});
}

export function getSharedFileInfo(token: string, code: string, password?: string): Promise<ShareTypes.ShareLink> {
export function getSharedFileInfo(
sharingId: string,
code: string,
password?: string,
): Promise<{
id: string;
itemId: string;
itemType: string;
ownerId: string;
sharedWith: string;
encryptionKey: string;
encryptionAlgorithm: string;
createdAt: string;
updatedAt: string;
type: string;
item: any;
itemToken: string;
}> {
const newApiURL = SdkFactory.getNewApiInstance().getApiUrl();
return httpService
.get<ShareTypes.ShareLink>(newApiURL + '/storage/share/' + token + '?code=' + code, {
.get<{
id: string;
itemId: string;
itemType: string;
ownerId: string;
sharedWith: string;
encryptionKey: string;
encryptionAlgorithm: string;
createdAt: string;
updatedAt: string;
type: string;
item: ShareTypes.ShareLink['item'];
itemToken: string;
}>(newApiURL + '/sharings/' + sharingId + '/meta?code=' + code, {
headers: {
'x-share-password': password,
},
Expand Down Expand Up @@ -550,6 +579,20 @@ export const processInvitation = async (
return response;
};

export function createPublicSharingItem(publicSharingPayload: CreateSharingPayload): Promise<SharingMeta> {
const shareClient = SdkFactory.getNewApiInstance().createShareClient();
return shareClient.createSharing(publicSharingPayload).catch((error) => {
throw errorService.castError(error);
});
}

export function getPublicSharingMeta(sharingId: string, code: string, password?: string): Promise<SharingMeta> {
const shareClient = SdkFactory.getNewApiInstance().createShareClient();
return shareClient.getSharingMeta(sharingId, code, password).catch((error) => {
throw errorService.castError(error);
});
}

const shareService = {
createShare,
createShareLink,
Expand Down Expand Up @@ -578,6 +621,8 @@ const shareService = {
acceptSharedFolderInvite,
declineSharedFolderInvite,
processInvitation,
createPublicSharingItem,
getPublicSharingMeta,
};

export default shareService;
31 changes: 16 additions & 15 deletions src/app/share/views/ShareView/ShareFileView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { useState, useEffect } from 'react';
import { match } from 'react-router';
import shareService, { getSharedFileInfo } from 'app/share/services/share.service';
import shareService from 'app/share/services/share.service';
import iconService from 'app/drive/services/icon.service';
import sizeService from 'app/drive/services/size.service';
import { TaskProgress } from 'app/tasks/types';
Expand All @@ -27,6 +27,7 @@ import SendBanner from './SendBanner';
import { useTranslationContext } from 'app/i18n/provider/TranslationProvider';
import { ShareTypes } from '@internxt/sdk/dist/drive';
import errorService from 'app/core/services/error.service';
import { SharingMeta } from '@internxt/sdk/dist/drive/share/types';

export interface ShareViewProps extends ShareViewState {
match: match<{
Expand All @@ -53,12 +54,12 @@ interface ShareViewState {

export default function ShareFileView(props: ShareViewProps): JSX.Element {
const { translate } = useTranslationContext();
const token = props.match.params.token;
const sharingId = props.match.params.token;
const code = props.match.params.code;
const [progress, setProgress] = useState(TaskProgress.Min);
const [blobProgress, setBlobProgress] = useState(TaskProgress.Min);
const [isDownloading, setIsDownloading] = useState(false);
const [info, setInfo] = useState<Partial<ShareTypes.ShareLink & { name: string }>>({});
const [info, setInfo] = useState<SharingMeta | Record<string, any>>({});
const [isLoaded, setIsLoaded] = useState(false);
const [isError, setIsError] = useState(false);
const [openPreview, setOpenPreview] = useState(false);
Expand Down Expand Up @@ -117,21 +118,22 @@ export default function ShareFileView(props: ShareViewProps): JSX.Element {
const getFormatFileName = (): string => {
const hasType = info?.item?.type !== null;
const extension = hasType ? `.${info?.item?.type}` : '';
return `${info?.item?.name}${extension}`;
return `${info?.item?.plainName}${extension}`;
};

const getFormatFileSize = (): string => {
return sizeService.bytesToString(info?.item?.size || 0);
};

function loadInfo(password?: string) {
return getSharedFileInfo(token, code, password)
.then((info) => {
return shareService
.getPublicSharingMeta(sharingId, code, password)
.then((res) => {
setIsLoaded(true);
setRequiresPassword(false);
setInfo({
...info,
name: info.item.name,
...res,
name: res.item.plainName,
});
})
.catch((err) => {
Expand All @@ -150,10 +152,10 @@ export default function ShareFileView(props: ShareViewProps): JSX.Element {
const encryptionKey = fileInfo.encryptionKey;

const readable = network.downloadFile({
bucketId: fileInfo.bucket,
bucketId: fileInfo.item.bucket,
fileId: fileInfo.item?.fileId,
encryptionKey: Buffer.from(encryptionKey, 'hex'),
token: (fileInfo as any).fileToken,
token: fileInfo.itemToken,
options: {
abortController,
notifyProgress: (totalProgress, downloadedBytes) => {
Expand All @@ -174,7 +176,7 @@ export default function ShareFileView(props: ShareViewProps): JSX.Element {

const download = async (): Promise<void> => {
if (!isDownloading) {
const fileInfo = info as unknown as ShareTypes.ShareLink;
const fileInfo = info;
const MIN_PROGRESS = 0;

if (fileInfo) {
Expand All @@ -183,16 +185,15 @@ export default function ShareFileView(props: ShareViewProps): JSX.Element {
setProgress(MIN_PROGRESS);
setIsDownloading(true);
const readable = await network.downloadFile({
bucketId: fileInfo.bucket,
bucketId: fileInfo.item.bucket,
fileId: fileInfo.item.fileId,
encryptionKey: Buffer.from(encryptionKey, 'hex'),
token: (fileInfo as any).fileToken,
token: fileInfo.itemToken,
options: {
notifyProgress: (totalProgress, downloadedBytes) => {
const progress = Math.trunc((downloadedBytes / totalProgress) * 100);
setProgress(progress);
if (progress == 100) {
shareService.incrementShareView(fileInfo.token);
setIsDownloading(false);
}
},
Expand Down Expand Up @@ -336,7 +337,7 @@ export default function ShareFileView(props: ShareViewProps): JSX.Element {
<SendBanner sendBannerVisible={sendBannerVisible} setIsSendBannerVisible={setIsSendBannerVisible} />
<FileViewer
show={openPreview}
file={info['item']}
file={info!['item']}
onClose={closePreview}
onDownload={onDownloadFromPreview}
progress={blobProgress}
Expand Down
1 change: 1 addition & 0 deletions src/react-app-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ declare namespace NodeJS {
REACT_APP_SEGMENT_DEBUG: string;
REACT_APP_RECAPTCHA_V3: string;
REACT_APP_SHARE_LINKS_DOMAIN: string;
REACT_APP_HOSTNAME: string;
}
}

Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1638,10 +1638,10 @@
resolved "https://npm.pkg.github.com/download/@internxt/prettier-config/1.0.2/9a19de23e3330a81c0e2bace1a6a0325fc8633197cf677e44ea75e54df315b5e"
integrity sha512-t4HiqvCbC7XgQepwWlIaFJe3iwW7HCf6xOSU9nKTV0tiGqOPz7xMtIgLEloQrDA34Cx4PkOYBXrvFPV6RxSFAA==

"@internxt/[email protected].52":
version "1.4.52"
resolved "https://npm.pkg.github.com/download/@internxt/sdk/1.4.52/1806b14f6560b0b791f4357b628d91a0f1988640#1806b14f6560b0b791f4357b628d91a0f1988640"
integrity sha512-wr/Hgk4LlA93qgrRWG6wBrxs9pnsTRfXDCAAW4bgO2ZhJsmnpP2VwFU9R73UqyoC3ButnklyXUyDmsIpM3lvAA==
"@internxt/[email protected].53":
version "1.4.53"
resolved "https://npm.pkg.github.com/download/@internxt/sdk/1.4.53/d6736a55bc3c9f8775706aa1f0aa46debfa9d19c#d6736a55bc3c9f8775706aa1f0aa46debfa9d19c"
integrity sha512-0zNMpxQK8+f1cNgDQZbR3nWLsYdhaVCGDNCezwb9LEtjNhguU+IB27zvDwnIwQsbZPFf8G/hNY+9ZCONLU1k/A==
dependencies:
axios "^0.24.0"
query-string "^7.1.0"
Expand Down