Skip to content

Commit

Permalink
satellite/{web,console}: add duplicate upload warning
Browse files Browse the repository at this point in the history
This change adds a warning dialog to warn users of potential file
overwrites when they upload to an unversioned bucket.

Issue: #6925

Change-Id: I54d4b8913e19802ca04e0ecafc453a6319725e5c
  • Loading branch information
wilfred-asomanii authored and Storj Robot committed May 10, 2024
1 parent e156216 commit f9db000
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 14 deletions.
3 changes: 3 additions & 0 deletions satellite/console/consoleweb/endpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ func TestAuth(t *testing.T) {
ServerSideEncryption: false,
PartnerUpgradeBanner: false,
ProjectMembersPassphrase: false,
UploadOverwriteWarning: false,
}

testGetSettings(expectedSettings{
Expand All @@ -182,6 +183,7 @@ func TestAuth(t *testing.T) {
noticeDismissal.ServerSideEncryption = true
noticeDismissal.PartnerUpgradeBanner = true
noticeDismissal.ProjectMembersPassphrase = true
noticeDismissal.UploadOverwriteWarning = true
resp, _ := test.request(http.MethodPatch, "/auth/account/settings",
test.toJSON(map[string]interface{}{
"sessionDuration": duration,
Expand All @@ -194,6 +196,7 @@ func TestAuth(t *testing.T) {
"serverSideEncryption": noticeDismissal.ServerSideEncryption,
"partnerUpgradeBanner": noticeDismissal.PartnerUpgradeBanner,
"projectMembersPassphrase": noticeDismissal.ProjectMembersPassphrase,
"UploadOverwriteWarning": noticeDismissal.UploadOverwriteWarning,
},
}))

Expand Down
2 changes: 2 additions & 0 deletions satellite/console/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2056,6 +2056,7 @@ func TestUserSettings(t *testing.T) {
ServerSideEncryption: false,
PartnerUpgradeBanner: false,
ProjectMembersPassphrase: false,
UploadOverwriteWarning: false,
}
require.Equal(t, noticeDismissal, settings.NoticeDismissal)

Expand Down Expand Up @@ -2087,6 +2088,7 @@ func TestUserSettings(t *testing.T) {
noticeDismissal.FileGuide = true
noticeDismissal.PartnerUpgradeBanner = true
noticeDismissal.ProjectMembersPassphrase = true
noticeDismissal.UploadOverwriteWarning = true
settings, err = srv.SetUserSettings(userCtx, console.UpsertUserSettingsRequest{
SessionDuration: &sessionDurPtr,
OnboardingStart: &onboardingBool,
Expand Down
1 change: 1 addition & 0 deletions satellite/console/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ type NoticeDismissal struct {
ServerSideEncryption bool `json:"serverSideEncryption"`
PartnerUpgradeBanner bool `json:"partnerUpgradeBanner"`
ProjectMembersPassphrase bool `json:"projectMembersPassphrase"`
UploadOverwriteWarning bool `json:"uploadOverwriteWarning"`
}

// SetUpAccountRequest holds data for completing account setup.
Expand Down
2 changes: 2 additions & 0 deletions satellite/satellitedb/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@ func TestUserSettings(t *testing.T) {
ServerSideEncryption: false,
PartnerUpgradeBanner: false,
ProjectMembersPassphrase: false,
UploadOverwriteWarning: false,
}

require.NoError(t, users.UpsertSettings(ctx, id, console.UpsertUserSettingsRequest{}))
Expand All @@ -633,6 +634,7 @@ func TestUserSettings(t *testing.T) {
noticeDismissal.ServerSideEncryption = true
noticeDismissal.PartnerUpgradeBanner = true
noticeDismissal.ProjectMembersPassphrase = true
noticeDismissal.UploadOverwriteWarning = true
require.NoError(t, users.UpsertSettings(ctx, id, console.UpsertUserSettingsRequest{
NoticeDismissal: &noticeDismissal,
}))
Expand Down
1 change: 1 addition & 0 deletions web/satellite/src/components/BrowserTableComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ async function fetchFiles(): Promise<void> {
if (isFetching.value || props.forceEmpty) return;
obStore.updateSelectedFiles([]);
obStore.updateVersionsExpandedKeys([]);
isFetching.value = true;
try {
Expand Down
142 changes: 142 additions & 0 deletions web/satellite/src/components/dialogs/UploadOverwriteWarningDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.

<template>
<v-dialog
v-model="model"
width="auto"
min-width="400px"
max-width="450px"
transition="fade-transition"
>
<v-card rounded="xlg">
<v-sheet>
<v-card-item class="py-4 pl-6">
<template #prepend>
<v-sheet
class="border-sm d-flex justify-center align-center"
width="40"
height="40"
rounded="lg"
>
<icon-info size="22" />
</v-sheet>
</template>
<v-card-title class="font-weight-bold">
File Overwrite Warning
</v-card-title>
<template #append>
<v-btn
icon="$close"
variant="text"
size="small"
color="default"
@click="onCancel"
/>
</template>
</v-card-item>
</v-sheet>

<v-divider />

<v-row>
<v-col class="pa-6 mx-3">
<p class="my-2 font-weight-bold">
You are uploading to an unversioned bucket.
</p>
<p>
This non-exhaustive list of files you are uploading have the same names as existing files in this bucket:
</p>
<v-chip v-for="name in filenames" :key="name" class="font-weight-bold text-wrap mt-3 mr-1 py-2">
{{ name }}
</v-chip>
<p />
<v-alert color="default" border variant="tonal" class="my-4">
If you continue with the upload, the existing file(s) will be permanently overwritten, and previous versions cannot be recovered.
</v-alert>
<v-checkbox-btn v-model="dismissPermanently" density="comfortable" class="mb-2 ml-n2" label="Do not show this warning again." />
</v-col>
</v-row>

<v-divider />

<v-card-actions class="pa-6">
<v-row>
<v-col>
<v-btn :disabled="isLoading" variant="outlined" color="default" block @click="onCancel">Cancel</v-btn>
</v-col>
<v-col>
<v-btn :loading="isLoading" color="primary" variant="flat" block @click="onContinue">
Continue
</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import {
VAlert,
VBtn,
VCard,
VCardActions,
VCardItem,
VCardTitle,
VCheckboxBtn,
VChip,
VCol,
VDialog,
VDivider,
VRow,
VSheet,
} from 'vuetify/components';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useLoading } from '@/composables/useLoading';
import { useUsersStore } from '@/store/modules/usersStore';
import { useNotify } from '@/utils/hooks';
import IconInfo from '@/components/icons/IconInfo.vue';
const userStore = useUsersStore();
const notify = useNotify();
const { withLoading, isLoading } = useLoading();
withDefaults(defineProps<{
filenames: string[],
}>(), {
filenames: () => [],
});
const model = defineModel<boolean>({ required: true });
const emit = defineEmits(['proceed', 'cancel']);
const dismissPermanently = ref(false);
function onContinue() {
withLoading(async () => {
try {
if (dismissPermanently.value) {
const noticeDismissal = { ...userStore.state.settings.noticeDismissal };
noticeDismissal.uploadOverwriteWarning = true;
await userStore.updateSettings({ noticeDismissal });
}
model.value = false;
emit('proceed');
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.UPLOAD_OVERWRITE_WARNING_DIALOG);
}
});
}
async function onCancel() {
emit('cancel');
model.value = false;
}
</script>
27 changes: 26 additions & 1 deletion web/satellite/src/store/modules/objectBrowserStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { useAppStore } from '@/store/modules/appStore';
import { useNotificationsStore } from '@/store/modules/notificationsStore';
import { useConfigStore } from '@/store/modules/configStore';
import { DEFAULT_PAGE_LIMIT } from '@/types/pagination';
import { DuplicateUploadError } from '@/utils/error';

const listCache = new Map();

Expand Down Expand Up @@ -457,7 +458,7 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
state.objectsCount = (!response || response.KeyCount === undefined) ? 0 : response.KeyCount;
}

async function upload({ e }: { e: DragEvent | Event }): Promise<void> {
async function upload({ e }: { e: DragEvent | Event }, ignoreDuplicate = false): Promise<void> {
assertIsInitialized(state);

type Item = DataTransferItem | FileSystemEntry;
Expand Down Expand Up @@ -520,7 +521,31 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
)
.filter(isFileSystemEntry) as FileSystemEntry[];

const fileNames = state.files.map((file) => file.Key);
const files: { path: string, file: File }[] = [];
const duplicateFiles: string[] = [];
let traversedCount = 0;
for await (const { path, file } of traverse(iterator)) {
const directories = path.split('/');
const fileName = path + file.name;
const hasDuplicate = fileNames.includes(directories[0]) || fileNames.includes(fileName);
if (!ignoreDuplicate && duplicateFiles.length < 5 && hasDuplicate) {
duplicateFiles.push(fileName);
// if we have 5 duplicate files, or we have traversed 100 files, we stop the loop.
// and later throw DuplicateUploadError to notify the user of possible duplicates overwrites.
if (duplicateFiles.length === 5 || traversedCount === 100) {
break;
}
}
files.push({ path, file });
traversedCount++;
}

if (duplicateFiles.length > 0) {
throw new DuplicateUploadError(duplicateFiles);
}

for await (const { path, file } of files) {
const directories = path.split('/');
const fileName = directories.join('/') + file.name;
const key = state.path + fileName;
Expand Down
1 change: 1 addition & 0 deletions web/satellite/src/types/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ export interface NoticeDismissal {
serverSideEncryption: boolean
partnerUpgradeBanner: boolean
projectMembersPassphrase: boolean
uploadOverwriteWarning?: boolean;
}

export interface SetUserSettingsData {
Expand Down
1 change: 1 addition & 0 deletions web/satellite/src/utils/constants/analyticsEventNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export enum AnalyticsErrorEventSource {
VERSIONING_TOGGLE_DIALOG = 'Versioning toggle dialog',
VERSIONING_BETA_DIALOG = 'Versioning beta dialog',
VERSIONING_BETA_BANNER = 'Versioning beta banner',
UPLOAD_OVERWRITE_WARNING_DIALOG = 'Upload Overwrite Warning Dialog',
}

export enum PageVisitSource {
Expand Down
15 changes: 13 additions & 2 deletions web/satellite/src/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

/**
* A custom error class with status and requestID properties.
* */
*/
export class APIError extends Error {
public status: number;
public requestID: string | null;
Expand All @@ -16,12 +16,23 @@ export class APIError extends Error {

/**
* Returns a new APIError with the same status and requestID but with a different message.
* */
*/
public withMessage(message: string): APIError {
return new APIError({
status: this.status,
message,
requestID: this.requestID,
});
}
}

/**
* A custom error class for reporting duplicate file uploads.
*/
export class DuplicateUploadError extends Error {
constructor(
public files: string[],
) {
super('Duplicate upload');
}
}

0 comments on commit f9db000

Please sign in to comment.