1- <!-- S3 Bucket Explorer Version: 3.0.8 -->
1+ <!-- S3 Bucket Explorer Version: 3.1.0 -->
22
33<!DOCTYPE html>
44< html lang ="en " style ="overflow-y: auto; ">
1212 logo : 'https://qoomon.github.io/aws-s3-bucket-browser/logo.png' ,
1313 favicon : 'https://qoomon.github.io/aws-s3-bucket-browser/favicon.ico' ,
1414 primaryColor : '#167df0' ,
15+ allowDownloadAll : true ,
1516
1617 bucketUrl : undefined ,
1718 // If bucketUrl is undefined, this script tries to determine bucket Rest API URL from this file location itself.
6970 integrity ="sha384-rEobhniwlwAquQiUTy2McGJWpm0H1EpkURdLKoThh6Lv2fkuM2NPIX64ptgfC8Me "
7071 crossorigin ="anonymous "> </ script >
7172
73+ < script src ="
https://cdn.jsdelivr.net/npm/[email protected] /umd/index.min.js "
> </ script > 74+
75+
7276 <!-- Explorer App Style -->
7377 < style >
7478 body {
8892 padding : 2.25rem 2.5rem ;
8993 }
9094
91- # app # breadcrumps {
95+ # app # breadcrumbs {
9296 margin-right : .5em ;
9397 }
9498
95- # app # breadcrumps .button .icon : first-child : not (: last-child ) {
99+ # app # breadcrumbs .button .icon : first-child : not (: last-child ) {
96100 margin-left : calc (-.5em - 1px );
97101 margin-right : calc (-.5em - 1px );
98102 }
111115 border-color : var (--primary-color ) !important ;
112116 color : var (--primary-color ) !important ;
113117 }
118+ # app .button .is-primary .is-outlined .is-loading ::after {
119+ border-color : transparent transparent var (--primary-color ) var (--primary-color ) !important ;
120+ }
114121
115122 # app .table .button .is-text {
116123 padding : .1em .75em ;
166173 flex-basis : 1.5rem ;
167174 flex-grow : 0 ;
168175 flex-shrink : 0 ;
176+ align-content : center;
177+ text-align : center;
169178 }
170179
171180 # app .name-column-buttons {
@@ -245,9 +254,9 @@ <h2 class="subtitle" v-html="config.subtitleHTML"></h2>
245254 <!-- Navigation Bar -->
246255 < div class ="container is-clearfix " style ="margin-bottom: 1rem; ">
247256 < div class ="buttons is-pulled-left " style ="width: 100%; ">
248- < div id ="breadcrumps ">
249- <!-- Prefix Breadcrumps -->
250- < b-button v-for ="(breadcrump, index) in pathBreadcrumps " v-bind:key ="breadcrump.url "
257+ < div id ="breadcrumbs ">
258+ <!-- Prefix Breadcrumbs -->
259+ < b-button v-for ="(breadcrump, index) in pathBreadcrumbs " v-bind:key ="breadcrump.url "
251260 type ="is-primary " rounded
252261 tag ="a "
253262 :href ="breadcrump.url "
@@ -260,6 +269,7 @@ <h2 class="subtitle" v-html="config.subtitleHTML"></h2>
260269 < template v-if ="index > 0 "> {{ breadcrump.name }}</ template >
261270 </ b-button >
262271 </ div >
272+
263273 < div class ="container " style ="display: flex; ">
264274 <!-- Prefix Search Input -->
265275 < b-field :type ="!validBucketPrefix(searchPrefix) ? 'is-danger' : '' "
@@ -280,17 +290,29 @@ <h2 class="subtitle" v-html="config.subtitleHTML"></h2>
280290 </ b-input >
281291 </ b-field >
282292
293+ <!-- Download All Button -->
294+ < b-button
295+ v-if ="config.allowDownloadAll && pathContentTableData.filter(item => item.type === 'content').length >= 2 "
296+ type ="is-primary " outlined rounded :loading ="downloadAllFilesProgress !== null "
297+ icon-pack ="fas "
298+ icon-left ="download "
299+ @click ="downloadAllFiles "
300+ style ="margin-left: 0.7rem; "
301+ > </ b-button >
302+
283303 <!-- Paginating Buttons -->
284304 < div class ="container "
285- v-show ="nextContinuationToken || previousContinuationTokens.length > 0 "
305+ v-if ="nextContinuationToken || previousContinuationTokens.length > 0 "
286306 style ="display: contents; ">
307+ < div style ="margin-left: 0.6rem; "> </ div >
308+
287309 < b-button
288310 type ="is-primary " rounded
289311 icon-pack ="fas "
290312 icon-left ="angle-left "
291313 @click ="previousPage "
292314 :disabled ="previousContinuationTokens.length === 0 "
293- style =" margin-left: 2rem; " >
315+ >
294316 </ b-button >
295317 < b-button
296318 type ="is-primary " rounded
@@ -305,6 +327,14 @@ <h2 class="subtitle" v-html="config.subtitleHTML"></h2>
305327 </ div >
306328 </ div >
307329
330+ < b-progress v-if ="downloadAllFilesProgress !== null "
331+ type ="is-info "
332+ :value ="downloadAllFilesProgress * 110 "
333+ show-value
334+ > {{ new Intl.NumberFormat('en-US', { style: 'percent', minimumFractionDigits: 1})
335+ .format(downloadAllFilesProgress) }}
336+ </ b-progress >
337+
308338 <!-- Content Table -->
309339 < b-table
310340 :data ="pathContentTableData "
@@ -323,8 +353,8 @@ <h2 class="subtitle" v-html="config.subtitleHTML"></h2>
323353 < b-icon
324354 pack ="far "
325355 :icon ="props.row.type === 'prefix' ? 'folder' : 'file-alt' "
356+ size ="is-medium "
326357 class ="name-column-icon "
327- style ="text-align: left; "
328358 >
329359 </ b-icon >
330360
@@ -338,26 +368,25 @@ <h2 class="subtitle" v-html="config.subtitleHTML"></h2>
338368 </ b-button >
339369
340370 < div class ="name-column-buttons ">
341- <!-- Markdown Preview Button -->
342- < b-button
343- v-if ="props.row.type === 'content' && props.row.name.endsWith('.md') "
344- type ="is-text "
345- tag ="a "
346- :href ="`#${pathPrefix.replace(/[^/]*$/, '') + props.row.name}?preview=markdown` "
347- icon-left ="image " size ="is-medium "
348- target ="_blank "
349- @click ="blurActiveElement "
350- > </ b-button >
351-
352- <!-- Install Button -->
353- < b-button
354- v-if ="props.row.installUrl "
355- type ="is-primary " rounded outlined
356- tag ="a "
357- :href ="props.row.installUrl "
358- @click ="blurActiveElement "
359- > Install
360- </ b-button >
371+ < template v-if ="props.row.type === 'content' ">
372+ <!-- Markdown Preview Button -->
373+ < b-button v-if ="props.row.name.endsWith('.md') "
374+ tag ="a " type ="is-text "
375+ icon-left ="image " size ="is-medium "
376+ :href ="`#${pathPrefix.replace(/[^/]*$/, '') + props.row.name}?preview=markdown` "
377+ target ="_blank "
378+
379+ > </ b-button >
380+
381+ <!-- Install Button -->
382+ < b-button v-if ="props.row.installUrl "
383+ tag ="a " type ="is-primary " rounded outlined
384+ :href ="props.row.installUrl "
385+ target ="_blank "
386+ @click ="blurActiveElement "
387+ > Install
388+ </ b-button >
389+ </ template >
361390 </ div >
362391 </ div >
363392
@@ -703,7 +732,9 @@ <h2 class="subtitle" v-html="config.subtitleHTML"></h2>
703732 continuationToken : undefined ,
704733 nextContinuationToken : undefined ,
705734
706- windowWidth : window . innerWidth
735+ windowWidth : window . innerWidth ,
736+
737+ downloadAllFilesProgress : null ,
707738 }
708739 } ,
709740 computed : {
@@ -712,7 +743,7 @@ <h2 class="subtitle" v-html="config.subtitleHTML"></h2>
712743 '--primary-color' : this . config . primaryColor
713744 }
714745 } ,
715- pathBreadcrumps ( ) {
746+ pathBreadcrumbs ( ) {
716747 return [ '' , ...( this . pathPrefix . match ( / [ ^ / ] * \/ / g) || [ ] ) ]
717748 . map ( ( pathPrefixPart , index , pathPrefixParts ) => ( {
718749 name : decodeURI ( pathPrefixPart ) ,
@@ -767,6 +798,58 @@ <h2 class="subtitle" v-html="config.subtitleHTML"></h2>
767798 this . refresh ( )
768799 }
769800 } ,
801+ async downloadAllFiles ( ) {
802+ const archiveFiles = this . pathContentTableData
803+ . filter ( item => item . type === 'content' )
804+ . map ( item => item . url ) ;
805+
806+ this . downloadAllFilesProgress = 0 ;
807+
808+ console . debug ( 'create archive...' )
809+ const archiveData = [ ]
810+ const archive = new fflate . Zip ( ( err , data , final ) => {
811+ if ( err ) throw err ;
812+ archiveData . push ( data ) ;
813+ } ) ;
814+
815+ let totalContentLength = 0 ;
816+ let totalReceivedLength = 0 ;
817+
818+ await Promise . all ( archiveFiles . map ( async ( url ) => {
819+ console . debug ( `downloading ${ url } ...` ) ;
820+
821+ const fileName = url . split ( '/' ) . pop ( ) ; // Extract file name from URL
822+ const fileStream = new fflate . ZipPassThrough ( fileName ) ;
823+ archive . add ( fileStream ) ;
824+
825+ const fileResponse = await fetch ( url ) ;
826+ totalContentLength += parseInt ( fileResponse . headers . get ( 'Content-Length' ) ) ;
827+ const fileReader = fileResponse . body . getReader ( ) ;
828+
829+ while ( true ) {
830+ const { done, value} = await fileReader . read ( ) ;
831+ if ( done ) {
832+ fileStream . push ( new Uint8Array ( ) , true ) ;
833+ break ;
834+ }
835+ fileStream . push ( new Uint8Array ( value ) ) ;
836+
837+ totalReceivedLength += value . length ;
838+ this . downloadAllFilesProgress = totalReceivedLength / totalContentLength ;
839+ }
840+ } ) ) . then ( ( ) => archive . end ( ) ) ;
841+ console . debug ( 'done!' )
842+
843+ const archiveBlob = new Blob ( archiveData , { type : 'application/octet-stream' } ) ;
844+ const objectUrl = window . URL . createObjectURL ( archiveBlob ) ;
845+ const link = document . createElement ( 'a' ) ;
846+ link . href = objectUrl ;
847+ link . download = `archive.zip` ;
848+ link . click ( ) ;
849+ window . URL . revokeObjectURL ( objectUrl ) ;
850+
851+ this . downloadAllFilesProgress = null ;
852+ } ,
770853 async refresh ( ) {
771854 let listBucketResult
772855 try {
0 commit comments