Comments (9)
That's strange indeed. Could you provide me your index.html?
from aws-s3-bucket-browser.
Here is my index.html - note that I only changed the bucketUrl. In addition, I've found a separate way to get the listing I want, so this isn't very high-priority, but I figured it would still be nice to have answered in case someone else runs into a similar situation. Thank you!
<!-- S3 Bucket Explorer Version: 1.8.1 -->
<!DOCTYPE html>
<html lang="en" style="overflow-y: auto;">
<head>
<script>
const config = {
title: 'ZAP Results',
subtitle: 'results',
logo: 'https://avatars.githubusercontent.com/u/6716868?s=200&v=4',
favicon: 'https://avatars.githubusercontent.com/u/6716868?s=200&v=4',
primaryColor: '#236bb5',
bucketUrl: 'https://zap-results.com',
// If bucketUrl is undefined, this script tries to determine bucket Rest API URL from this file location itself.
// This will only work for locations like these
// * https://s3.BUCKET-REGION.amazonaws.com/BUCKET-NAME/index.html
// * http://BUCKET-NAME.s3-website-BUCKET-REGION.amazonaws.com/index.html
// * https://storage.googleapis.com/BUCKET-NAME/index.html
// * https://BUCKET-NAME.s3-web.BUCKET-REGION.cloud-object-storage.appdomain.cloud/
// If bucketUrl is set manually, ensure this is the bucket Rest API URL, e.g.
// * https://s3.BUCKET-REGION.amazonaws.com/BUCKET-NAME
// * https://storage.googleapis.com/BUCKET-NAME
// The URL should return an XML document with <ListBucketResult> as root element.
rootPrefix: undefined, // e.g. 'subfolder/'
keyExcludePatterns: [/^index\.html$/],
pageSize: 50,
bucketMaskUrl: undefined,
// If bucketMaskUrl is set file urls will be changed from ${bucketUrl}/${file} to ${bucketMaskUrl}/${file}
// bucketMaskUrl: 'https://example.org'
// => https://example.org/foo/bar.txt
// bucketMaskUrl: document.location.origin
// => ${document.location.origin}/foo/bar.txt
defaultOrder: 'name-asc' // (name|size|dateModified)-(asc|desc)
}
</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link id="favicon" rel="shortcut icon" />
<script src="https://unpkg.com/[email protected]/dist/vue.min.js" integrity="sha384-ULpZhk1pvhc/UK5ktA9kwb2guy9ovNSTyxPNHANnA35YjBQgdwI+AhLkixDvdlw4" crossorigin="anonymous"></script>
<script>Vue.config.productionTip = false;</script>
<style>[v-cloak] {display: none}</style>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.materialdesignicons.com/5.3.45/css/materialdesignicons.min.css" integrity="sha384-rJzKgf2UUnpzXvYOutlTiL8YYlEwrfhBmJI+ymqTQ67Dw3qJJeHA76pmK2IaTjuD" crossorigin="anonymous">
<script src="https://unpkg.com/[email protected]/dist/buefy.min.js" integrity="sha384-T58J1OGlL1z5XNzGTewXlzY/GUx9W3Nw7jUux1EJSA11VzG7YVKuzRTKo/lo/xRc" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/buefy.min.css" integrity="sha384-QtNuB2V0zXqYlM3LrzVmFc4U2pjfGzRfZbANB/BodC/oDS6aMw7CoDl4Me3X6UQS" crossorigin="anonymous">
<script src="https://unpkg.com/[email protected]/min/moment.min.js" integrity="sha384-Uz1UHyakAAz121kPY0Nx6ZGzYeUTy9zAtcpdwVmFCEwiTGPA2K6zSGgkKJEQfMhK" crossorigin="anonymous"></script>
</head>
<body >
<div id="app" v-cloak :style="cssVars">
<!-- Header -->
<div class="level">
<div class="level-left" style="display: flex;">
<figure class="level-item image is-96x96" style="margin-right: 1.5rem;">
<img :src="config.logo" />
</figure>
<div>
<h1 class="title">{{config.title}}</h1>
<h2 class="subtitle">{{config.subtitle}}</h2>
</div>
</div>
</div>
<div class="container">
<!-- Navigation Bar -->
<div class="container is-clearfix">
<!-- Prefix Breadcrumps -->
<div class="buttons is-pulled-left">
<b-button v-for="(breadcrump, index) in pathBreadcrumps" v-bind:key="breadcrump.url"
type="is-primary" rounded
tag="a"
:href="breadcrump.url"
icon-pack="fas"
:icon-left="index == 0 ? 'folder' : ''"
target=""
:style="{ fontWeight: index == 0 ? 'bolder': ''}"
style="white-space: pre;"
>
<template>{{index > 0 ? breadcrump.name : '/'}}</template>
</b-button>
</div>
<!-- Paginating Buttons -->
<div v-show="nextContinuationToken || previousContinuationTokens.length > 0"
class="buttons is-pulled-right">
<b-button
type="is-primary" rounded
icon-pack="fas"
icon-left="angle-left"
@click="previousPage"
:disabled="previousContinuationTokens.length === 0"
>
</b-button>
<b-button
type="is-primary" rounded
icon-pack="fas"
icon-right="angle-right"
@click="nextPage"
:disabled="!nextContinuationToken"
>
</b-button>
</div>
</div>
<!-- Content Table -->
<b-table
:data="pathContentTableData"
:default-sort="config.defaultOrder.split('-')[0]"
:default-sort-direction="config.defaultOrder.split('-')[1]"
:mobile-cards="true"
>
<b-table-column
v-slot="props"
field="name"
label="Name"
sortable :custom-sort="sortTableData('name')"
cell-class="name-column"
>
<div style="display: flex; align-items: center;">
<b-icon
pack="far"
:icon="props.row.type === 'prefix' ? 'folder' : 'file-alt'"
class="name-column-icon"
>
</b-icon>
<div style="text-align: left;">
<b-button
type="is-text" rounded
tag="a"
:href="props.row.type === 'content' ? props.row.url : `#${props.row.prefix}`"
@click="document.activeElement.blur()"
style="text-align: left; white-space: pre-wrap;"
>
<template>{{ props.row.name }}</template>
</b-button>
<br>
<b-button
v-if="props.row.installUrl"
type="is-primary" rounded outlined
tag="a"
:href="props.row.installUrl"
class="name-column-install-button"
>
Install
</b-button>
</div>
</div>
<div
v-if="cardView && (props.row.size || props.row.dateModified)"
class="name-column-details"
>
<div>{{ formatBytes(props.row.size) }}</div>
<b-tooltip
:active="!!props.row.dateModified"
type="is-light"
size="is-small"
position="is-left"
animated
multilined
>
<div>{{ formatDateTime_relative(props.row.dateModified) }}</div>
<template v-slot:content>
<span>{{ formatDateTime_date(props.row.dateModified) }}</span>
<br>
<span>{{ formatDateTime_time(props.row.dateModified) }}</span>
</template>
</b-tooltip>
</div>
</b-table-column>
<b-table-column
v-slot="props"
field="size" numeric
label="Size"
sortable :custom-sort="sortTableData('size')"
centered width="128"
cell-class="size-column"
>
{{ formatBytes(props.row.size) }}
</b-table-column>
<b-table-column
v-slot="props"
field="dateModified"
label="Date Modified"
sortable :custom-sort="sortTableData('dateModified')"
centered width="256"
cell-class="modified-column"
>
<b-tooltip
:active="!!props.row.dateModified"
type="is-light"
size="is-small"
position="is-left"
animated
multilined
>
<div>{{ formatDateTime_relative(props.row.dateModified) }}</div>
<template v-slot:content>
<span>{{ formatDateTime_date(props.row.dateModified) }}</span>
<br>
<span>{{ formatDateTime_time(props.row.dateModified) }}</span>
</template>
</b-tooltip>
</b-table-column>
</b-table>
<!-- Paginating Buttons -->
<div class="container is-clearfix" style="margin-top: 1rem;">
<div v-show="nextContinuationToken || previousContinuationTokens.length > 0"
class="buttons is-pulled-right">
<b-button
type="is-primary" rounded
icon-pack="fas"
icon-left="angle-left"
@click="previousPage"
:disabled="previousContinuationTokens.length === 0"
>
</b-button>
<b-button
type="is-primary" rounded
icon-pack="fas"
icon-right="angle-right"
@click="nextPage"
:disabled="!nextContinuationToken"
>
</b-button>
</div>
</div>
</div>
<!-- Footer -->
<div class="footer-bucket-url">
<a :href="`${config.bucketUrl}?prefix=${config.rootPrefix}${pathPrefix}`">Bucket: {{ config.bucketUrl }}</a>
</div>
</div>
<script>
function escapeRegExp(text) {
// $& means the whole matched string
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
function escapeHTML(text) {
let div = document.createElement('div')
div.innerText = text
return div.innerHTML
}
function devicePlatform_iOS() {
return /iPad|iPhone|iPod/.test(navigator.platform) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
}
</script>
<!-- Main Script -->
<script>
if (!config.bucketUrl) {
// try get bucket url by request parameter
config.bucketUrl = new URL(window.location).searchParams.get('bucket')
}
if (!config.bucketUrl) {
config.bucketUrl = window.location.href
}
// try adjusting bucket url to bucket rest api endpoint
let match
let type
if (!match) {
type = 'AWS'
// check for urls like https://s3.eu-central-1.amazonaws.com/example-bucket/index.html
match = config.bucketUrl.match(/(?<protocol>[^:]+):\/\/s3\.(?<region>[^.]+)\.amazonaws.com\/(?<name>[^/]+)/)
}
if (!match) {
type = 'AWS'
// check for urls like http://example-bucket.s3-website-eu-west-1.amazonaws.com/index.html
match = config.bucketUrl.match(/(?<protocol>[^:]+):\/\/(?<name>[^.]+)\.s3-website-(?<region>[^.]+)\.amazonaws\.com/)
}
if (!match) {
type = 'AWS'
// check for urls like http://example-bucket.s3-website.eu-central-1.amazonaws.com/index.html
match = config.bucketUrl.match(/(?<protocol>[^:]+):\/\/(?<name>[^.]+)\.s3-website\.(?<region>[^.]+)\.amazonaws\.com/)
}
if (!match) {
type = 'GCP'
// check for urls like https://storage.googleapis.com/example-bucket/index.html
match = config.bucketUrl.match(/(?<protocol>[^:]+):\/\/storage\.googleapis\.com\/(?<name>[^.]+)/)
}
if (!match) {
type = 'IBM'
// check for urls like http://example-bucket.s3-web.us-south.cloud-object-storage.appdomain.cloud/index.html
match = config.bucketUrl.match(/(?<protocol>[^:]+):\/\/(?<name>[^.]+)\.s3-web\.(?<region>[^.]+)\.cloud-object-storage\.appdomain\.cloud/)
}
if (match) {
switch (type) {
case 'AWS':
config.bucketUrl = `${match.groups.protocol}://s3.${match.groups.region}.amazonaws.com/${match.groups.name}`
break;
case 'GCP':
config.bucketUrl = `${match.groups.protocol}://storage.googleapis.com/${match.groups.name}`
break;
case 'IBM':
config.bucketUrl = `${match.groups.protocol}://${match.groups.name}.s3.${match.groups.region}.cloud-object-storage.appdomain.cloud/`
break;
}
}
console.log("Bucket REST API: " + config.bucketUrl)
config.rootPrefix = config.rootPrefix || ''
if (config.rootPrefix) {
if (!config.rootPrefix.endsWith('/')) {
config.rootPrefix += '/'
}
console.log("Bucket Root Prefix: " + config.rootPrefix)
}
document.title = config.title
document.getElementById('favicon').href = config.favicon
Vue.use(Buefy.default, {
defaultIconPack: 'fa'
})
new Vue({
el: '#app',
data: {
config, // defined in <head> section
pathPrefix: '',
pathContentTableData: [],
previousContinuationTokens: [],
continuationToken: undefined,
nextContinuationToken: undefined,
windowWidth: window.innerWidth
},
computed: {
cssVars() {
return {
'--primary-color': this.config.primaryColor
}
},
pathBreadcrumps() {
return `/${this.pathPrefix}`.match(/(?=[/])|[^/]+[/]?/g)
.map((pathPrefixPart, index, pathPrefixParts) => ({
name: decodeURI(pathPrefixPart),
url: '#' + pathPrefixParts.slice(0, index).join('') + pathPrefixPart
}))
},
cardView() {
return this.windowWidth <= 768
}
},
watch: {
pathPrefix() {
this.previousContinuationTokens = []
this.continuationToken = undefined
this.nextContinuationToken = undefined
this.refresh()
}
},
methods: {
moment,
previousPage() {
if (this.previousContinuationTokens.length > 0) {
this.continuationToken = this.previousContinuationTokens.pop()
this.refresh()
}
},
nextPage() {
if (this.nextContinuationToken) {
this.previousContinuationTokens.push(this.continuationToken)
this.continuationToken = this.nextContinuationToken
this.refresh()
}
},
async refresh() {
let listBucketResult
try {
if (!config.bucketUrl) {
throw Error("Bucket url is undefined!")
}
let bucketListApiUrl = `${config.bucketUrl}?list-type=2`
bucketListApiUrl += `&delimiter=/`
let bucketPrefix = `${config.rootPrefix}${this.pathPrefix}`
bucketListApiUrl += `&prefix=${bucketPrefix}`
if (config.pageSize) {
bucketListApiUrl += `&max-keys=${config.pageSize}`
}
if (this.continuationToken) {
bucketListApiUrl += `&continuation-token=${encodeURIComponent(this.continuationToken)}`
}
let listBucketResultResponse = await fetch(bucketListApiUrl)
let listBucketResultXml = await listBucketResultResponse.text()
listBucketResult = new DOMParser().parseFromString(listBucketResultXml, "text/xml")
if (!listBucketResult.querySelector('ListBucketResult')) {
throw Error("List bucket response does not contain <ListBucketResult> tag!")
}
} catch (error) {
this.$buefy.notification.open({
message: escapeHTML(error.message),
type: 'is-danger',
duration: 60000,
position: 'is-bottom'
})
throw error
}
let nextContinuationTokenTag = listBucketResult.querySelector("NextContinuationToken")
this.nextContinuationToken = nextContinuationTokenTag && nextContinuationTokenTag.textContent
let commonPrefixes = [...listBucketResult.querySelectorAll("ListBucketResult > CommonPrefixes")]
.map(tag => ({
prefix: tag.querySelector('Prefix').textContent
.replace(RegExp(`^${escapeRegExp(config.rootPrefix)}`), '')
}))
.filter(prefix => !config.keyExcludePatterns.find(pattern => pattern.test(prefix.prefix)))
.map(prefix => ({
type: 'prefix',
name: prefix.prefix.split('/').slice(-2)[0] + '/',
prefix: prefix.prefix
}))
let contents = [...listBucketResult.querySelectorAll("ListBucketResult > Contents")]
.map(tag => ({
key: tag.querySelector('Key').textContent,
size: parseInt(tag.querySelector('Size').textContent),
dateModified: new Date(tag.querySelector('LastModified').textContent)
}))
.filter(content => content.key !== decodeURI(this.pathPrefix))
.filter(content => !config.keyExcludePatterns.find(pattern => pattern.test(content.key)))
.map(content => {
if (content.key.endsWith('/') && !content.size) {
return {
type: 'prefix',
name: content.key.split('/')[0] + '/',
prefix: `${this.pathPrefix}${content.key}`
}
}
let url = `${(config.bucketMaskUrl || config.bucketUrl).replace(/\/*$/,'')}/${content.key}`
let installUrl = undefined
if (url.endsWith('/manifest.plist') && devicePlatform_iOS()) {
// generate manifest.plist install urls
installUrl = `itms-services://?action=download-manifest&url=${url.replace(/\/[^/]*$/,'')}/manifest.plist`
}
return {
type: 'content',
name: content.key.split('/').slice(-1)[0],
size: content.size,
dateModified: content.dateModified,
key: content.key,
url,
installUrl
}
})
this.pathContentTableData = [...commonPrefixes, ...contents]
},
sortTableData(columnName) {
return (rowA, rowB, isAsc) => {
// prefixes always first
if (rowA.type != rowB.type) {
return rowA.type === 'prefix' ? -1 : 1
}
const valueA = rowA[columnName]
const valueB = rowB[columnName]
if (valueA != valueB) {
if (valueA === undefined) {
return isAsc ? -1 : 1
}
if (valueB === undefined) {
return isAsc ? 1 : -1
}
return isAsc ?
(valueA < valueB ? -1 : 1) :
(valueA < valueB ? 1 : -1)
}
return 0
}
},
formatBytes(size) {
if (!size) {
return '-'
}
const KB = 1024
if (size < KB) {
return size + ' B'
}
const MB = 1000000
if (size < MB) {
return (size / KB).toFixed(0) + ' KB'
}
const GB = 1000000000
if (size < GB) {
return (size / MB).toFixed(2) + ' MB'
}
return (size / GB).toFixed(2) + ' GB'
},
formatDateTime_date(date) {
return date ? moment(date).format('ddd, DD. MMM YYYY') : '-'
},
formatDateTime_time(date) {
return date ? moment(date).format('hh:mm:ss') : '-'
},
formatDateTime_relative(date) {
return date ? moment(date).fromNow() : '-'
}
},
async mounted() {
window.onpopstate = () => this.pathPrefix = window.location.hash.replace(/^#\/?/, '')
window.onpopstate()
window.addEventListener('resize', () => {
this.windowWidth = window.innerWidth
})
this.refresh()
},
async beforeDestroy() {
window.removeEventListener('resize')
}
})
</script>
<style scoped>
body {
width: 100vw;
min-height: 100vh;
position: relative;
padding: 1.25rem 2.5rem 2.5rem 1.5rem;
background-color: #f5f5f5;
overflow-y: auto;
}
.button.is-primary {
background-color: var(--primary-color) !important;
}
.button.is-primary.is-outlined {
background-color: transparent !important;
border-color: var(--primary-color) !important;
color: var(--primary-color) !important;
}
.button.is-text {
padding: 0 !important;
height: auto !important;
text-decoration: none !important;
box-shadow: none !important;
background-color: unset !important;
user-select: text !important;
color: var(--primary-color) !important;
font-weight: 500;
}
.button.is-text:focus {
font-weight: bold;
}
.name-column-icon {
display: block;
text-align: left;
flex-basis: 1.5rem;
flex-grow: 0;
flex-shrink: 0;"
}
.name-column-install-button {
height: 0;
padding: 0.6rem;
margin-top: 0.4rem;
font-size: 0.8rem;
}
.name-column-details {
display: flex;
align-items: flex-end;
justify-content: center;
flex-shrink: 0;
min-width: 5rem;
font-size: 0.85rem;
line-height: 1.5rem;
flex-direction: column;
}
.footer-bucket-url {
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin-bottom: 0.5rem;
font-size: small;
text-align: center;
color: darkgray;
}
.footer-bucket-url a {
color: inherit;
}
@media screen and (max-width: 768px) {
.name-column::before {
display: none !important;
}
.size-column,
.modified-column {
display: none !important;
}
}
</style>
</body>
</html>
from aws-s3-bucket-browser.
what the different way looks like?
from aws-s3-bucket-browser.
I just developed some HTML/JavaScript to send requests to pull the XML, then I did the parsing manually, no fancy libraries or anything like that. Didn't really have anything in common with the project here, sorry!
from aws-s3-bucket-browser.
I found the problem although I don't know why this is happening though.
if you request your bucket api (NOT bucket url) like <URL BUCKET API URL>?list-type=2&delimiter=/&prefix=&max-keys=50
e.g. https://s3.eu-west-1.amazonaws.com/data.openspending.org?list-type=2&delimiter=/&prefix=worldbank/cameroon/&max-keys=50
You should get a response like the following, with <Delimiter>
and <CommonPrefixes>
elements, but you don't thats why the listing is not working as expected
<ListBucketResult
xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Name>data.openspending.org</Name>
<Prefix></Prefix>
<KeyCount>3</KeyCount>
<MaxKeys>50</MaxKeys>
<Delimiter>/</Delimiter>
<IsTruncated>false</IsTruncated>
<Contents>
<Key>index.html</Key>
<LastModified>2021-12-17T19:48:25.000Z</LastModified>
<ETag>"02c58fe9379dca33b143fc0f5af05ece"</ETag>
<Size>402</Size>
<StorageClass>DEEP_ARCHIVE</StorageClass>
</Contents>
<CommonPrefixes>
<Prefix>datasets/</Prefix>
</CommonPrefixes>
<CommonPrefixes>
<Prefix>worldbank/</Prefix>
</CommonPrefixes>
</ListBucketResult>
from aws-s3-bucket-browser.
Makes sense, thank you!
from aws-s3-bucket-browser.
I think you don't use a S3 bucket api url.
// If bucketUrl is set manually, ensure this is the bucket Rest API URL, e.g.
// * https://s3.BUCKET-REGION.amazonaws.com/BUCKET-NAME
// * https://storage.googleapis.com/BUCKET-NAME
from aws-s3-bucket-browser.
<YOUR BUCKET URL>?list-type=2&delimiter=/
should return a xml
with with an < ListBucketResult ><Delimiter>
element
from aws-s3-bucket-browser.
I'll add this api validation check to the bucket explorer script.
from aws-s3-bucket-browser.
Related Issues (20)
- first time visit does not browse into given path prefix provided in url HOT 3
- Files containing a "+" in their name are not linked properly HOT 3
- Bucket URL https://bucket/index.html is not a valid bucket API URL, response does not contain <ListBucketResult><Delimiter> tag. HOT 1
- rootPrefix folder is visible on every page. It should be hidden. HOT 7
- Add search bar at the top which can help users to search files quickly HOT 6
- Content Security Policy: The page’s settings blocked the loading of a resource at inline (“script-src”). HOT 2
- Bucket has to be public in order to work with s3-bucket-brower ? HOT 7
- %2F delimeters in href HOT 9
- issue with ssl HOT 2
- Feature Request: Compatibility with Cloudflare R2 HOT 4
- Question: ListObjects / ListObjectsV2 HOT 3
- Issues with listing subdirectories HOT 9
- A way to change {bucket].s3-website.us-east-1. to just the site/domain we are hosting? HOT 11
- index.html isn't being excluded HOT 8
- Previously working sites have stopped working with CORS errors HOT 3
- Very good job, I also use it on aliyun oss bucket
- Content downloaded instead of serving HOT 2
- Improved directory listing experience with CloudFront Functions HOT 8
- Feature Request: Search Behavior HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from aws-s3-bucket-browser.