Support for directory listing of ResourceCollections

This is a counter to #8427 to show that Resource.listing is still needed
This commit is contained in:
Greg Wilkins 2022-08-15 10:54:00 +10:00
parent 9e745f7fdb
commit e0a9c21615
6 changed files with 178 additions and 76 deletions

View File

@ -13,24 +13,18 @@
package org.eclipse.jetty.server;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.UrlEncoded;
import org.eclipse.jetty.util.resource.PathCollators;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollators;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -58,26 +52,8 @@ public class ResourceListing
base = URIUtil.normalizePath(base);
if (base == null || !resource.isDirectory())
return null;
Path path = resource.getPath();
if (path == null) // Should never happen, as new Resource contract is that all Resources are a Path.
return null;
List<Path> listing = null;
try (Stream<Path> listStream = Files.list(resource.getPath()))
{
listing = listStream.collect(Collectors.toCollection(ArrayList::new));
}
catch (IOException e)
{
if (LOG.isDebugEnabled())
LOG.debug("Unable to get Directory Listing for: {}", resource, e);
}
if (listing == null)
{
return null;
}
List<Resource> listing = new ArrayList<>(resource.list().stream().map(URIUtil::encodePath).map(resource::resolve).toList());
boolean sortOrderAscending = true;
String sortColumn = "N"; // name (or "M" for Last Modified, or "S" for Size)
@ -108,18 +84,13 @@ public class ResourceListing
}
// Perform sort
if (sortColumn.equals("M"))
Comparator<? super Resource> sort = switch (sortColumn)
{
listing.sort(PathCollators.byLastModified(sortOrderAscending));
}
else if (sortColumn.equals("S"))
{
listing.sort(PathCollators.bySize(sortOrderAscending));
}
else
{
listing.sort(PathCollators.byName(sortOrderAscending));
}
case "M" -> ResourceCollators.byLastModified(sortOrderAscending);
case "S" -> ResourceCollators.bySize(sortOrderAscending);
default -> ResourceCollators.byName(sortOrderAscending);
};
listing.sort(sort);
String decodedBase = URIUtil.decodePath(base);
String title = "Directory: " + deTag(decodedBase);
@ -229,30 +200,22 @@ public class ResourceListing
DateFormat dfmt = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM);
for (Path item : listing)
for (Resource item : listing)
{
Path fileName = item.getFileName();
if (fileName == null)
{
continue; // skip
}
String name = fileName.toString();
// TODO this feels fragile, as collections probably should not return a Path here
// and even if they do, it might not be named correctly
String name = item.getPath().toFile().getName();
if (StringUtil.isBlank(name))
{
return null;
}
continue;
if (Files.isDirectory(item))
{
if (item.isDirectory() && !name.endsWith("/"))
name += URIUtil.SLASH;
}
// Name
buf.append("<tr><td class=\"name\"><a href=\"");
String href = URIUtil.addEncodedPaths(encodedBase, URIUtil.encodePath(name));
buf.append(href);
// TODO should this be a relative link?
String path = URIUtil.addEncodedPaths(encodedBase, URIUtil.encodePath(name));
buf.append(path);
buf.append("\">");
buf.append(deTag(name));
buf.append("&nbsp;");
@ -260,35 +223,21 @@ public class ResourceListing
// Last Modified
buf.append("<td class=\"lastmodified\">");
try
{
FileTime lastModified = Files.getLastModifiedTime(item, LinkOption.NOFOLLOW_LINKS);
buf.append(dfmt.format(new Date(lastModified.toMillis())));
}
catch (IOException ignore)
{
// do nothing (lastModifiedTime not supported by this file system)
}
long lastModified = item.lastModified();
if (lastModified > 0)
buf.append(dfmt.format(new Date(item.lastModified())));
buf.append("&nbsp;</td>");
// Size
buf.append("<td class=\"size\">");
try
long length = item.length();
if (length >= 0)
{
long length = Files.size(item);
if (length >= 0)
{
buf.append(String.format("%,d bytes", length));
}
}
catch (IOException ignore)
{
// do nothing (size not supported by this file system)
buf.append(String.format("%,d bytes", item.length()));
}
buf.append("&nbsp;</td></tr>\n");
}
buf.append("</tbody>\n");
buf.append("</table>\n");
buf.append("</body></html>\n");

View File

@ -216,6 +216,8 @@ public class ResourceHandler extends Handler.Wrapper
*/
public void setBaseResource(Resource base)
{
if (isStarted())
throw new IllegalStateException(getState());
_resourceBase = base;
}

View File

@ -55,6 +55,7 @@ import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.resource.FileSystemPool;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
@ -1771,6 +1772,153 @@ public class ResourceHandlerTest
assertThat(response.get(LOCATION), endsWith("/context/directory/;JSESSIONID=12345678?name=value"));
}
@Test
public void testDirectory() throws Exception
{
copySimpleTestResource(docRoot);
HttpTester.Response response = HttpTester.parseResponse(
_local.getResponse("""
GET /context/ HTTP/1.1\r
Host: local\r
Connection: close\r
\r
"""));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
String content = response.getContent();
assertThat(content, containsString("<link href=\"jetty-dir.css\" rel=\"stylesheet\" />"));
assertThat(content, containsString("Directory: /context"));
assertThat(content, containsString("/context/big.txt")); // TODO should these be relative links?
assertThat(content, containsString("/context/simple.txt"));
assertThat(content, containsString("/context/directory/"));
response = HttpTester.parseResponse(
_local.getResponse("""
GET /context/jetty-dir.css HTTP/1.1\r
Host: local\r
Connection: close\r
\r
"""));
// TODO fix this!
// assertThat(response.getStatus(), is(HttpStatus.OK_200));
}
@Test
public void testDirectoryOfCollection() throws Exception
{
copySimpleTestResource(docRoot);
_rootResourceHandler.stop();
_rootResourceHandler.setBaseResource(Resource.combine(
ResourceFactory.root().newResource(MavenTestingUtils.getTestResourcePathDir("layer0/")),
_rootResourceHandler.getResourceBase()));
_rootResourceHandler.start();
HttpTester.Response response = HttpTester.parseResponse(
_local.getResponse("""
GET /context/other/ HTTP/1.1\r
Host: local\r
Connection: close\r
\r
"""));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
String content = response.getContent();
assertThat(content, containsString("<link href=\"jetty-dir.css\" rel=\"stylesheet\" />"));
assertThat(content, containsString("Directory: /context/other"));
assertThat(content, containsString("/context/other/data.txt"));
response = HttpTester.parseResponse(
_local.getResponse("""
GET /context/other/jetty-dir.css HTTP/1.1\r
Host: local\r
Connection: close\r
\r
"""));
// TODO fix this!
// assertThat(response.getStatus(), is(HttpStatus.OK_200));
response = HttpTester.parseResponse(
_local.getResponse("""
GET /context/double/ HTTP/1.1\r
Host: local\r
Connection: close\r
\r
"""));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContent();
assertThat(content, containsString("<link href=\"jetty-dir.css\" rel=\"stylesheet\" />"));
assertThat(content, containsString("Directory: /context/double"));
assertThat(content, containsString("/context/double/zero.txt"));
response = HttpTester.parseResponse(
_local.getResponse("""
GET /context/double/jetty-dir.css HTTP/1.1\r
Host: local\r
Connection: close\r
\r
"""));
// TODO fix this!
// assertThat(response.getStatus(), is(HttpStatus.OK_200));
}
@Test
public void testDirectoryOfCollections() throws Exception
{
copySimpleTestResource(docRoot);
_rootResourceHandler.stop();
_rootResourceHandler.setBaseResource(Resource.combine(
ResourceFactory.root().newResource(MavenTestingUtils.getTestResourcePathDir("layer0/")),
ResourceFactory.root().newResource(MavenTestingUtils.getTestResourcePathDir("layer1/")),
_rootResourceHandler.getResourceBase()));
_rootResourceHandler.start();
HttpTester.Response response = HttpTester.parseResponse(
_local.getResponse("""
GET /context/ HTTP/1.1\r
Host: local\r
Connection: close\r
\r
"""));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
String content = response.getContent();
assertThat(content, containsString("<link href=\"jetty-dir.css\" rel=\"stylesheet\" />"));
assertThat(content, containsString("Directory: /context"));
assertThat(content, containsString("/context/big.txt")); // TODO should these be relative links?
assertThat(content, containsString("/context/simple.txt"));
assertThat(content, containsString("/context/directory/"));
assertThat(content, containsString("/context/other/"));
assertThat(content, containsString("/context/double/"));
response = HttpTester.parseResponse(
_local.getResponse("""
GET /context/double/ HTTP/1.1\r
Host: local\r
Connection: close\r
\r
"""));
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContent();
assertThat(content, containsString("<link href=\"jetty-dir.css\" rel=\"stylesheet\" />"));
assertThat(content, containsString("Directory: /context/double"));
assertThat(content, containsString("/context/double/zero.txt"));
assertThat(content, containsString("/context/double/one.txt"));
response = HttpTester.parseResponse(
_local.getResponse("""
GET /context/double/jetty-dir.css HTTP/1.1\r
Host: local\r
Connection: close\r
\r
"""));
// TODO fix this!
// assertThat(response.getStatus(), is(HttpStatus.OK_200));
}
@Test
public void testEtagIfMatchAlwaysFailsDueToWeakEtag() throws Exception
{

View File

@ -0,0 +1 @@
From layer zero

View File

@ -0,0 +1 @@
some data

View File

@ -0,0 +1 @@
From layer 1