11import { Abortable } from '../../lib/abortable.js' ;
22import { i18n } from '../../lib/i18n/i18n.js' ;
33import { 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' ;
55import '../cc-smart-container/cc-smart-container.js' ;
66import { CcCellarNavigateToHomeEvent } from './cc-cellar-object-list.events.js' ;
77import './cc-cellar-object-list.js' ;
88
99/**
1010 * @import { CcCellarObjectList } from './cc-cellar-object-list.js'
11- * @import { CellarObjectListState, CellarObjectListStateLoaded, CellarObjectState, CellarFileState, CellarFileDetailsState } from './cc-cellar-object-list.types.js'
11+ * @import { CellarObjectListState, CellarObjectListStateLoaded, CellarObjectState, CellarFileState, CellarFileDetailsState, CellarDirectoryCreateFormState } from './cc-cellar-object-list.types.js'
1212 * @import { CellarExplorerClient } from '../cc-cellar-explorer/cc-cellar-explorer.client.js'
13+ * @import { CellarDirectory } from '../cc-cellar-explorer/cc-cellar-explorer.client.types.js'
1314 * @import { UpdateCallback } from '../common.types.js'
1415 * @import { OnEventCallback } from '../../lib/smart/smart-component.types.js'
1516 */
1617
18+ const DIRECTORY_INVALID_CHARS = / [ / \\ { } ^ % ` \] " > [ ~ < # | \u0080 - \u00FF ] / ;
19+
1720export class ObjectListController {
1821 /** @type {CellarExplorerClient } */
1922 #cellarClient;
@@ -86,6 +89,18 @@ export class ObjectListController {
8689 onEvent ( 'cc-cellar-object-delete' , ( objectKey ) => {
8790 this . deleteObject ( objectKey ) ;
8891 } ) ;
92+
93+ onEvent ( 'cc-cellar-object-create-directory' , ( directoryName ) => {
94+ this . createDirectory ( directoryName ) ;
95+ } ) ;
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+ } ) ;
89104 }
90105
91106 abort ( ) {
@@ -212,6 +227,32 @@ export class ObjectListController {
212227 }
213228 }
214229
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+
215256 async #fetchObjects( ) {
216257 try {
217258 const response = await this . #abortable. run ( ( ) =>
@@ -220,7 +261,9 @@ export class ObjectListController {
220261 filter : this . #filter,
221262 } ) ,
222263 ) ;
223- 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+ ) ;
224267 this . #nextCursor = response . cursor ;
225268 this . #updateState( {
226269 type : 'loaded' ,
@@ -310,4 +353,130 @@ export class ObjectListController {
310353 this . #signedUrlInterval = null ;
311354 }
312355 }
356+
357+ /**
358+ * @param {string } directoryName
359+ */
360+ async createDirectory ( directoryName ) {
361+ this . #updateCreateForm( { type : 'creating' , directoryName, error : null } ) ;
362+
363+ if ( DIRECTORY_INVALID_CHARS . test ( directoryName ) ) {
364+ this . #updateCreateForm( { type : 'idle' , directoryName, error : 'directory-name-invalid' } ) ;
365+ return ;
366+ }
367+
368+ const alreadyExists = this . #objects. some ( ( obj ) => obj . type === 'directory' && obj . name === directoryName ) ;
369+
370+ if ( alreadyExists ) {
371+ this . #updateCreateForm( { type : 'idle' , directoryName, error : 'directory-already-exists' } ) ;
372+ return ;
373+ }
374+
375+ /** @type {CellarDirectory } */
376+ const newDir = {
377+ type : 'directory' ,
378+ key : [ ...this . #path, directoryName ] . join ( '/' ) ,
379+ name : directoryName ,
380+ volatile : true ,
381+ } ;
382+
383+ this . #objects = [ ...this . #objects, newDir ] . sort ( ( a , b ) => {
384+ if ( a . volatile && ! b . volatile ) {
385+ return - 1 ;
386+ }
387+ if ( ! a . volatile && b . volatile ) {
388+ return 1 ;
389+ }
390+ if ( a . type === 'directory' && b . type === 'file' ) {
391+ return - 1 ;
392+ }
393+ if ( a . type === 'file' && b . type === 'directory' ) {
394+ return 1 ;
395+ }
396+ return a . name . localeCompare ( b . name ) ;
397+ } ) ;
398+
399+ this . #updateState(
400+ /** @param {CellarObjectListStateLoaded } state */ ( state ) => {
401+ state . objects = this . #objects;
402+ state . createDirectoryForm = null ;
403+ } ,
404+ ) ;
405+ notifySuccess ( i18n ( 'cc-cellar-object-list.success.directory-created' , { directoryName } ) ) ;
406+ }
407+
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+
468+ /**
469+ * @param {Partial<CellarDirectoryCreateFormState>|null } newState
470+ */
471+ #updateCreateForm( newState ) {
472+ this . #updateState(
473+ /** @param {CellarObjectListStateLoaded } state */ ( state ) => {
474+ if ( newState == null ) {
475+ state . createDirectoryForm = null ;
476+ } else {
477+ state . createDirectoryForm = { ...state . createDirectoryForm , ...newState } ;
478+ }
479+ } ,
480+ ) ;
481+ }
313482}
0 commit comments