web/satellite: add folder sharing

This change allows users to generate links for sharing folders.
Previously, users were only able to do this with files and buckets.

Resolves #5644

Change-Id: I16dd8270337e3561b6bda895b46d3cc9be5f8041
This commit is contained in:
Jeremy Wharton 2023-07-04 05:07:04 -05:00 committed by Vitalii Shpital
parent 5fc6eaab17
commit 074457fa4e
18 changed files with 225 additions and 676 deletions

View File

@ -81,6 +81,11 @@
/>
<dots-icon v-else />
<div v-if="dropdownOpen" class="file-entry__functional__dropdown">
<div class="file-entry__functional__dropdown__item" @click.stop="share">
<share-icon />
<p class="file-entry__functional__dropdown__item__label">Share</p>
</div>
<div
v-if="!deleteConfirmation" class="file-entry__functional__dropdown__item"
@click.stop="confirmDeletion"
@ -125,6 +130,7 @@ import { useAppStore } from '@/store/modules/appStore';
import { useConfigStore } from '@/store/modules/configStore';
import { AnalyticsHttpApi } from '@/api/analytics';
import { ObjectType } from '@/utils/objectIcon';
import { ShareType } from '@/types/browser';
import TableItem from '@/components/common/TableItem.vue';
@ -390,7 +396,8 @@ function setShiftSelectedFiles(): void {
function share(): void {
obStore.closeDropdown();
obStore.setObjectPathForModal(props.path + props.file.Key);
appStore.updateActiveModal(MODALS.shareObject);
appStore.setShareModalType(props.file.type === 'file' ? ShareType.File : ShareType.Folder);
appStore.updateActiveModal(MODALS.share);
}
/**

View File

@ -35,7 +35,7 @@
<ButtonIcon
class="gallery__header__functional__item"
:icon="ShareIcon"
:on-press="() => setActiveModal(ShareModal)"
:on-press="showShareModal"
info="Share"
/>
<ButtonIcon
@ -48,7 +48,7 @@
:on-distribution="() => setActiveModal(DistributionModal)"
:on-view-details="() => setActiveModal(DetailsModal)"
:on-download="download"
:on-share="() => setActiveModal(ShareModal)"
:on-share="showShareModal"
:on-delete="() => setActiveModal(DeleteModal)"
/>
</div>
@ -121,12 +121,14 @@ import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames
import { useAppStore } from '@/store/modules/appStore';
import { useNotify } from '@/utils/hooks';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { useLinksharing } from '@/composables/useLinksharing';
import { RouteConfig } from '@/types/router';
import { ShareType } from '@/types/browser';
import ButtonIcon from '@/components/browser/galleryView/ButtonIcon.vue';
import OptionsDropdown from '@/components/browser/galleryView/OptionsDropdown.vue';
import DeleteModal from '@/components/browser/galleryView/modals/Delete.vue';
import ShareModal from '@/components/browser/galleryView/modals/Share.vue';
import ShareModal from '@/components/modals/ShareModal.vue';
import DetailsModal from '@/components/browser/galleryView/modals/Details.vue';
import DistributionModal from '@/components/browser/galleryView/modals/Distribution.vue';
import VLoader from '@/components/common/VLoader.vue';
@ -148,6 +150,7 @@ const appStore = useAppStore();
const obStore = useObjectBrowserStore();
const bucketsStore = useBucketsStore();
const notify = useNotify();
const { generateObjectPreviewAndMapURL } = useLinksharing();
const route = useRoute();
@ -182,13 +185,6 @@ const fileIndex = computed((): number => {
return obStore.sortedFiles.findIndex(f => f.Key === filePath.value.split('/').pop());
});
/**
* Format the file size to be displayed.
*/
const size = computed((): string => {
return prettyBytes(obStore.sortedFiles.find(f => f.Key === file.value.Key)?.Size || 0);
});
/**
* Retrieve the filepath of the modal from the store.
*/
@ -271,7 +267,13 @@ const currentPath = computed((): string => {
async function fetchPreviewAndMapUrl(): Promise<void> {
isLoading.value = true;
const url: string = await obStore.state.fetchPreviewAndMapUrl(filePath.value);
let url = '';
try {
url = await generateObjectPreviewAndMapURL(filePath.value);
} catch (error) {
notify.error(`Unable to get file preview and map URL. ${error.message}`, AnalyticsErrorEventSource.GALLERY_VIEW);
}
if (!url) {
previewAndMapFailed.value = true;
isLoading.value = false;
@ -435,6 +437,14 @@ function findCachedURL(): string | undefined {
return cache.url;
}
/**
* Displays the Share modal.
*/
function showShareModal(): void {
appStore.setShareModalType(ShareType.File);
appStore.updateActiveModal(ShareModal);
}
/**
* Call `fetchPreviewAndMapUrl` on before mount lifecycle method.
*/

View File

@ -44,7 +44,7 @@ import { BrowserObject, useObjectBrowserStore } from '@/store/modules/objectBrow
import VModal from '@/components/common/VModal.vue';
import VButton from '@/components/common/VButton.vue';
import ModalHeader from '@/components/browser/galleryView/modals/ModalHeader.vue';
import ModalHeader from '@/components/modals/ModalHeader.vue';
import DeleteIcon from '@/../static/images/browser/galleryView/modals/delete.svg';

View File

@ -48,7 +48,7 @@ import { BrowserObject, useObjectBrowserStore } from '@/store/modules/objectBrow
import VModal from '@/components/common/VModal.vue';
import VButton from '@/components/common/VButton.vue';
import ModalHeader from '@/components/browser/galleryView/modals/ModalHeader.vue';
import ModalHeader from '@/components/modals/ModalHeader.vue';
import DetailsIcon from '@/../static/images/browser/galleryView/modals/details.svg';

View File

@ -44,7 +44,7 @@
<script setup lang="ts">
import VModal from '@/components/common/VModal.vue';
import VButton from '@/components/common/VButton.vue';
import ModalHeader from '@/components/browser/galleryView/modals/ModalHeader.vue';
import ModalHeader from '@/components/modals/ModalHeader.vue';
import GlobeIcon from '@/../static/images/browser/galleryView/modals/globe.svg';

View File

@ -117,10 +117,12 @@
import { computed, onBeforeMount, ref, watch } from 'vue';
import prettyBytes from 'pretty-bytes';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
import { BrowserObject, useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
import { useAppStore } from '@/store/modules/appStore';
import { useLinksharing } from '@/composables/useLinksharing';
import { AnalyticsHttpApi } from '@/api/analytics';
import VModal from '@/components/common/VModal.vue';
import VButton from '@/components/common/VButton.vue';
@ -129,9 +131,12 @@ import VLoader from '@/components/common/VLoader.vue';
import ErrorNoticeIcon from '@/../static/images/common/errorNotice.svg?url';
import PlaceholderImage from '@/../static/images/browser/placeholder.svg';
const analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
const appStore = useAppStore();
const obStore = useObjectBrowserStore();
const notify = useNotify();
const { generateFileOrFolderShareURL, generateObjectPreviewAndMapURL } = useLinksharing();
const isLoading = ref<boolean>(false);
const previewAndMapFailed = ref<boolean>(false);
@ -233,9 +238,12 @@ const placeHolderDisplayable = computed((): boolean => {
async function fetchPreviewAndMapUrl(): Promise<void> {
isLoading.value = true;
const url: string = await obStore.state.fetchPreviewAndMapUrl(
filePath.value,
);
let url = '';
try {
url = await generateObjectPreviewAndMapURL(filePath.value);
} catch (error) {
notify.error(`Unable to get file preview and map URL. ${error.message}`, AnalyticsErrorEventSource.ACCESS_GRANTS_PAGE);
}
if (!url) {
previewAndMapFailed.value = true;
@ -294,9 +302,12 @@ async function copy(): Promise<void> {
* Get the share link of the current opened file.
*/
async function getSharedLink(): Promise<void> {
objectLink.value = await obStore.state.fetchSharedLink(
filePath.value,
);
analytics.eventTriggered(AnalyticsEvent.LINK_SHARED);
try {
objectLink.value = await generateFileOrFolderShareURL(filePath.value);
} catch (error) {
notify.error(`Unable to get sharing URL. ${error.message}`, AnalyticsErrorEventSource.OBJECT_DETAILS_MODAL);
}
}
/**

View File

@ -1,281 +0,0 @@
// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<VModal :on-close="closeModal">
<template #content>
<div class="modal">
<h1 class="modal__title">Share Bucket</h1>
<ShareContainer :link="link" />
<p class="modal__label">
Or copy link:
</p>
<VLoader v-if="isLoading" width="20px" height="20px" />
<div v-if="!isLoading" class="modal__input-group">
<input
id="url"
class="modal__input"
type="url"
:value="link"
aria-describedby="btn-copy-link"
readonly
>
<VButton
:label="copyButtonState === ButtonStates.Copy ? 'Copy' : 'Copied'"
width="114px"
height="40px"
:on-press="onCopy"
:is-disabled="isLoading"
:is-green="copyButtonState === ButtonStates.Copied"
:icon="copyButtonState === ButtonStates.Copied ? 'none' : 'copy'"
>
<template v-if="copyButtonState === ButtonStates.Copied" #icon>
<check-icon />
</template>
</VButton>
</div>
</div>
</template>
</VModal>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { AccessGrant, EdgeCredentials } from '@/types/accessGrants';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
import { useAppStore } from '@/store/modules/appStore';
import { useAccessGrantsStore } from '@/store/modules/accessGrantsStore';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useConfigStore } from '@/store/modules/configStore';
import VModal from '@/components/common/VModal.vue';
import VLoader from '@/components/common/VLoader.vue';
import VButton from '@/components/common/VButton.vue';
import ShareContainer from '@/components/common/share/ShareContainer.vue';
import CheckIcon from '@/../static/images/common/check.svg';
enum ButtonStates {
Copy,
Copied,
}
const configStore = useConfigStore();
const bucketsStore = useBucketsStore();
const appStore = useAppStore();
const agStore = useAccessGrantsStore();
const projectsStore = useProjectsStore();
const notify = useNotify();
const worker = ref<Worker | null>(null);
const isLoading = ref<boolean>(true);
const link = ref<string>('');
const copyButtonState = ref<ButtonStates>(ButtonStates.Copy);
/**
* Returns chosen bucket name from store.
*/
const bucketName = computed((): string => {
return bucketsStore.state.fileComponentBucketName;
});
/**
* Returns passphrase from store.
*/
const passphrase = computed((): string => {
return bucketsStore.state.passphrase;
});
/**
* Copies link to users clipboard.
*/
async function onCopy(): Promise<void> {
await navigator.clipboard.writeText(link.value);
copyButtonState.value = ButtonStates.Copied;
setTimeout(() => {
copyButtonState.value = ButtonStates.Copy;
}, 2000);
await notify.success('Link copied successfully.');
}
/**
* Sets share bucket link.
*/
async function setShareLink(): Promise<void> {
if (!worker.value) {
return;
}
try {
let path = `${bucketName.value}`;
const now = new Date();
const LINK_SHARING_AG_NAME = `${path}_shared-bucket_${now.toISOString()}`;
const cleanAPIKey: AccessGrant = await agStore.createAccessGrant(LINK_SHARING_AG_NAME, projectsStore.state.selectedProject.id);
const satelliteNodeURL = configStore.state.config.satelliteNodeURL;
const salt = await projectsStore.getProjectSalt(projectsStore.state.selectedProject.id);
worker.value.postMessage({
'type': 'GenerateAccess',
'apiKey': cleanAPIKey.secret,
'passphrase': passphrase.value,
'salt': salt,
'satelliteNodeURL': satelliteNodeURL,
});
const grantEvent: MessageEvent = await new Promise(resolve => {
if (worker.value) {
worker.value.onmessage = resolve;
}
});
const grantData = grantEvent.data;
if (grantData.error) {
await notify.error(grantData.error, AnalyticsErrorEventSource.SHARE_BUCKET_MODAL);
return;
}
worker.value.postMessage({
'type': 'RestrictGrant',
'isDownload': true,
'isUpload': false,
'isList': true,
'isDelete': false,
'paths': [path],
'grant': grantData.value,
});
const event: MessageEvent = await new Promise(resolve => {
if (worker.value) {
worker.value.onmessage = resolve;
}
});
const data = event.data;
if (data.error) {
await notify.error(data.error, AnalyticsErrorEventSource.SHARE_BUCKET_MODAL);
return;
}
const credentials: EdgeCredentials = await agStore.getEdgeCredentials(data.value, undefined, true);
path = encodeURIComponent(path.trim());
const publicLinksharingURL = configStore.state.config.publicLinksharingURL;
link.value = `${publicLinksharingURL}/${credentials.accessKeyId}/${path}`;
} catch (error) {
await notify.error(error.message, AnalyticsErrorEventSource.SHARE_BUCKET_MODAL);
} finally {
isLoading.value = false;
}
}
/**
* Sets local worker with worker instantiated in store.
*/
function setWorker(): void {
worker.value = agStore.state.accessGrantsWebWorker;
if (worker.value) {
worker.value.onerror = (error: ErrorEvent) => {
notify.error(error.message, AnalyticsErrorEventSource.SHARE_BUCKET_MODAL);
};
}
}
/**
* Closes open bucket modal.
*/
function closeModal(): void {
if (isLoading.value) return;
appStore.removeActiveModal();
}
onMounted(async () => {
setWorker();
await setShareLink();
});
</script>
<style scoped lang="scss">
.modal {
font-family: 'font_regular', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
padding: 50px;
max-width: 470px;
@media screen and (width <= 430px) {
padding: 20px;
}
&__title {
font-family: 'font_bold', sans-serif;
font-size: 22px;
line-height: 29px;
color: #1b2533;
margin: 10px 0 35px;
}
&__label {
font-family: 'font_medium', sans-serif;
font-size: 14px;
line-height: 21px;
color: #354049;
align-self: center;
margin: 20px 0 10px;
}
&__link {
font-size: 16px;
line-height: 21px;
color: #384b65;
align-self: flex-start;
word-break: break-all;
text-align: left;
}
&__buttons {
display: flex;
column-gap: 20px;
margin-top: 32px;
width: 100%;
@media screen and (width <= 430px) {
flex-direction: column-reverse;
column-gap: unset;
row-gap: 15px;
}
}
&__input-group {
border: 1px solid var(--c-grey-4);
background: var(--c-grey-1);
padding: 10px;
display: flex;
justify-content: space-between;
border-radius: 8px;
width: 100%;
height: 42px;
}
&__input {
background: none;
border: none;
font-size: 14px;
color: var(--c-grey-6);
outline: none;
max-width: 340px;
width: 100%;
@media screen and (width <= 430px) {
max-width: 210px;
}
}
}
</style>

View File

@ -2,12 +2,12 @@
// See LICENSE for copying information.
<template>
<VModal :on-close="onClose">
<VModal :on-close="closeModal">
<template #content>
<div class="modal">
<ModalHeader
:icon="ShareIcon"
title="Share File"
:title="'Share ' + shareType"
/>
<VLoader v-if="loading" width="40px" height="40px" />
<template v-else>
@ -29,7 +29,7 @@
width="100%"
border-radius="10px"
font-size="14px"
:on-press="onClose"
:on-press="closeModal"
is-white
/>
<VButton
@ -50,13 +50,19 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { useAppStore } from '@/store/modules/appStore';
import { useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
import { useLinksharing } from '@/composables/useLinksharing';
import { useNotify } from '@/utils/hooks';
import { AnalyticsHttpApi } from '@/api/analytics';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { ShareType } from '@/types/browser';
import VModal from '@/components/common/VModal.vue';
import VLoader from '@/components/common/VLoader.vue';
import VButton from '@/components/common/VButton.vue';
import ShareContainer from '@/components/common/share/ShareContainer.vue';
import ModalHeader from '@/components/browser/galleryView/modals/ModalHeader.vue';
import ModalHeader from '@/components/modals/ModalHeader.vue';
import ShareIcon from '@/../static/images/browser/galleryView/modals/share.svg';
@ -65,16 +71,24 @@ enum ButtonStates {
Copied,
}
const obStore = useObjectBrowserStore();
const analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
const props = defineProps<{
onClose: () => void
}>();
const appStore = useAppStore();
const obStore = useObjectBrowserStore();
const { generateFileOrFolderShareURL, generateBucketShareURL } = useLinksharing();
const notify = useNotify();
const link = ref<string>('');
const loading = ref<boolean>(true);
const copyButtonState = ref<ButtonStates>(ButtonStates.Copy);
/**
* Returns what type of entity is being shared.
*/
const shareType = computed((): ShareType => {
return appStore.state.shareModalType;
});
/**
* Retrieve the path to the current file.
*/
@ -94,10 +108,26 @@ async function onCopy(): Promise<void> {
}, 2000);
}
/**
* Closes the modal.
*/
function closeModal(): void {
if (loading.value) return;
appStore.removeActiveModal();
}
onMounted(async (): Promise<void> => {
link.value = await obStore.state.fetchSharedLink(
filePath.value,
);
analytics.eventTriggered(AnalyticsEvent.LINK_SHARED);
try {
if (shareType.value === ShareType.Bucket) {
link.value = await generateBucketShareURL();
} else {
link.value = await generateFileOrFolderShareURL(filePath.value, shareType.value === ShareType.Folder);
}
} catch (error) {
notify.error(`Unable to get sharing URL. ${error.message}`, AnalyticsErrorEventSource.SHARE_MODAL);
}
loading.value = false;
});

View File

@ -1,189 +0,0 @@
// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<VModal :on-close="closeModal">
<template #content>
<div class="modal">
<h1 class="modal__title">Share File</h1>
<ShareContainer :link="link" />
<p class="modal__label">
Or copy link:
</p>
<VLoader v-if="isLoading" width="20px" height="20px" />
<div v-if="!isLoading" class="modal__input-group">
<input
id="url"
class="modal__input"
type="url"
:value="link"
aria-describedby="btn-copy-link"
readonly
>
<VButton
:label="copyButtonState === ButtonStates.Copy ? 'Copy' : 'Copied'"
width="114px"
height="40px"
:on-press="onCopy"
:is-disabled="isLoading"
:is-green="copyButtonState === ButtonStates.Copied"
:icon="copyButtonState === ButtonStates.Copied ? 'none' : 'copy'"
>
<template v-if="copyButtonState === ButtonStates.Copied" #icon>
<check-icon />
</template>
</VButton>
</div>
</div>
</template>
</VModal>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { useNotify } from '@/utils/hooks';
import { useAppStore } from '@/store/modules/appStore';
import { useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
import VModal from '@/components/common/VModal.vue';
import VButton from '@/components/common/VButton.vue';
import VLoader from '@/components/common/VLoader.vue';
import ShareContainer from '@/components/common/share/ShareContainer.vue';
import CheckIcon from '@/../static/images/common/check.svg';
enum ButtonStates {
Copy,
Copied,
}
const appStore = useAppStore();
const obStore = useObjectBrowserStore();
const notify = useNotify();
const isLoading = ref<boolean>(true);
const link = ref<string>('');
const copyButtonState = ref<ButtonStates>(ButtonStates.Copy);
/**
* Retrieve the path to the current file that has the fileShareModal opened from the store.
*/
const filePath = computed((): string => {
return obStore.state.objectPathForModal;
});
/**
* Copies link to users clipboard.
*/
async function onCopy(): Promise<void> {
await navigator.clipboard.writeText(link.value);
copyButtonState.value = ButtonStates.Copied;
setTimeout(() => {
copyButtonState.value = ButtonStates.Copy;
}, 2000);
await notify.success('Link copied successfully.');
}
/**
* Closes open bucket modal.
*/
function closeModal(): void {
if (isLoading.value) return;
appStore.removeActiveModal();
}
/**
* Lifecycle hook after initial render.
* Sets share link.
*/
onMounted(async (): Promise<void> => {
link.value = await obStore.state.fetchSharedLink(
filePath.value,
);
isLoading.value = false;
});
</script>
<style scoped lang="scss">
.modal {
font-family: 'font_regular', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
padding: 50px;
max-width: 470px;
@media screen and (width <= 430px) {
padding: 20px;
}
&__title {
font-family: 'font_bold', sans-serif;
font-size: 22px;
line-height: 29px;
color: #1b2533;
margin: 10px 0 35px;
}
&__label {
font-family: 'font_medium', sans-serif;
font-size: 14px;
line-height: 21px;
color: #354049;
align-self: center;
margin: 20px 0 10px;
}
&__link {
font-size: 16px;
line-height: 21px;
color: #384b65;
align-self: flex-start;
word-break: break-all;
text-align: left;
}
&__buttons {
display: flex;
column-gap: 20px;
margin-top: 32px;
width: 100%;
@media screen and (width <= 430px) {
flex-direction: column-reverse;
column-gap: unset;
row-gap: 15px;
}
}
&__input-group {
border: 1px solid var(--c-grey-4);
background: var(--c-grey-1);
padding: 10px;
display: flex;
justify-content: space-between;
border-radius: 8px;
width: 100%;
height: 42px;
}
&__input {
background: none;
border: none;
font-size: 14px;
color: var(--c-grey-6);
outline: none;
max-width: 340px;
width: 100%;
@media screen and (width <= 430px) {
max-width: 210px;
}
}
}
</style>

View File

@ -34,6 +34,7 @@ import { RouteConfig } from '@/types/router';
import { MODALS } from '@/utils/constants/appStatePopUps';
import { useAppStore } from '@/store/modules/appStore';
import { useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
import { ShareType } from '@/types/browser';
import ArrowDownIcon from '@/../static/images/common/dropIcon.svg';
import DetailsIcon from '@/../static/images/objects/details.svg';
@ -84,7 +85,8 @@ function onDetailsClick(): void {
* Toggles share bucket modal.
*/
function onShareBucketClick(): void {
appStore.updateActiveModal(MODALS.shareBucket);
appStore.setShareModalType(ShareType.Bucket);
appStore.updateActiveModal(MODALS.share);
isDropdownOpen.value = false;
}
</script>

View File

@ -14,19 +14,16 @@
import { computed, onBeforeMount, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { AnalyticsHttpApi } from '@/api/analytics';
import { RouteConfig } from '@/types/router';
import { AccessGrant, EdgeCredentials } from '@/types/accessGrants';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { EdgeCredentials } from '@/types/accessGrants';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { MODALS } from '@/utils/constants/appStatePopUps';
import { BucketPage } from '@/types/buckets';
import { useNotify } from '@/utils/hooks';
import { useAppStore } from '@/store/modules/appStore';
import { useAccessGrantsStore } from '@/store/modules/accessGrantsStore';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
import { useConfigStore } from '@/store/modules/configStore';
import FileBrowser from '@/components/browser/FileBrowser.vue';
import UploadCancelPopup from '@/components/objects/UploadCancelPopup.vue';
@ -34,16 +31,10 @@ import UploadCancelPopup from '@/components/objects/UploadCancelPopup.vue';
const obStore = useObjectBrowserStore();
const bucketsStore = useBucketsStore();
const appStore = useAppStore();
const configStore = useConfigStore();
const agStore = useAccessGrantsStore();
const projectsStore = useProjectsStore();
const router = useRouter();
const notify = useNotify();
const analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
const worker = ref<Worker | null>(null);
/**
* Indicates if upload cancel popup is visible.
*/
@ -58,13 +49,6 @@ const passphrase = computed((): string => {
return bucketsStore.state.passphrase;
});
/**
* Returns apiKey from store.
*/
const apiKey = computed((): string => {
return bucketsStore.state.apiKey;
});
/**
* Returns bucket name from store.
*/
@ -86,153 +70,16 @@ const edgeCredentials = computed((): EdgeCredentials => {
return bucketsStore.state.edgeCredentials;
});
/**
* Returns linksharing URL from store.
*/
const linksharingURL = computed((): string => {
return configStore.state.config.linksharingURL;
});
/**
* Returns public linksharing URL from store.
*/
const publicLinksharingURL = computed((): string => {
return configStore.state.config.publicLinksharingURL;
});
/**
* Generates a URL for an object map.
*/
async function generateObjectPreviewAndMapUrl(path: string): Promise<string> {
path = `${bucket.value}/${path}`;
try {
const creds: EdgeCredentials = await generateCredentials(apiKey.value, path, false);
path = encodeURIComponent(path.trim());
return `${linksharingURL.value}/s/${creds.accessKeyId}/${path}`;
} catch (error) {
notify.error(error.message, AnalyticsErrorEventSource.UPLOAD_FILE_VIEW);
return '';
}
}
/**
* Generates a URL for a link sharing service.
*/
async function generateShareLinkUrl(path: string): Promise<string> {
path = `${bucket.value}/${path}`;
const now = new Date();
const LINK_SHARING_AG_NAME = `${path}_shared-object_${now.toISOString()}`;
const cleanAPIKey: AccessGrant = await agStore.createAccessGrant(LINK_SHARING_AG_NAME, projectsStore.state.selectedProject.id);
try {
const credentials: EdgeCredentials = await generateCredentials(cleanAPIKey.secret, path, true);
path = encodeURIComponent(path.trim());
await analytics.eventTriggered(AnalyticsEvent.LINK_SHARED);
return `${publicLinksharingURL.value}/${credentials.accessKeyId}/${path}`;
} catch (error) {
await notify.error(error.message, AnalyticsErrorEventSource.UPLOAD_FILE_VIEW);
return '';
}
}
/**
* Sets local worker with worker instantiated in store.
*/
function setWorker(): void {
worker.value = agStore.state.accessGrantsWebWorker;
if (worker.value) {
worker.value.onerror = (error: ErrorEvent) => {
notify.error(error.message, AnalyticsErrorEventSource.UPLOAD_FILE_VIEW);
};
}
}
/**
* Generates share gateway credentials.
*/
async function generateCredentials(cleanApiKey: string, path: string, areEndless: boolean): Promise<EdgeCredentials> {
if (!worker.value) {
throw new Error('Worker is not defined');
}
const satelliteNodeURL = configStore.state.config.satelliteNodeURL;
const salt = await projectsStore.getProjectSalt(projectsStore.state.selectedProject.id);
worker.value.postMessage({
'type': 'GenerateAccess',
'apiKey': cleanApiKey,
'passphrase': passphrase.value,
'salt': salt,
'satelliteNodeURL': satelliteNodeURL,
});
const grantEvent: MessageEvent = await new Promise(resolve => {
if (worker.value) {
worker.value.onmessage = resolve;
}
});
const grantData = grantEvent.data;
if (grantData.error) {
await notify.error(grantData.error, AnalyticsErrorEventSource.UPLOAD_FILE_VIEW);
return new EdgeCredentials();
}
let permissionsMsg = {
'type': 'RestrictGrant',
'isDownload': true,
'isUpload': false,
'isList': true,
'isDelete': false,
'paths': [path],
'grant': grantData.value,
};
if (!areEndless) {
const now = new Date();
const inOneDay = new Date(now.setDate(now.getDate() + 1));
permissionsMsg = Object.assign(permissionsMsg, { 'notAfter': inOneDay.toISOString() });
}
worker.value.postMessage(permissionsMsg);
const event: MessageEvent = await new Promise(resolve => {
if (worker.value) {
worker.value.onmessage = resolve;
}
});
const data = event.data;
if (data.error) {
await notify.error(data.error, AnalyticsErrorEventSource.UPLOAD_FILE_VIEW);
return new EdgeCredentials();
}
return await agStore.getEdgeCredentials(data.value, undefined, true);
}
/**
* Initiates file browser.
*/
onBeforeMount(() => {
setWorker();
obStore.init({
endpoint: edgeCredentials.value.endpoint,
accessKey: edgeCredentials.value.accessKeyId,
secretKey: edgeCredentials.value.secretKey,
bucket: bucket.value,
browserRoot: RouteConfig.Buckets.with(RouteConfig.UploadFile).path,
fetchPreviewAndMapUrl: generateObjectPreviewAndMapUrl,
fetchSharedLink: generateShareLinkUrl,
});
});

View File

@ -0,0 +1,111 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
import { computed } from 'vue';
import { useAccessGrantsStore } from '@/store/modules/accessGrantsStore';
import { useConfigStore } from '@/store/modules/configStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { AccessGrant, EdgeCredentials } from '@/types/accessGrants';
const WORKER_ERR_MSG = 'Worker is not defined';
export function useLinksharing() {
const agStore = useAccessGrantsStore();
const configStore = useConfigStore();
const projectsStore = useProjectsStore();
const bucketsStore = useBucketsStore();
const worker = computed((): Worker | null => agStore.state.accessGrantsWebWorker);
async function generateFileOrFolderShareURL(path: string, isFolder = false): Promise<string> {
const fullPath = `${bucketsStore.state.fileComponentBucketName}/${path}`;
const type = isFolder ? 'folder' : 'object';
return generateShareURL(fullPath, type);
}
async function generateBucketShareURL(): Promise<string> {
return generateShareURL(bucketsStore.state.fileComponentBucketName, 'bucket');
}
async function generateShareURL(path: string, type: string): Promise<string> {
if (!worker.value) throw new Error(WORKER_ERR_MSG);
const LINK_SHARING_AG_NAME = `${path}_shared-${type}_${new Date().toISOString()}`;
const grant: AccessGrant = await agStore.createAccessGrant(LINK_SHARING_AG_NAME, projectsStore.state.selectedProject.id);
const credentials: EdgeCredentials = await generateCredentials(grant.secret, path, null);
return `${configStore.state.config.publicLinksharingURL}/${credentials.accessKeyId}/${encodeURIComponent(path.trim())}`;
}
async function generateObjectPreviewAndMapURL(path: string): Promise<string> {
if (!worker.value) throw new Error(WORKER_ERR_MSG);
path = bucketsStore.state.fileComponentBucketName + '/' + path;
const now = new Date();
const inOneDay = new Date(now.setDate(now.getDate() + 1));
const creds: EdgeCredentials = await generateCredentials(bucketsStore.state.apiKey, path, inOneDay);
return `${configStore.state.config.linksharingURL}/s/${creds.accessKeyId}/${encodeURIComponent(path.trim())}`;
}
async function generateCredentials(cleanAPIKey: string, path: string, expiration: Date | null): Promise<EdgeCredentials> {
if (!worker.value) throw new Error(WORKER_ERR_MSG);
const satelliteNodeURL = configStore.state.config.satelliteNodeURL;
const salt = await projectsStore.getProjectSalt(projectsStore.state.selectedProject.id);
worker.value.postMessage({
'type': 'GenerateAccess',
'apiKey': cleanAPIKey,
'passphrase': bucketsStore.state.passphrase,
'salt': salt,
'satelliteNodeURL': satelliteNodeURL,
});
const grantEvent: MessageEvent = await new Promise(resolve => {
if (worker.value) {
worker.value.onmessage = resolve;
}
});
const grantData = grantEvent.data;
if (grantData.error) {
throw new Error(grantData.error);
}
let permissionsMsg = {
'type': 'RestrictGrant',
'isDownload': true,
'isUpload': false,
'isList': true,
'isDelete': false,
'paths': [path],
'grant': grantData.value,
};
if (expiration) {
permissionsMsg = Object.assign(permissionsMsg, { 'notAfter': expiration.toISOString() });
}
worker.value.postMessage(permissionsMsg);
const event: MessageEvent = await new Promise(resolve => {
if (worker.value) {
worker.value.onmessage = resolve;
}
});
const data = event.data;
if (data.error) {
throw new Error(data.error);
}
return agStore.getEdgeCredentials(data.value, undefined, true);
}
return {
generateBucketShareURL,
generateFileOrFolderShareURL,
generateObjectPreviewAndMapURL,
};
}

View File

@ -9,6 +9,7 @@ import { FetchState } from '@/utils/constants/fetchStateEnum';
import { ManageProjectPassphraseStep } from '@/types/managePassphrase';
import { LocalData } from '@/utils/localData';
import { LimitToChange } from '@/types/projects';
import { ShareType } from '@/types/browser';
class AppState {
public fetchState = FetchState.LOADING;
@ -35,6 +36,7 @@ class AppState {
public isLargeUploadWarningNotificationShown = false;
public activeChangeLimit: LimitToChange = LimitToChange.Storage;
public isProjectTableViewEnabled = LocalData.getProjectTableViewEnabled();
public shareModalType: ShareType = ShareType.File;
}
class ErrorPageState {
@ -154,6 +156,10 @@ export const useAppStore = defineStore('app', () => {
state.isGalleryView = value;
}
function setShareModalType(type: ShareType): void {
state.shareModalType = type;
}
function closeDropdowns(): void {
state.activeDropdown = '';
}
@ -185,6 +191,7 @@ export const useAppStore = defineStore('app', () => {
state.error.visible = false;
state.isGalleryView = false;
state.isProjectTableViewEnabled = false;
state.shareModalType = ShareType.File;
LocalData.removeProjectTableViewConfig();
}
@ -211,6 +218,7 @@ export const useAppStore = defineStore('app', () => {
setUploadingModal,
setLargeUploadWarningNotification,
setLargeUploadNotification,
setShareModalType,
closeDropdowns,
setErrorPage,
removeErrorPage,

View File

@ -77,8 +77,6 @@ export class FilesState {
selectedFiles: BrowserObject[] = [];
shiftSelectedFiles: BrowserObject[] = [];
filesToBeDeleted: BrowserObject[] = [];
fetchSharedLink: (arg0: string) => Promisable<string> = () => 'javascript:null';
fetchPreviewAndMapUrl: (arg0: string) => Promisable<string> = () => 'javascript:null';
openedDropdown: null | string = null;
headingSorted = 'name';
orderBy: 'asc' | 'desc' = 'asc';
@ -164,8 +162,6 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
endpoint,
browserRoot,
openModalOnFirstUpload = true,
fetchSharedLink = () => 'javascript:null',
fetchPreviewAndMapUrl = () => 'javascript:null',
}: {
accessKey: string;
secretKey: string;
@ -173,8 +169,6 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
endpoint: string;
browserRoot: string;
openModalOnFirstUpload?: boolean;
fetchSharedLink: (arg0: string) => Promisable<string>;
fetchPreviewAndMapUrl: (arg0: string) => Promisable<string>;
}): void {
const s3Config: S3ClientConfig = {
credentials: {
@ -192,8 +186,6 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
state.bucket = bucket;
state.browserRoot = browserRoot;
state.openModalOnFirstUpload = openModalOnFirstUpload;
state.fetchSharedLink = fetchSharedLink;
state.fetchPreviewAndMapUrl = fetchPreviewAndMapUrl;
state.path = '';
state.files = [];
}
@ -810,8 +802,6 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
state.selectedFiles = [];
state.shiftSelectedFiles = [];
state.filesToBeDeleted = [];
state.fetchSharedLink = () => 'javascript:null';
state.fetchPreviewAndMapUrl = () => 'javascript:null';
state.openedDropdown = null;
state.headingSorted = 'name';
state.orderBy = 'asc';

View File

@ -22,3 +22,9 @@ export class ShareButtonConfig {
public image: string = EmailIcon,
) {}
}
export enum ShareType {
File = 'File',
Folder = 'Folder',
Bucket = 'Bucket',
}

View File

@ -90,7 +90,7 @@ export enum AnalyticsErrorEventSource {
CREATE_FOLDER_MODAL = 'Create folder modal',
OBJECT_DETAILS_MODAL = 'Object details modal',
OPEN_BUCKET_MODAL = 'Open bucket modal',
SHARE_BUCKET_MODAL = 'Share bucket modal',
SHARE_MODAL = 'Share modal',
OBJECTS_UPLOAD_MODAL = 'Objects upload modal',
NAVIGATION_ACCOUNT_AREA = 'Navigation account area',
NAVIGATION_PROJECT_SELECTION = 'Navigation project selection',

View File

@ -14,8 +14,7 @@ import MFARecoveryCodesModal from '@/components/modals/MFARecoveryCodesModal.vue
import EnableMFAModal from '@/components/modals/EnableMFAModal.vue';
import DisableMFAModal from '@/components/modals/DisableMFAModal.vue';
import AddTokenFundsModal from '@/components/modals/AddTokenFundsModal.vue';
import ShareBucketModal from '@/components/modals/ShareBucketModal.vue';
import ShareObjectModal from '@/components/modals/ShareObjectModal.vue';
import ShareModal from '@/components/modals/ShareModal.vue';
import DeleteBucketModal from '@/components/modals/DeleteBucketModal.vue';
import CreateBucketModal from '@/components/modals/CreateBucketModal.vue';
import NewFolderModal from '@/components/modals/NewFolderModal.vue';
@ -67,8 +66,7 @@ enum Modals {
ENABLE_MFA = 'enableMFA',
DISABLE_MFA = 'disableMFA',
ADD_TOKEN_FUNDS = 'addTokenFunds',
SHARE_BUCKET = 'shareBucket',
SHARE_OBJECT = 'shareObject',
SHARE = 'share',
DELETE_BUCKET = 'deleteBucket',
CREATE_BUCKET = 'createBucket',
NEW_FOLDER = 'newFolder',
@ -101,8 +99,7 @@ export const MODALS: Record<Modals, Component> = {
[Modals.ENABLE_MFA]: EnableMFAModal,
[Modals.DISABLE_MFA]: DisableMFAModal,
[Modals.ADD_TOKEN_FUNDS]: AddTokenFundsModal,
[Modals.SHARE_BUCKET]: ShareBucketModal,
[Modals.SHARE_OBJECT]: ShareObjectModal,
[Modals.SHARE]: ShareModal,
[Modals.DELETE_BUCKET]: DeleteBucketModal,
[Modals.CREATE_BUCKET]: CreateBucketModal,
[Modals.NEW_FOLDER]: NewFolderModal,