ifdef NoNanoHTTPD !include once #1028763 endifdef // TODO: implement If-Modified-Since? sclass HttpFromFileSystem { File baseDir; bool serveDirectoryListings; // default to false bool serveIndexHTML = true; bool maxCache; // treat files as immutable bool debug; bool sendLastModified = true; settable bool serveDirectoriesAsZipFiles; SS params, headers; S baseLink = ""; *() {} *(File *baseDir) {} *(File *baseDir, bool *serveDirectoryListings) {} O serve(S uri, SS params, SS headers default null) { if (eq("1", params.get("maxCache"))) maxCache = true; this.params = params; this.headers = headers; ret serve(uri); } O serve(S uri) { S originalURI = uri; uri = urldecode(uri); LS parts = nempties(splitAtSlash(uri)); if (baseDir == null) ret serve404("No base directory set"); // process . and .. for i over parts: if (eq(parts.get(i), ".")) parts.remove(i); else if (eq(parts.get(i), "..")) if (i == 0) fail(".. leading nowhere: " + uri); else { parts.remove(i); parts.remove(--i); } if (!all validFileName(parts)) { print("Parts: " + quoteAll(parts)); fail("Bad path: " + uri); } File f = baseDir; if (!isDirectory(baseDir)) print("Base dir not found: " + f2s(f)); for (S part : parts) { f = newFile(f, part); if (!fileExists(f)) ret serve404("Path not found: " + uri); } print("Trying to serve: " + f2s(f)); if (isDirectory(f)) if (eq(params.get("asZip"), "1")) ret serveDirAsZip(f); else ret serveListingOrIndexHTML(uri, f, originalURI); else ret serveFile(f); } swappable O addHeaders(O response) { if (maxCache) ret subBot_maxCacheHeaders(response); ret response; } swappable O serveFile(File f) { O response; S range = mapGet(headers, "range"); S ct = contentTypeForFile(f); if (nempty(range)) { print("Serving range query " + f + ": " + range); response = subBot_serveFileRange(f, ct, range); } else { response = main serveFile(f, ct); if (sendLastModified) addHeader("Last-Modified", formatDateForLastModifiedHeader(f.lastModified()), response); } ret addHeaders(response); } O serveListingOrIndexHTML(S uri, File dir, S originalURI) { if (serveIndexHTML) { File index = newFile(dir, "index.html"); if (fileExists(index)) { /*if (nempty(originalURI) && !endsWithSlash(originalURI)) ret subBot_serveRedirect(baseLink + originalURI + "/");*/ ret serveFile(index); } } ret serveListing(uri, dir, originalURI); } O serveListing(S uri, File dir, S originalURI) { if (!serveDirectoryListings) fail("Can't serve directory listings"); // take care of directory listing without trailing slash in URI // XXX broken? S local = addSlashIfNempty(afterLast("/" + uri, '/')); S local = addSlashIfNempty(uri); var names = listFileNames(dir); sortInPlaceByCalculatedField(names, name -> { File f = newFile(dir, name); ret f.isDirectory() ? pair(false, lower(name)) : pair(true, modificationTime(f)); }); S parent = dropAfterLastSlashOrEmpty(dropTrailingSlash(uri)); S title = "Directory listing of " + htmlEncode2(dropTrailingSlash(baseLink + uri)); S debugText = !debug ? "" : htmlEncode2(renderVars(+baseLink, +uri, +local, +parent)); O body = postProcessDirListing(h3(title) + pUnlessEmpty(debugText) + (l(parent) >= l(uri) ? "" : p(ahref(addSlashSuffix(baseLink + parent), "Parent directory"))) + ul(map(names, name -> { File f = newFile(dir, name); bool isDir = isDirectory(f); if (isDir) name += "/"; new LS l; // make the actual link to file/directroy l.add(ahref(baseLink + local + name, htmlEncode2(name))); if (isDir && serveDirectoriesAsZipFiles) l.add(ahref(appendParamsToURL(baseLink + local + name, asZip := 1), "[download zip]")); if (!isDir) l.add(bracketed(joinNemptiesWithComma(fileInfoHintsWithDate(f)))); ret hcombine(l); }))); ret hhtml( hhead(hcombine( htitle_decode(title), hsansserif(), hmobilefix(), ) + hbody(body)); } swappable S contentTypeForFile(File f) { ret or2(extensionToMimeType(fileExtension(f)), binaryMimeType()); } swappable bool validFileName(S name) { ret isValidFileName(name); } swappable O postProcessDirListing(S html) { ret html; } O serveDirAsZip(File dir) ctex { new ByteArrayOutputStream baos; ZipOutputStream zipOut = new(baos); dir2zip_recurse(dir, zipOut, fileName(dir) + "/", 0); zipOut.close(); byte[] data = baos.toByteArray(); S zipName = dir.getName() + "-" + ymdMinusHMS() + ".zip"; ret addFilenameHeader(zipName, subBot_serveInputStream(byteArrayInputStream(data), zipMimeType())); } }