Skip to content

Commit

Permalink
Merge pull request #840 from internxt/feature/download-public-shared-…
Browse files Browse the repository at this point in the history
…items

[PB-897]: reature/download public shared items
  • Loading branch information
CandelR authored Sep 20, 2023
2 parents 35bbe78 + 5989126 commit fda290c
Show file tree
Hide file tree
Showing 13 changed files with 133 additions and 40 deletions.
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

0 comments on commit fda290c

Please sign in to comment.