web/satellite: added notification when storage limit is exceeded in file browser

Added error notification when user tries to upload object after storage limit is exceeded.
Slightly changed error handling on failed preview/map request.

Issue: https://github.com/storj/storj/issues/5003

Change-Id: Iee91c1037b5c6de7b9718adcdfcea850fd3056ca
This commit is contained in:
Vitalii 2022-07-28 15:39:05 +03:00 committed by Vitalii Shpital
parent 27f6fbdeda
commit e1a85fccf4
3 changed files with 283 additions and 261 deletions

View File

@ -28,31 +28,40 @@
"
>
<img
v-if="previewIsImage"
ref="previewImage"
class="preview img-fluid"
src="/static/static/images/common/loader.svg"
aria-roledescription="image-preview"
v-if="previewFailed"
class="failed-preview"
src="/static/static/images/common/errorNotice.svg"
alt="failed preview"
>
<template v-else>
<img
v-if="previewIsImage"
ref="previewImage"
class="preview img-fluid"
src="/static/static/images/common/loader.svg"
aria-roledescription="image-preview"
alt="preview"
>
<video
v-if="previewIsVideo"
ref="previewVideo"
class="preview"
controls
src=""
aria-roledescription="video-preview"
/>
<video
v-if="previewIsVideo"
ref="previewVideo"
class="preview"
controls
src=""
aria-roledescription="video-preview"
/>
<audio
v-if="previewIsAudio"
ref="previewAudio"
class="preview"
controls
src=""
aria-roledescription="audio-preview"
/>
<PlaceholderImage v-if="placeHolderDisplayable" />
<audio
v-if="previewIsAudio"
ref="previewAudio"
class="preview"
controls
src=""
aria-roledescription="audio-preview"
/>
<PlaceholderImage v-if="placeHolderDisplayable" />
</template>
</div>
</div>
<div class="col-6 col-lg-4 pr-5">
@ -176,8 +185,7 @@
</svg>
</span>
</button>
<div class="mt-5">
<div v-if="!mapFailed" class="mt-5">
<div class="storage-nodes">
Nodes storing this file
</div>
@ -210,6 +218,8 @@ import PlaceholderImage from '@/../static/images/browser/placeholder.svg'
export default class FileModal extends Vue {
public objectLink = '';
public copyText = 'Copy Link';
public previewFailed = false;
public mapFailed = false;
public $refs!: {
objectMap: HTMLImageElement;
@ -328,23 +338,20 @@ export default class FileModal extends Vue {
* Get the object map for the file being displayed.
*/
private async fetchObjectMap(): Promise<void> {
try {
if (!this.$store.state.files.fetchObjectMap) {
return;
}
const objectMap: Blob | null = await this.$store.state.files.fetchObjectMap(
this.filePath
);
if (!objectMap) {
return;
}
this.$refs.objectMap.src = URL.createObjectURL(objectMap);
} catch (error) {
await this.$notify.error(error.message);
if (!this.$store.state.files.fetchObjectMap) {
return;
}
const objectMap: Blob | null = await this.$store.state.files.fetchObjectMap(
this.filePath
);
if (!objectMap) {
this.mapFailed = true;
return;
}
this.$refs.objectMap.src = URL.createObjectURL(objectMap);
}
/**
@ -363,33 +370,30 @@ export default class FileModal extends Vue {
* Set preview object.
*/
public async setPreview(): Promise<void> {
try {
if (!this.$store.state.files.fetchObjectPreview) {
return;
}
if (!this.$store.state.files.fetchObjectPreview) {
return;
}
const object: Blob | null = await this.$store.state.files.fetchObjectPreview(
this.filePath
);
const object: Blob | null = await this.$store.state.files.fetchObjectPreview(
this.filePath
);
if (!object) {
return;
}
if (!object) {
this.previewFailed = true
return;
}
const objectURL = URL.createObjectURL(object);
const objectURL = URL.createObjectURL(object);
switch (true) {
case this.previewIsImage:
this.$refs.previewImage.src = objectURL;
break;
case this.previewIsVideo:
this.$refs.previewVideo.src = objectURL;
break;
case this.previewIsAudio:
this.$refs.previewAudio.src = objectURL;
}
} catch (error) {
await this.$notify.error(error.message);
switch (true) {
case this.previewIsImage:
this.$refs.previewImage.src = objectURL;
break;
case this.previewIsVideo:
this.$refs.previewVideo.src = objectURL;
break;
case this.previewIsAudio:
this.$refs.previewAudio.src = objectURL;
}
}
@ -404,7 +408,7 @@ export default class FileModal extends Vue {
* Copy the current opened file.
*/
public async copy(): Promise<void> {
await navigator.clipboard.writeText(this.objectLink);
await this.$copyText(this.objectLink);
this.copyText = 'Copied!';
setTimeout(() => {
this.copyText = 'Copy Link';
@ -529,4 +533,8 @@ export default class FileModal extends Vue {
font-size: 14px;
padding: 0 16px;
}
.failed-preview {
width: 50%;
}
</style>

View File

@ -75,14 +75,26 @@ export default class UploadFile extends Vue {
* Generates a URL for an object map.
*/
public async fetchObjectMap(path: string): Promise<Blob | null> {
return await this.getObjectViewOrMapBySignedRequest(path, true)
try {
return await this.getObjectViewOrMapBySignedRequest(path, true)
} catch (error) {
await this.$notify.error('Failed to fetch object map. Bandwidth limit may be exceeded');
return null
}
}
/**
* Generates a URL for an object map.
*/
public async fetchObjectPreview(path: string): Promise<Blob | null> {
return await this.getObjectViewOrMapBySignedRequest(path, false)
try {
return await this.getObjectViewOrMapBySignedRequest(path, false)
} catch (error) {
await this.$notify.error('Failed to fetch object view. Bandwidth limit may be exceeded');
return null
}
}
/**
@ -123,65 +135,64 @@ export default class UploadFile extends Vue {
* Returns a URL for an object or a map.
*/
private async getObjectViewOrMapBySignedRequest(path: string, isMap: boolean): Promise<Blob | null> {
path = `${this.bucket}/${path}`;
path = encodeURIComponent(path.trim());
const url = new URL(`${this.linksharingURL}/s/${this.credentials.accessKeyId}/${path}`)
let request: HttpRequest = {
method: 'GET',
protocol: url.protocol,
hostname: url.hostname,
port: parseFloat(url.port),
path: url.pathname,
headers: {
'host': url.host,
}
}
if (isMap) {
request = Object.assign(request, {query: { 'map': '1' }});
} else {
request = Object.assign(request, {query: { 'view': '1' }});
}
const signerCredentials: Credentials = {
accessKeyId: this.credentials.accessKeyId,
secretAccessKey: this.credentials.secretKey,
};
const signer = new SignatureV4({
applyChecksum: true,
uriEscapePath: false,
credentials: signerCredentials,
region: "eu1",
service: "linksharing",
sha256: Sha256,
});
let signedRequest: HttpRequest;
try {
path = `${this.bucket}/${path}`;
path = encodeURIComponent(path.trim());
const url = new URL(`${this.linksharingURL}/s/${this.credentials.accessKeyId}/${path}`)
let request: HttpRequest = {
method: 'GET',
protocol: url.protocol,
hostname: url.hostname,
port: parseFloat(url.port),
path: url.pathname,
headers: {
'host': url.host,
}
}
if (isMap) {
request = Object.assign(request, {query: { 'map': '1' }});
} else {
request = Object.assign(request, {query: { 'view': '1' }});
}
const signerCredentials: Credentials = {
accessKeyId: this.credentials.accessKeyId,
secretAccessKey: this.credentials.secretKey,
};
const signer = new SignatureV4({
applyChecksum: true,
uriEscapePath: false,
credentials: signerCredentials,
region: "eu1",
service: "linksharing",
sha256: Sha256,
});
const signedRequest: HttpRequest = await signer.sign(request);
let requestURL = `${this.linksharingURL}${signedRequest.path}`;
if (isMap) {
requestURL = `${requestURL}?map=1`;
} else {
requestURL = `${requestURL}?view=1`;
}
const response = await fetch(requestURL, signedRequest);
if (response.ok) {
return await response.blob();
}
await this.$notify.error(`${response.status}. Failed to fetch object view or map`);
return null;
signedRequest = await signer.sign(request);
} catch (error) {
await this.$notify.error(error.message);
return null;
}
let requestURL = `${this.linksharingURL}${signedRequest.path}`;
if (isMap) {
requestURL = `${requestURL}?map=1`;
} else {
requestURL = `${requestURL}?view=1`;
}
const response = await fetch(requestURL, signedRequest);
if (response.ok) {
return await response.blob();
}
return null;
}
/**

View File

@ -291,7 +291,7 @@ export const makeFilesModule = (): FilesModule => ({
},
actions: {
async list({ commit, state }, path = state.path) {
if (listCache.has(path) === true) {
if (listCache.has(path)) {
commit("updateFiles", {
path,
files: listCache.get(path),
@ -323,10 +323,10 @@ export const makeFilesModule = (): FilesModule => ({
Contents.sort((a, b) => {
if (
a === undefined ||
a.LastModified === undefined ||
b === undefined ||
b.LastModified === undefined ||
a.LastModified === b.LastModified
a.LastModified === undefined ||
b === undefined ||
b.LastModified === undefined ||
a.LastModified === b.LastModified
) {
return 0;
}
@ -334,44 +334,44 @@ export const makeFilesModule = (): FilesModule => ({
return a.LastModified < b.LastModified ? -1 : 1;
});
type DefinedCommonPrefix = CommonPrefix & {
Prefix: string;
};
type DefinedCommonPrefix = CommonPrefix & {
Prefix: string;
};
const isPrefixDefined = (
value: CommonPrefix
): value is DefinedCommonPrefix => value.Prefix !== undefined;
const isPrefixDefined = (
value: CommonPrefix
): value is DefinedCommonPrefix => value.Prefix !== undefined;
const prefixToFolder = ({
Prefix,
}: {
Prefix: string;
}): BrowserObject => ({
Key: Prefix.slice(path.length, -1),
LastModified: 0,
Size: 0,
type: "folder",
});
const prefixToFolder = ({
Prefix,
}: {
Prefix: string;
}): BrowserObject => ({
Key: Prefix.slice(path.length, -1),
LastModified: 0,
Size: 0,
type: "folder",
});
const makeFileRelative = (file) => ({
...file,
Key: file.Key.slice(path.length),
type: "file",
});
const makeFileRelative = (file) => ({
...file,
Key: file.Key.slice(path.length),
type: "file",
});
const isFileVisible = (file) =>
file.Key.length > 0 && file.Key !== ".file_placeholder";
const isFileVisible = (file) =>
file.Key.length > 0 && file.Key !== ".file_placeholder";
const files = [
...CommonPrefixes.filter(isPrefixDefined).map(prefixToFolder),
...Contents.map(makeFileRelative).filter(isFileVisible),
];
const files = [
...CommonPrefixes.filter(isPrefixDefined).map(prefixToFolder),
...Contents.map(makeFileRelative).filter(isFileVisible),
];
listCache.set(path, files);
commit("updateFiles", {
path,
files,
});
listCache.set(path, files);
commit("updateFiles", {
path,
files,
});
},
async back({ state, dispatch }) {
@ -391,136 +391,139 @@ export const makeFilesModule = (): FilesModule => ({
async upload({ commit, state, dispatch }, e: DragEvent) {
assertIsInitialized(state);
type Item = DataTransferItem | FileSystemEntry;
type Item = DataTransferItem | FileSystemEntry;
const items: Item[] = e.dataTransfer
? [...e.dataTransfer.items]
: e.target !== null
? ((e.target as unknown) as { files: FileSystemEntry[] }).files
: [];
const items: Item[] = e.dataTransfer
? [...e.dataTransfer.items]
: e.target !== null
? ((e.target as unknown) as { files: FileSystemEntry[] }).files
: [];
async function* traverse(item: Item | Item[], path = "") {
if ('isFile' in item && item.isFile === true) {
const file = await new Promise(item.file.bind(item));
yield { path, file };
} else if (item instanceof File) {
let relativePath = item.webkitRelativePath
.split("/")
.slice(0, -1)
.join("/");
async function* traverse(item: Item | Item[], path = "") {
if ('isFile' in item && item.isFile === true) {
const file = await new Promise(item.file.bind(item));
yield { path, file };
} else if (item instanceof File) {
let relativePath = item.webkitRelativePath
.split("/")
.slice(0, -1)
.join("/");
if (relativePath.length) {
relativePath += "/";
}
if (relativePath.length) {
relativePath += "/";
}
yield { path: relativePath, file: item };
} else if ('isFile' in item && item.isDirectory) {
const dirReader = item.createReader();
yield { path: relativePath, file: item };
} else if ('isFile' in item && item.isDirectory) {
const dirReader = item.createReader();
const entries = await new Promise(
dirReader.readEntries.bind(dirReader)
);
const entries = await new Promise(
dirReader.readEntries.bind(dirReader)
);
for (const entry of entries) {
yield* traverse(
for (const entry of entries) {
yield* traverse(
(entry as FileSystemEntry) as Item,
path + item.name + "/"
);
}
} else if ("length" in item && typeof item.length === "number") {
for (const i of item) {
yield* traverse(i);
}
} else {
throw new Error("Item is not directory or file");
}
}
);
}
} else if ("length" in item && typeof item.length === "number") {
for (const i of item) {
yield* traverse(i);
}
} else {
throw new Error("Item is not directory or file");
}
}
const isFileSystemEntry = (
a: FileSystemEntry | null
): a is FileSystemEntry => a !== null;
const isFileSystemEntry = (
a: FileSystemEntry | null
): a is FileSystemEntry => a !== null;
const iterator = [...items]
.map((item) =>
"webkitGetAsEntry" in item ? item.webkitGetAsEntry() : item
)
.filter(isFileSystemEntry) as FileSystemEntry[];
const iterator = [...items]
.map((item) =>
"webkitGetAsEntry" in item ? item.webkitGetAsEntry() : item
)
.filter(isFileSystemEntry) as FileSystemEntry[];
const fileNames = state.files.map((file) => file.Key);
const fileNames = state.files.map((file) => file.Key);
function getUniqueFileName(fileName) {
for (let count = 1; fileNames.includes(fileName); count++) {
if (count > 1) {
fileName = fileName.replace(/\((\d+)\)(.*)/, `(${count})$2`);
} else {
fileName = fileName.replace(/([^.]*)(.*)/, `$1 (${count})$2`);
}
}
function getUniqueFileName(fileName) {
for (let count = 1; fileNames.includes(fileName); count++) {
if (count > 1) {
fileName = fileName.replace(/\((\d+)\)(.*)/, `(${count})$2`);
} else {
fileName = fileName.replace(/([^.]*)(.*)/, `$1 (${count})$2`);
}
}
return fileName;
}
return fileName;
}
for await (const { path, file } of traverse(iterator)) {
const directories = path.split("/");
const uniqueFirstDirectory = getUniqueFileName(directories[0]);
directories[0] = uniqueFirstDirectory;
for await (const { path, file } of traverse(iterator)) {
const directories = path.split("/");
directories[0] = getUniqueFileName(directories[0]);
const fileName = getUniqueFileName(directories.join("/") + file.name);
const fileName = getUniqueFileName(directories.join("/") + file.name);
const params = {
Bucket: state.bucket,
Key: state.path + fileName,
Body: file,
};
const params = {
Bucket: state.bucket,
Key: state.path + fileName,
Body: file,
};
const upload = state.s3.upload(
{ ...params },
{ partSize: 64 * 1024 * 1024 }
);
const upload = state.s3.upload(
{ ...params },
{ partSize: 64 * 1024 * 1024 }
);
upload.on("httpUploadProgress", (progress) => {
commit("setProgress", {
Key: params.Key,
progress: Math.round((progress.loaded / progress.total) * 100),
});
});
upload.on("httpUploadProgress", (progress) => {
commit("setProgress", {
Key: params.Key,
progress: Math.round((progress.loaded / progress.total) * 100),
});
});
commit("pushUpload", {
...params,
upload,
progress: 0,
});
commit("pushUpload", {
...params,
upload,
progress: 0,
});
commit("addUploadToChain", async () => {
if (
state.uploading.findIndex((file) => file.Key === params.Key) === -1
) {
// upload cancelled or removed
return -1;
}
commit("addUploadToChain", async () => {
if (
state.uploading.findIndex((file) => file.Key === params.Key) === -1
) {
// upload cancelled or removed
return -1;
}
try {
await upload.promise();
} catch (e) {
// An error is raised if the upload is aborted by the user
console.log(e);
}
try {
await upload.promise();
} catch (error) {
const limitExceededError = 'storage limit exceeded'
if (error.message.includes(limitExceededError)) {
dispatch('error', `Error: ${limitExceededError}`, {root:true})
} else {
dispatch('error', error.message, {root:true})
}
}
await dispatch("list");
await dispatch("list");
const uploadedFiles = state.files.filter(
(file) => file.type === "file"
);
const uploadedFiles = state.files.filter(
(file) => file.type === "file"
);
if (uploadedFiles.length === 1) {
if (state.openModalOnFirstUpload === true) {
commit("openModal", params.Key);
}
}
if (uploadedFiles.length === 1) {
if (state.openModalOnFirstUpload === true) {
commit("openModal", params.Key);
}
}
commit("finishUpload", params.Key);
});
}
commit("finishUpload", params.Key);
});
}
},
async createFolder({ state, dispatch }, name) {