diff --git a/bert/caddy-natural-sort.patch b/bert/caddy-natural-sort.patch index f86505a..1c39a5d 100644 --- a/bert/caddy-natural-sort.patch +++ b/bert/caddy-natural-sort.patch @@ -1,17 +1,50 @@ -commit 668a0efbf6591ed7a90298bd8f6c63ad1a783587 +commit cf4751f200cbdb3823d02647a985555e43706f0f Author: Chandler Swift Date: Sat Aug 30 22:52:35 2025 -0500 file_server: Implement natural sort for browse templates + + For files with regions of numbers and other characters, it's intuitive + to have the numbers sorted by increasing numeric value rather than by + ASCII code. (Jeff Atwood has a nice article here: + https://blog.codinghorror.com/sorting-for-humans-natural-sort-order/) + + For example, the listing for a directory containing `foo1`, `foo2`, and + `foo10` would sort in that order, rather than putting `foo10` between + `foo1` and `foo2`. + + Closes #7226 diff --git a/modules/caddyhttp/fileserver/browsetplcontext.go b/modules/caddyhttp/fileserver/browsetplcontext.go -index b9489c6a..339eea21 100644 +index b9489c6a..2d7eae1e 100644 --- a/modules/caddyhttp/fileserver/browsetplcontext.go +++ b/modules/caddyhttp/fileserver/browsetplcontext.go -@@ -325,11 +325,72 @@ type ( - byTime browseTemplateContext - ) +@@ -329,7 +329,7 @@ func (l byName) Len() int { return len(l.Items) } + func (l byName) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] } + func (l byName) Less(i, j int) bool { +- return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name) ++ return naturalLess(strings.ToLower(l.Items[i].Name), strings.ToLower(l.Items[j].Name)) + } + + func (l byNameDirFirst) Len() int { return len(l.Items) } +@@ -338,7 +338,7 @@ func (l byNameDirFirst) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l. + func (l byNameDirFirst) Less(i, j int) bool { + // sort by name if both are dir or file + if l.Items[i].IsDir == l.Items[j].IsDir { +- return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name) ++ return naturalLess(strings.ToLower(l.Items[i].Name), strings.ToLower(l.Items[j].Name)) + } + // sort dir ahead of file + return l.Items[i].IsDir +diff --git a/modules/caddyhttp/fileserver/natsort.go b/modules/caddyhttp/fileserver/natsort.go +new file mode 100644 +index 00000000..65f6a161 +--- /dev/null ++++ b/modules/caddyhttp/fileserver/natsort.go +@@ -0,0 +1,65 @@ ++package fileserver ++ +func isDigit(b byte) bool { return '0' <= b && b <= '9' } + +// naturalLess compares two strings using natural ordering. This means that e.g. @@ -22,6 +55,9 @@ index b9489c6a..339eea21 100644 +// the number of leading zeros is used as a tie-breaker, so e.g. "2" < "02") +// +// Limitation: only ASCII digits (0-9) are considered. ++// ++// This implementation is copied from https://github.com/fvbommel/sortorder, ++// which is MIT licensed. +func naturalLess(str1, str2 string) bool { + idx1, idx2 := 0, 0 + for idx1 < len(str1) && idx2 < len(str2) { @@ -72,22 +108,93 @@ index b9489c6a..339eea21 100644 + // it sorts last. + return len(str1) < len(str2) +} +diff --git a/modules/caddyhttp/fileserver/natsort_test.go b/modules/caddyhttp/fileserver/natsort_test.go +new file mode 100644 +index 00000000..321f1197 +--- /dev/null ++++ b/modules/caddyhttp/fileserver/natsort_test.go +@@ -0,0 +1,63 @@ ++// Copyright 2015 Matthew Holt and The Caddy Authors ++// ++// Licensed under the Apache License, Version 2.0 (the "License"); ++// you may not use this file except in compliance with the License. ++// You may obtain a copy of the License at ++// ++// http://www.apache.org/licenses/LICENSE-2.0 ++// ++// Unless required by applicable law or agreed to in writing, software ++// distributed under the License is distributed on an "AS IS" BASIS, ++// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++// See the License for the specific language governing permissions and ++// limitations under the License. + - func (l byName) Len() int { return len(l.Items) } - func (l byName) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] } - - func (l byName) Less(i, j int) bool { -- return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name) -+ return naturalLess(strings.ToLower(l.Items[i].Name), strings.ToLower(l.Items[j].Name)) - } - - func (l byNameDirFirst) Len() int { return len(l.Items) } -@@ -338,7 +399,7 @@ func (l byNameDirFirst) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l. - func (l byNameDirFirst) Less(i, j int) bool { - // sort by name if both are dir or file - if l.Items[i].IsDir == l.Items[j].IsDir { -- return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name) -+ return naturalLess(strings.ToLower(l.Items[i].Name), strings.ToLower(l.Items[j].Name)) - } - // sort dir ahead of file - return l.Items[i].IsDir ++package fileserver ++ ++import ( ++ "context" ++ "io/fs" ++ "net/http/httptest" ++ "os" ++ "testing" ++ ++ "github.com/caddyserver/caddy/v2/modules/caddyhttp" ++) ++ ++// TestNatSort confirms that, although an ASCIIbetical sort would order foo2.txt ++// after foo10.txt, Caddy will return them in a "natural" human-intuitive order. ++func TestNatSort(t *testing.T) { ++ fsrv := &FileServer{Browse: &Browse{}} ++ ++ base := "./testdata" ++ dirName := "natsort" ++ ++ fsys := os.DirFS(base) ++ f, err := fsys.Open(dirName) ++ if err != nil { ++ t.Fatalf("opening testdata dir: %v", err) ++ } ++ defer f.Close() ++ ++ repl := caddyhttp.NewTestReplacer(httptest.NewRequest("GET", "/", nil)) ++ ++ listing, err := fsrv.loadDirectoryContents(context.Background(), fsys, f.(fs.ReadDirFile), base, "/natsort/", repl) ++ if err != nil { ++ t.Fatalf("loadDirectoryContents returned error: %v", err) ++ } ++ ++ if len(listing.Items) != 3 { ++ t.Fatalf("expected 3 items in listing, got %d", len(listing.Items)) ++ } ++ ++ listing.applySortAndLimit(sortByNameDirFirst, sortOrderAsc, "", "") ++ ++ got := []string{listing.Items[0].Name, listing.Items[1].Name, listing.Items[2].Name} ++ want := []string{"foo1.txt", "foo2.txt", "foo10.txt"} ++ ++ for i := range want { ++ if got[i] != want[i] { ++ t.Fatalf("unexpected item at index %v: got %v, want %v", i, got, want) ++ } ++ } ++} +diff --git a/modules/caddyhttp/fileserver/testdata/natsort/foo1.txt b/modules/caddyhttp/fileserver/testdata/natsort/foo1.txt +new file mode 100644 +index 00000000..b0a92ba4 +--- /dev/null ++++ b/modules/caddyhttp/fileserver/testdata/natsort/foo1.txt +@@ -0,0 +1 @@ ++foo1.txt +diff --git a/modules/caddyhttp/fileserver/testdata/natsort/foo10.txt b/modules/caddyhttp/fileserver/testdata/natsort/foo10.txt +new file mode 100644 +index 00000000..ade2a424 +--- /dev/null ++++ b/modules/caddyhttp/fileserver/testdata/natsort/foo10.txt +@@ -0,0 +1 @@ ++foo10.txt +diff --git a/modules/caddyhttp/fileserver/testdata/natsort/foo2.txt b/modules/caddyhttp/fileserver/testdata/natsort/foo2.txt +new file mode 100644 +index 00000000..6aab57b1 +--- /dev/null ++++ b/modules/caddyhttp/fileserver/testdata/natsort/foo2.txt +@@ -0,0 +1 @@ ++foo2.txt