lists3 0.1.1

Light-weight CLI tool to create a file browser for S3 buckets provisioning bucket listing.
Documentation
/// Loads the HTML content for listing S3 contents with vanilla JS template.
///
/// # Returns
///
/// Returns the HTML content as a `String`
pub fn get_content() -> String {
    r###"<!DOCTYPE html>
<!--suppress JSUnresolvedLibraryURL, JSUnresolvedReference -->
<html lang="en">
<head>
    <title>Rustic Bucket Listing</title>
    <meta property="og:type" content="FileBrowser">
    <meta content="This is a filebrowser for S3 buckets" name="description">
    <meta name="keywords" content="AWS, S3, Rust, FileBrowser, HTML, CSS, JavaScript, Jinja2">
    <meta name="author" content="Vignesh Rao">
    <meta content="width=device-width, initial-scale=1" name="viewport">
    <link name="favicon" property="og:image" rel="icon" href="https://thevickypedia.github.io/open-source/images/logo/rust.ico">
    <link name="apple-touch-icon" property="og:image" rel="apple-touch-icon" href="https://thevickypedia.github.io/open-source/images/logo/rust.png">
    <meta http-equiv="content-type" content="no-cache, no-store, must-revalidate">
    <meta http-equiv="refresh" content="no-cache">
    <style>
        .corner {
            position: absolute;
            top: 3%;
            right: 1px;
            bottom: 97%;
            display: inline-flex;
        }

        .corner p {
            font-size: 14px;
            font-family: 'Courier New', monospace !important;
            margin-right: 15px; /* Spacing between the joining <p> tags */
        }

        small {
            margin-left: 10px;
        }
    </style>
    <script>
        function getSpace(s, l) {
            let ret = "";
            while (s.length + ret.length < l) {
                ret = ret + " ";
            }
            return ret;
        }

        location.querystring = (function () {
            // The return is a collection of key/value pairs
            let result = {};

            // Gets the query string with a preceeding '?'
            let querystring = location.search;

            // document.location.search is empty if a query string is absent
            if (!querystring)
                return result;

            // substring(1) to remove the '?'
            let pairs = querystring.substring(1).split("&");
            let splitPair;

            // Load the key/values of the return collection
            for (let i = 0; i < pairs.length; i++) {
                splitPair = pairs[i].split("=");
                result[splitPair[0]] = splitPair[1];
            }

            return result;
        })();

        function createRequestObject() {
            let request_o; // Declare the variable to hold the object.
            // noinspection JSDeprecatedSymbols
            let browser = navigator.appName; // Find the browser name
            if (browser === "Microsoft Internet Explorer") {
                /* Create the object using MSIE's method */
                request_o = new ActiveXObject("Microsoft.XMLHTTP");
            } else {
                /* Create the object using other browser's method */
                request_o = new XMLHttpRequest();
            }
            return request_o; // return the object
        }

        /* You can get more specific with version information by using
          parseInt(navigator.appVersion)
          Which will extract an integer value containing the version
          of the browser being used. */
        /* The variable http will hold our new XMLHttpRequest object. */
        let http = createRequestObject();

        function getList(bucketName, regionName, folderNames, ignoreObjects) {
            let pretext = document.getElementById('pretext');
            pretext.innerHTML = "<a href='https://crates.io/crates/lists3'>Rustic Bucket Listing - v{{ cargo_version }}</a>";

            let origin = `https://s3.${regionName}.amazonaws.com/${bucketName}`
            let responseType = "application/xml"
            http.open('get', origin, false)
            http.onreadystatechange = function () {
                handleList(folderNames, ignoreObjects)
            };
            http.send(null);
        }

        function filterByPrefix(files, folderNames, ignoreObjects) {
            let filteredFiles = [];
            let totalSize = 0;
            let filteredSize = 0;
            for (let i = 0; i < files.length; i++) {
                let fileName = files[i].getElementsByTagName('Key')[0].firstChild.data;
                let fileSize = files[i].getElementsByTagName('Size')[0].firstChild.data;
                totalSize += parseInt(fileSize);
                let ignoreFlag = false;
                for (let j = 0; j < ignoreObjects.length; j++) {
                    if (fileName === ignoreObjects[j]) {
                        ignoreFlag = true;
                        break;
                    }
                }
                if (!ignoreFlag) {
                    if (folderNames.length > 0) {
                        for (let k = 0; k < folderNames.length; k++) {
                            if (fileName != folderNames[k] && fileName.startsWith(folderNames[k])) {
                                filteredFiles.push(files[i]);
                                filteredSize += parseInt(fileSize);
                            }
                        }
                    } else {
                        filteredFiles.push(files[i]);
                    }
                }
            }
            let totalSizeField = document.getElementById('bucketSize');
            let totalCountField = document.getElementById('bucketCount');
            let filterCountField = document.getElementById('filterCount');
            let filteredSizeField = document.getElementById('filteredSize');

            let totalSizeConverted = sizeConverter(totalSize);

            totalSizeField.innerHTML = `Bucket Size: ${totalSizeConverted}`;
            totalCountField.innerHTML = `Total Objects: ${files.length}`;

            if (filteredFiles.length !== files.length) {
                filterCountField.innerHTML = `Filtered Objects: ${filteredFiles.length}`;
            }
            let filteredSizeConverted = sizeConverter(filteredSize);
            if (filteredSize !== 0 && filteredSize !== totalSize && filteredSizeConverted !== totalSizeConverted) {
                filteredSizeField.innerHTML = `Filtered Size: ${filteredSizeConverted}`;
            }
            if (filteredFiles.length === 0) {
                alert(`No objects were retrieved for the filters\n\nIN: ${JSON.stringify(folderNames)}\nNOT IN: ${JSON.stringify(ignoreObjects)}`);
            }
            return filteredFiles;
        }

        // Mask the full path in 'Object Name' column to improve readability
        // This functionality is available only for objects filtered with `--filter` flag
        function maskPath(objectName, folderNames) {
            let stripped = objectName;
            for (let folder of folderNames) {
                if (objectName.startsWith(folder)) {
                    stripped = objectName.slice(folder.length);
                    break;
                }
            }
            if (stripped.startsWith('/')) {
                stripped = stripped.slice(1);
            }
            return stripped;
        }

        function handleList(folderNames, ignoreObjects) {
            if (http.readyState === 4) { // Finished loading the response
                let response = http.responseXML;
                let unfiltered = response.getElementsByTagName('Contents');
                let filteredFiles = filterByPrefix(unfiltered, folderNames, ignoreObjects);
                let fileList = [];
                for (let i = 0; i < filteredFiles.length; i++) {
                    let fileData = [];
                    fileList[i] = fileData;
                    let size = filteredFiles[i].getElementsByTagName('Size')[0].firstChild.data;
                    let name = filteredFiles[i].getElementsByTagName('Key')[0].firstChild.data;
                    let storage = filteredFiles[i].getElementsByTagName('StorageClass')[0].firstChild.data;
                    let lastmod = filteredFiles[i].getElementsByTagName('LastModified')[0].firstChild.data;
                    fileData[0] = name;
                    fileData[1] = size;
                    fileData[2] = lastmod;
                    fileData[3] = storage;
                }
                fileList.sort(getSort());

                let tableBody = document.getElementById('bucket_body');
                tableBody.innerHTML = ''; // Clear existing table content

                for (let i = 0; i < fileList.length; i++) {
                    let fileData = fileList[i];
                    let name = fileData[0];
                    let size = fileData[1];
                    let lastmod = fileData[2];
                    let storage = fileData[3];
                    let row = document.createElement('tr');
                    let sizeCell = document.createElement('td');
                    sizeCell.innerHTML = sizeConverter(size);
                    let lastmodCell = document.createElement('td');
                    lastmodCell.innerHTML = lastmod;
                    let storageCell = document.createElement('td');
                    storageCell.innerHTML = storage;
                    let nameCell = document.createElement('td');
                    nameCell.innerHTML = "<a href=\"" + name + "\">" + maskPath(name, folderNames) + "</a>";
                    row.appendChild(sizeCell);
                    row.appendChild(lastmodCell);
                    row.appendChild(storageCell);
                    row.appendChild(nameCell);
                    tableBody.appendChild(row);
                }
            }
        }

        function getQueryVariable(variable) {
            let query = window.location.search.substring(1);
            let vars = query.split("&");
            for (let i = 0; i < vars.length; i++) {
                let pair = vars[i].split("=");
                if (pair[0] === variable) {
                    return pair[1];
                }
            }
            return null;
        }

        function sortSize(a, b) {
            if (parseInt(a[1]) > parseInt(b[1])) return 1;
            if (parseInt(a[1]) < parseInt(b[1])) return -1;
            return 0;
        }

        function sortSizeDesc(a, b) {
            return (-sortSize(a, b));
        }

        function sortLastmod(a, b) {
            if (a[2] > b[2]) return 1;
            if (a[2] < b[2]) return -1;
            return 0;
        }

        function sortLastmodDesc(a, b) {
            return (-sortLastmod(a, b));
        }

        function sortStorage(a, b) {
            if (a[2] > b[2]) return 1;
            if (a[2] < b[2]) return -1;
            return 0;
        }

        function sortStorageDesc(a, b) {
            return (-sortStorage(a, b));
        }

        function sortName(a, b) {
            if (a[0] > b[0]) return 1;
            if (a[0] < b[0]) return -1;
            return 0;
        }

        function sortNameDesc(a, b) {
            return -sortName(a, b);
        }

        function getSort() {
            let s = getQueryVariable("sort");
            let d = getQueryVariable("sortdir");
            if (s === 'size') {
                return d === 'desc' ? sortSizeDesc : sortSize
            }
            if (s === 'name') {
                return d === 'desc' ? sortNameDesc : sortName
            }
            if (s === 'lastmod') {
                return d === 'desc' ? sortLastmodDesc : sortLastmod
            }
            if (s === 'storage') {
                return d === 'desc' ? sortStorageDesc : sortStorage
            }
            return sortName;
        }

        function sizeConverter(byteSize) {
            let sizeName = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
            let index = Math.floor(Math.log(byteSize) / Math.log(1024));
            return (byteSize / Math.pow(1024, index)).toFixed(2) + " " + sizeName[index];
        }

        function getNextSortDir(sortCol) {
            if (sortCol === getQueryVariable("sort"))
                return getQueryVariable("sortdir") === 'desc' ? 'asc' : 'desc';
            return 'asc'
        }

        function getLinkSize() {
            window.location.href = "?sort=size&sortdir=" + getNextSortDir('size');
        }

        function getLinkName() {
            window.location.href = "?sort=name&sortdir=" + getNextSortDir('name');
        }

        function getLinkLastmod() {
            window.location.href = "?sort=lastmod&sortdir=" + getNextSortDir('lastmod');
        }

        function getLinkStorage() {
            window.location.href = "?sort=storage&sortdir=" + getNextSortDir('storage');
        }

    </script>
    <style>
        table {
            border-collapse: collapse;
            width: 100%;
            color: #333;
            font-family: Arial, sans-serif;
            font-size: 14px;
            text-align: left;
            border-radius: 10px;
            overflow: hidden;
            box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
            margin: 50px auto;
            /* margin: auto;
            margin-top: 50px;
            margin-bottom: 50px; */
        }

        table th {
            background-color: #0e42bd;
            color: #fff;
            font-weight: bold;
            padding: 10px;
            text-transform: uppercase;
            letter-spacing: 1px;
            border-top: 1px solid #fff;
            border-bottom: 1px solid #ccc;
        }

        table tr:nth-child(even) td {
            background-color: #f2f2f2;
        }

        table tr:hover td {
            background-color: #ffedcc;
        }

        table td {
            background-color: #fff;
            padding: 10px;
            border-bottom: 1px solid #ccc;
            font-weight: bold;
        }
    </style>
</head>
<body onLoad='getList(
    bucketName="{{ bucket_name }}",
    regionName="{{ region_name }}",
    folderNames={{ folder_names }},
    ignoreObjects={{ ignore_objects }}
);'>
<pre><small id="pretext"></small></pre>
<div class="corner">
    <p id="bucketSize"></p>
    <p id="bucketCount"></p>
    <p id="filteredSize"></p>
    <p id="filterCount"></p>
</div>
<br>
<table id="bucket_table">
    <thead>
    <tr>
        <th><a style="color:#FFF" href="javascript:getLinkSize()">Size</a></th>
        <th><a style="color:#FFF" href="javascript:getLinkLastmod()">Last Modified</a></th>
        <th><a style="color:#FFF" href="javascript:getLinkStorage()">Storage Class</a></th>
        <th><a style="color:#FFF" href="javascript:getLinkName()">Name</a></th>
        <!--<th><a href="?sort=name&sortdir=asc">Name</a></th>-->
    </tr>
    </thead>
    <tbody id="bucket_body">
    <!-- Table body content will be populated by JavaScript -->
    </tbody>
</table>
</body>
</html>
"###.to_string()
}