Skip to content

Commit 9a4b3c4

Browse files
committed
feat: download all files from current page
1 parent c5990a8 commit 9a4b3c4

File tree

1 file changed

+114
-31
lines changed

1 file changed

+114
-31
lines changed

index.html

Lines changed: 114 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
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;">
@@ -12,6 +12,7 @@
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.
@@ -69,6 +70,9 @@
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 {
@@ -88,11 +92,11 @@
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
}
@@ -111,6 +115,9 @@
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;
@@ -166,6 +173,8 @@
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

Comments
 (0)