Tuesday | 05 NOV 2024
[ previous ]
[ next ]

Directory Listing in WebDAV

Title:
Date: 2023-01-26
Tags:  

To get a directory listing from webdav on nginx, we'll first need to install the ngx_http_dav_ext_module.

https://nivethan.dev/devlog/nginx-and-webdav-extended.html

Once this module is set up and the methods defined in the nginx configuration we can then test it out.

curl -X PROPFIND http://user:pass@localhost/webdav/ -H "Depth: 1"

The depth option tells webdav how much information to return. Though I tried changing the depth to 2 and that caused an error. I'm going to stick to Depth of 1 for now but this is something to look up.

This will return something like:

<?xml version="1.0" encoding="utf-8" ?>
<D:multistatus xmlns:D="DAV:">
    <D:response>
        <D:href>/webdav</D:href>
        <D:propstat>
            <D:prop>
                <D:displayname>webdav</D:displayname>
                <D:getlastmodified>Fri, 27 Jan 2023 02:12:09 GMT</D:getlastmodified>
                <D:resourcetype><D:collection/></D:resourcetype>
                <D:lockdiscovery/>
                <D:supportedlock>
                </D:supportedlock>
            </D:prop>
            <D:status>HTTP/1.1 200 OK</D:status>
        </D:propstat>
    </D:response>
    <D:response>
        <D:href>/webdav/test.txt</D:href>
        <D:propstat>
            <D:prop>
                <D:displayname>test.txt</D:displayname>
                <D:getcontentlength>0</D:getcontentlength>
                <D:getlastmodified>Fri, 27 Jan 2023 01:30:28 GMT</D:getlastmodified>
                <D:resourcetype></D:resourcetype>
                <D:lockdiscovery/>
                <D:supportedlock>
                </D:supportedlock>
            </D:prop>
            <D:status>HTTP/1.1 200 OK</D:status>
        </D:propstat>
    </D:response>
</D:multistatus>

The response is verbose but it tells us quite a bit. It gives the path to the file, the name of the file, the size, the last modified and the resourcetype. In the case of a folder, it will get a resource type called collection.

This same call can also be made in javascript:

    let response = await fetch(`/webdav/`, {
        method: 'PROPFIND',
        headers: new Headers({ "Depth": "1" }),
    });

    let data = await response.text();
    console.log(data);

Now that we have the data, we can write some helper utilities to convert this xml to json objects. I find it much easier to manage json so I'm going to write a few helper functions.

The first step is to take the data and convert it to an xml object. The browser has a DOMParser included with it so we will use that.

    let data = await response.text();
    data = data.replaceAll("D:","");

    let parser = new DOMParser();

    let xml = parser.parseFromString(data, "text/xml");
    let rawItems = xml.getElementsByTagName("response");

We take the data, do a bit of cleanup and then we build an xml object. We can then get all the response elements. This will be an array that we will then iterate over.

    let items = [];

    for (let r of rawItems) {
        let item = { }; 

        item.isDirectory = isDirectory(r);
        item.name = getValue(r, "displayname");
        item.href = getValue(r, "href");
        item.modified = getValue(r, "getlastmodified");

        if (!item.isDirectory) {
            item.size = getValue(r, "getcontentlength");
        }

        items.push(item);
    }

We loop through the array and the first thing we need to do is find out if the response element we're processing is a file or a directory. We will find this information by checking the resource type.

function isDirectory(item) {
    let collection = item.getElementsByTagName("collection")[0];
    if (collection === undefined || collection === null) {
        return false;
    } else {
        return true;
    }
}

All responses will have a resourcetype associated with it but only folders will have the collection tag inside them. So we can check for the collection tag.

This is what the resourcetype tag of a folder would look like:

<resourcetype><collection/></resourcetype>

The second function we need is one that will get the value inside an arbitrary field that we can pass it.

function getValue(item, key) {
    let value = item.getElementsByTagName(key)[0].childNodes[0].nodeValue;
    return value;
}

Here we do some xml wrangling and return a value.

We then use all the values to build a javascript object. We also check to see if its a directory before we get the size field, this is because directories don't have a size associated with them.

With that we are done! We have now gotten a directory listing from a webdav server and made it into something that we can easily use.

The next step on this adventure is to add in petitevue and build a rudimentary file explorer.

The full code:

function getValue(item, key) {
    let value = item.getElementsByTagName(key)[0].childNodes[0].nodeValue;
    return value;
}

function isDirectory(item) {
    let collection = item.getElementsByTagName("collection")[0];
    if (collection === undefined || collection === null) {
        return false;
    } else {
        return true;
    }
}

async function main() {
    let response = await fetch(`/webdav/`, {
        method: 'PROPFIND',
        headers: new Headers({ "Depth": "1" }),
    });

    let data = await response.text();
    data = data.replaceAll("D:","");

    let parser = new DOMParser();

    let xml = parser.parseFromString(data, "text/xml");
    let rawItems = xml.getElementsByTagName("response");

    for (let r of rawItems) {
        let item = { }; 

        item.isDirectory = isDirectory(r);
        item.name = getValue(r, "displayname");
        item.href = getValue(r, "href");
        item.modified = getValue(r, "getlastmodified");

        if (!item.isDirectory) {
            item.size = getValue(r, "getcontentlength");
        }
        console.log(item);
    }
}

main();