Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Directory structure not being recognized from XML keys #31

Closed
chwilliams9487 opened this issue Apr 8, 2022 · 9 comments
Closed

Directory structure not being recognized from XML keys #31

chwilliams9487 opened this issue Apr 8, 2022 · 9 comments

Comments

@chwilliams9487
Copy link

chwilliams9487 commented Apr 8, 2022

Hello, first of all thank you for creating this incredible tool! I'm running into a bit of an issue when my S3 bucket objects are being listed and hopefully you'll be able to help me.

I'm running a private S3 origin bucket for Cloudfront, and I can go to my designated URL and get the XML layout of all of the objects within, which should include directories such as 'HCU' and 'OWASPJuiceShop'. Note that some aspects were blurred for privacy reasons:
image

However, when I check the index.html page to see the reflected contents, this directory structure appears to be missing:

image

All I've modified in the example index.html you provide is my bucket URL (https://zap-results.com), and my search through the rest of the style changes doesn't seem to indicate that anything would remove these directory headings - any help would be greatly appreciated!

@qoomon
Copy link
Owner

qoomon commented Apr 9, 2022

That's strange indeed. Could you provide me your index.html?

@chwilliams9487
Copy link
Author

chwilliams9487 commented Apr 11, 2022

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/vue@2.6.14/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/buefy@0.9.10/dist/buefy.min.js" integrity="sha384-T58J1OGlL1z5XNzGTewXlzY/GUx9W3Nw7jUux1EJSA11VzG7YVKuzRTKo/lo/xRc" crossorigin="anonymous"></script>
  <link rel="stylesheet" href="https://unpkg.com/buefy@0.9.10/dist/buefy.min.css" integrity="sha384-QtNuB2V0zXqYlM3LrzVmFc4U2pjfGzRfZbANB/BodC/oDS6aMw7CoDl4Me3X6UQS" crossorigin="anonymous">
  <script src="https://unpkg.com/moment@2.29.1/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>

@qoomon
Copy link
Owner

qoomon commented Apr 11, 2022

what the different way looks like?

@chwilliams9487
Copy link
Author

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!

@qoomon
Copy link
Owner

qoomon commented Apr 11, 2022

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>&quot;02c58fe9379dca33b143fc0f5af05ece&quot;</ETag>
        <Size>402</Size>
        <StorageClass>DEEP_ARCHIVE</StorageClass>
    </Contents>
    <CommonPrefixes>
        <Prefix>datasets/</Prefix>
    </CommonPrefixes>
    <CommonPrefixes>
        <Prefix>worldbank/</Prefix>
    </CommonPrefixes>
</ListBucketResult>

@chwilliams9487
Copy link
Author

Makes sense, thank you!

@qoomon
Copy link
Owner

qoomon commented Apr 11, 2022

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

@qoomon
Copy link
Owner

qoomon commented Apr 11, 2022

<YOUR BUCKET URL>?list-type=2&delimiter=/should return a xml with with an < ListBucketResult ><Delimiter> element

@qoomon
Copy link
Owner

qoomon commented Apr 11, 2022

I'll add this api validation check to the bucket explorer script.

@qoomon qoomon closed this as completed Apr 11, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants