Skip to content

Commit c99f755

Browse files
committed
feat(cc-cellar-explorer): add objects upload
1 parent 5e35a22 commit c99f755

8 files changed

Lines changed: 177 additions & 8 deletions

File tree

src/components/cc-cellar-explorer/cc-cellar-explorer.client.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,36 @@ export class CellarExplorerClient {
154154
.then(sendToApi({ apiConfig: this._apiConfig }))
155155
.catch(catchError);
156156
}
157+
158+
/**
159+
* @param {string} bucketName
160+
* @param {string} objectName
161+
* @param {File} file
162+
*/
163+
async uploadObject(bucketName, objectName, file) {
164+
const result = await Promise.resolve({
165+
method: 'post',
166+
url: `/v4/cellar/organisations/${this._ownerId}/cellar/${this._addonId}/buckets/${encodeURIComponent(bucketName)}/objects/${encodeURIComponent(objectName)}/presigned-url`,
167+
})
168+
.then(sendToApi({ apiConfig: this._apiConfig }))
169+
.catch(catchError);
170+
171+
const presignedUrl = result.url;
172+
173+
const response = await fetch(presignedUrl, {
174+
method: 'post',
175+
headers: { 'Content-Type': file.type ?? 'application/octet-stream' },
176+
body: file,
177+
});
178+
179+
if (!response.ok) {
180+
const body = await response.json().catch(() => /** @type {null} */ (null));
181+
if (body?.code != null) {
182+
throw new CellarExplorerError(body.code, body.error, body.context);
183+
}
184+
throw new Error(`Upload failed with status ${response.status}`);
185+
}
186+
}
157187
}
158188

159189
/**

src/components/cc-cellar-explorer/cc-cellar-explorer.client.types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface CellarFile {
2828
name: string;
2929
updatedAt: string;
3030
contentLength: number;
31+
volatile?: boolean;
3132
}
3233

3334
export interface CellarDirectory {

src/components/cc-cellar-object-list/cc-cellar-object-list.ctrl.js

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Abortable } from '../../lib/abortable.js';
22
import { i18n } from '../../lib/i18n/i18n.js';
33
import { notifyError, notifySuccess } from '../../lib/notifications.js';
4-
import { isCellarExplorerErrorWithCode } from '../cc-cellar-explorer/cc-cellar-explorer.client.js';
4+
import { isCellarExplorerErrorWithCode, pathToString } from '../cc-cellar-explorer/cc-cellar-explorer.client.js';
55
import '../cc-smart-container/cc-smart-container.js';
66
import { CcCellarNavigateToHomeEvent } from './cc-cellar-object-list.events.js';
77
import './cc-cellar-object-list.js';
@@ -93,6 +93,14 @@ export class ObjectListController {
9393
onEvent('cc-cellar-object-create-directory', (directoryName) => {
9494
this.createDirectory(directoryName);
9595
});
96+
97+
onEvent('cc-cellar-object-download', (objectKey) => {
98+
this.download(objectKey);
99+
});
100+
101+
onEvent('cc-cellar-object-upload', (file) => {
102+
this.uploadObject(file);
103+
});
96104
}
97105

98106
abort() {
@@ -219,6 +227,32 @@ export class ObjectListController {
219227
}
220228
}
221229

230+
/**
231+
* @param {string} objectKey
232+
* @returns {Promise<void>}
233+
*/
234+
async download(objectKey) {
235+
this.#updateDetails({ state: 'downloading' });
236+
try {
237+
const signedUrl = await this.#cellarClient.getObjectSignedUrl(this.#bucketName, objectKey);
238+
239+
const element = document.createElement('a');
240+
element.setAttribute('href', signedUrl.url);
241+
element.setAttribute('target', '_blank');
242+
document.body.appendChild(element);
243+
element.click();
244+
document.body.removeChild(element);
245+
} catch (error) {
246+
this.#handleErrorOnObject({
247+
error,
248+
objectKey,
249+
orElse: () => notifyError(i18n('cc-cellar-object-list.error.object-download-failed', { objectKey })),
250+
});
251+
} finally {
252+
this.#updateDetails({ state: 'idle' });
253+
}
254+
}
255+
222256
async #fetchObjects() {
223257
try {
224258
const response = await this.#abortable.run(() =>
@@ -227,7 +261,9 @@ export class ObjectListController {
227261
filter: this.#filter,
228262
}),
229263
);
230-
this.#objects = [...response.directories, ...response.content].map((object) => ({ state: 'idle', ...object }));
264+
this.#objects = [...response.directories, ...response.content].map((object) =>
265+
object.type === 'file' ? /** @type {CellarFileState} */ ({ ...object, state: 'idle' }) : object,
266+
);
231267
this.#nextCursor = response.cursor;
232268
this.#updateState({
233269
type: 'loaded',
@@ -369,6 +405,66 @@ export class ObjectListController {
369405
notifySuccess(i18n('cc-cellar-object-list.success.directory-created', { directoryName }));
370406
}
371407

408+
/**
409+
* @param {File} file
410+
* @returns {Promise<void>}
411+
*/
412+
async uploadObject(file) {
413+
const pathAtUploadStart = this.#path;
414+
const objectKey = pathToString(this.#path) + file.name;
415+
this.#updateUploadState({ type: 'uploading' });
416+
417+
try {
418+
await this.#cellarClient.uploadObject(this.#bucketName, objectKey, file);
419+
notifySuccess(i18n('cc-cellar-object-list.success.object-uploaded', { objectKey }));
420+
421+
if (this.#path === pathAtUploadStart) {
422+
/** @type {CellarFileState} */
423+
const newFile = {
424+
type: 'file',
425+
key: objectKey,
426+
name: file.name,
427+
updatedAt: new Date().toISOString(),
428+
contentLength: file.size,
429+
volatile: true,
430+
state: 'idle',
431+
};
432+
this.#objects = [newFile, ...this.#objects.filter((obj) => obj.key !== objectKey)];
433+
this.#updateState(
434+
/** @param {CellarObjectListStateLoaded} state */ (state) => {
435+
state.objects = this.#objects;
436+
state.uploadState = { type: 'idle' };
437+
},
438+
);
439+
}
440+
} catch (error) {
441+
if (this.#path === pathAtUploadStart) {
442+
this.#handleErrorOnObject({
443+
error,
444+
objectKey,
445+
orElse: () => notifyError(i18n('cc-cellar-object-list.error.object-upload-failed', { objectKey })),
446+
});
447+
} else {
448+
notifyError(i18n('cc-cellar-object-list.error.object-upload-failed', { objectKey }));
449+
}
450+
} finally {
451+
if (this.#path === pathAtUploadStart) {
452+
this.#updateUploadState({ type: 'idle' });
453+
}
454+
}
455+
}
456+
457+
/**
458+
* @param {import('./cc-cellar-object-list.types.js').CellarObjectUploadState} newState
459+
*/
460+
#updateUploadState(newState) {
461+
this.#updateState(
462+
/** @param {CellarObjectListStateLoaded} state */ (state) => {
463+
state.uploadState = newState;
464+
},
465+
);
466+
}
467+
372468
/**
373469
* @param {Partial<CellarDirectoryCreateFormState>|null} newState
374470
*/

src/components/cc-cellar-object-list/cc-cellar-object-list.events.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,21 @@ export class CcCellarObjectDeleteEvent extends CcEvent {
123123
}
124124
}
125125

126+
/**
127+
* Dispatched when a Cellar object download is requested.
128+
* @extends {CcEvent<string>}
129+
*/
130+
export class CcCellarObjectDownloadEvent extends CcEvent {
131+
static TYPE = 'cc-cellar-object-download';
132+
133+
/**
134+
* @param {string} details
135+
*/
136+
constructor(details) {
137+
super(CcCellarObjectDownloadEvent.TYPE, details);
138+
}
139+
}
140+
126141
/**
127142
* Dispatched when a new directory is created.
128143
* @extends {CcEvent<string>}
@@ -137,3 +152,18 @@ export class CcCellarObjectCreateDirectoryEvent extends CcEvent {
137152
super(CcCellarObjectCreateDirectoryEvent.TYPE, details);
138153
}
139154
}
155+
156+
/**
157+
* Dispatched when a file upload is requested.
158+
* @extends {CcEvent<File>}
159+
*/
160+
export class CcCellarObjectUploadEvent extends CcEvent {
161+
static TYPE = 'cc-cellar-object-upload';
162+
163+
/**
164+
* @param {File} file
165+
*/
166+
constructor(file) {
167+
super(CcCellarObjectUploadEvent.TYPE, file);
168+
}
169+
}

src/lib/events-map.types.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@ import {
3333
CcCellarNavigateToPreviousPageEvent,
3434
CcCellarObjectCreateDirectoryEvent,
3535
CcCellarObjectDeleteEvent,
36+
CcCellarObjectDownloadEvent,
3637
CcCellarObjectFilterEvent,
3738
CcCellarObjectHideEvent,
3839
CcCellarObjectShowEvent,
40+
CcCellarObjectUploadEvent,
3941
} from '../components/cc-cellar-object-list/cc-cellar-object-list.events.js';
4042
import {
4143
CcDomainAddEvent,
@@ -244,9 +246,11 @@ declare global {
244246
'cc-cellar-navigate-to-previous-page': CcCellarNavigateToPreviousPageEvent;
245247
'cc-cellar-object-create-directory': CcCellarObjectCreateDirectoryEvent;
246248
'cc-cellar-object-delete': CcCellarObjectDeleteEvent;
249+
'cc-cellar-object-download': CcCellarObjectDownloadEvent;
247250
'cc-cellar-object-filter': CcCellarObjectFilterEvent;
248251
'cc-cellar-object-hide': CcCellarObjectHideEvent;
249252
'cc-cellar-object-show': CcCellarObjectShowEvent;
253+
'cc-cellar-object-upload': CcCellarObjectUploadEvent;
250254
'cc-click': CcClickEvent;
251255
'cc-close': CcCloseEvent;
252256
'cc-close-request': CcCloseRequestEvent;

src/stories/fixtures/cellar.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const bucket1 = {
1414
updatedAt: new Date().toISOString(),
1515
sizeInBytes: random(150_000, 2_000_000),
1616
objectsCount: random(1_000, 1_000_000),
17-
versioning: 'enabled',
17+
versioning: 'ENABLED',
1818
};
1919

2020
/** @type {CellarBucketState} */
@@ -36,7 +36,7 @@ export const bucketEmpty = {
3636
updatedAt: new Date().toISOString(),
3737
sizeInBytes: 0,
3838
objectsCount: 0,
39-
versioning: 'enabled',
39+
versioning: 'ENABLED',
4040
};
4141

4242
/** @type {CellarBucketState} */
@@ -129,6 +129,7 @@ export function fileState(extension, path ) {
129129
export function fileDetailsState(extension, path ) {
130130
return {
131131
state: 'idle',
132+
signedUrl:'https://cellar-c2.services.clever-cloud.com/bucket-name/file-1.extension?X-Amz-Signature=fake',
132133
...fileDetails(extension, path),
133134
}
134135
}
@@ -185,4 +186,4 @@ function generateDirectories(count) {
185186
}
186187

187188
return count.map(toDir);
188-
}
189+
}

src/translations/translations.en.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,7 @@ export const translations = {
502502
'cc-cellar-object-list.add-directory.dialog.submit': `Create directory`,
503503
'cc-cellar-object-list.back-to-bucket-list': `Go back to list of buckets`,
504504
'cc-cellar-object-list.badge.new': `New`,
505+
'cc-cellar-object-list.button.upload': `Upload object`,
505506
'cc-cellar-object-list.create.directory-name.help': `Directory name cannot contain non-printable characters (128–255 decimal characters) or the following characters: / \\ { } ^ % \` [ ] " < > ~ # |`,
506507
'cc-cellar-object-list.date': /** @param {{date: string}} _ */ ({ date }) => formatDateOnly(date),
507508
'cc-cellar-object-list.details.actions.delete.button': `Delete object`,
@@ -532,10 +533,14 @@ export const translations = {
532533
'cc-cellar-object-list.error.directory-name-invalid': `The directory name contains an invalid character (see the help message above)`,
533534
'cc-cellar-object-list.error.object-deletion-failed': /** @param {{objectKey: string}} _ */ ({ objectKey }) =>
534535
`Failed to delete object ${objectKey}`,
536+
'cc-cellar-object-list.error.object-download-failed': /** @param {{objectKey: string}} _ */ ({ objectKey }) =>
537+
`Failed to download object ${objectKey}`,
535538
'cc-cellar-object-list.error.object-fetch-failed': /** @param {{objectKey: string}} _ */ ({ objectKey }) =>
536539
`Failed to get object ${objectKey}`,
537540
'cc-cellar-object-list.error.object-not-found': /** @param {{objectKey: string}} _ */ ({ objectKey }) =>
538541
`Object ${objectKey} does not exist`,
542+
'cc-cellar-object-list.error.object-upload-failed': /** @param {{objectKey: string}} _ */ ({ objectKey }) =>
543+
`Failed to upload object ${objectKey}`,
539544
'cc-cellar-object-list.grid.a11y-name': `List of objects`,
540545
'cc-cellar-object-list.grid.column.last-update': `Last update`,
541546
'cc-cellar-object-list.grid.column.name': `Name`,
@@ -563,6 +568,8 @@ export const translations = {
563568
`Object ${objectKey} was already deleted`,
564569
'cc-cellar-object-list.success.object-deleted': /** @param {{objectKey: string}} _ */ ({ objectKey }) =>
565570
`Object ${objectKey} deleted successfully`,
571+
'cc-cellar-object-list.success.object-uploaded': /** @param {{objectKey: string}} _ */ ({ objectKey }) =>
572+
`Object ${objectKey} uploaded successfully`,
566573
//#endregion
567574
//#region cc-clipboard
568575
'cc-clipboard.copied': `The text has been copied`,

src/translations/translations.fr.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ export const translations = {
513513
'cc-cellar-object-list.add-directory.dialog.submit': `Créer le dossier`,
514514
'cc-cellar-object-list.back-to-bucket-list': `Retour à la liste des buckets`,
515515
'cc-cellar-object-list.badge.new': `Nouveau`,
516-
'cc-cellar-object-list.button.upload': `Téléverser un objet`,
516+
'cc-cellar-object-list.button.upload': `Uploader un objet`,
517517
'cc-cellar-object-list.create.directory-name.help': `Le nom du dossier ne peut pas contenir de caractères ASCII non imprimables (128–255 caractères décimaux) ni les caractères suivants : / \\ { } ^ % \` [ ] " < > ~ # |`,
518518
'cc-cellar-object-list.date': /** @param {{date: string}} _ */ ({ date }) => formatDateOnly(date),
519519
'cc-cellar-object-list.details.actions.delete.button': `Supprimer l'objet`,
@@ -551,7 +551,7 @@ export const translations = {
551551
'cc-cellar-object-list.error.object-not-found': /** @param {{objectKey: string}} _ */ ({ objectKey }) =>
552552
`L'objet ${objectKey} n'existe pas`,
553553
'cc-cellar-object-list.error.object-upload-failed': /** @param {{objectKey: string}} _ */ ({ objectKey }) =>
554-
`Le téléversement de l'objet ${objectKey} a échoué`,
554+
`L'upload de l'objet ${objectKey} a échoué`,
555555
'cc-cellar-object-list.grid.a11y-name': `Liste des objets`,
556556
'cc-cellar-object-list.grid.column.last-update': `Dernière modification`,
557557
'cc-cellar-object-list.grid.column.name': `Nom`,
@@ -580,7 +580,7 @@ export const translations = {
580580
'cc-cellar-object-list.success.object-deleted': /** @param {{objectKey: string}} _ */ ({ objectKey }) =>
581581
`L'objet ${objectKey} a été supprimé avec succès`,
582582
'cc-cellar-object-list.success.object-uploaded': /** @param {{objectKey: string}} _ */ ({ objectKey }) =>
583-
`L'objet ${objectKey} a été téléversé avec succès`,
583+
`L'objet ${objectKey} a été uploadé avec succès`,
584584
//#endregion
585585
//#region cc-clipboard
586586
'cc-clipboard.copied': `Le texte a été copié`,

0 commit comments

Comments
 (0)