diff --git a/imageserver/httpd/listDirectories.go b/imageserver/httpd/listDirectories.go index 4a8f695f..1025dbb5 100644 --- a/imageserver/httpd/listDirectories.go +++ b/imageserver/httpd/listDirectories.go @@ -4,6 +4,8 @@ import ( "bufio" "fmt" "net/http" + "net/url" + "text/template" "github.com/Cloud-Foundations/Dominator/lib/html" "github.com/Cloud-Foundations/Dominator/lib/image" @@ -32,8 +34,20 @@ func (s state) listDirectoriesHandler(w http.ResponseWriter, fmt.Fprintln(writer, ``) tw, _ := html.NewTableWriter(writer, true, "Name", "Owner Group") for _, directory := range directories { - tw.WriteRow("", "", directory.Name, directory.Metadata.OwnerGroup) + writeDirectory(tw, directory) } tw.Close() fmt.Fprintln(writer, "") } + +func writeDirectory(tw *html.TableWriter, directory image.Directory) { + name := directory.Name + ownerGroup := directory.Metadata.OwnerGroup + tw.WriteRow("", "", + fmt.Sprintf( + "%s", + url.QueryEscape(name), template.HTMLEscapeString(name), + ), + ownerGroup, + ) +} diff --git a/imageserver/httpd/listImages.go b/imageserver/httpd/listImages.go index a375abff..953b115d 100644 --- a/imageserver/httpd/listImages.go +++ b/imageserver/httpd/listImages.go @@ -14,9 +14,17 @@ import ( func (s state) listImagesHandler(w http.ResponseWriter, req *http.Request) { writer := bufio.NewWriter(w) defer writer.Flush() - imageNames := s.imageDataBase.ListImages() - verstr.Sort(imageNames) - if req.URL.RawQuery == "output=text" { + query := req.URL.Query() + var imageNames []string + if directoryName := query.Get("directoryName"); directoryName != "" && + directoryName != "." { + // Output is already sorted. + imageNames = s.imageDataBase.ListImagesInDirectory(directoryName) + } else { + imageNames = s.imageDataBase.ListImages() + verstr.Sort(imageNames) + } + if query.Get("output") == "text" { for _, name := range imageNames { fmt.Fprintln(writer, name) } diff --git a/imageserver/scanner/api.go b/imageserver/scanner/api.go index a9f42ce2..27067ec5 100644 --- a/imageserver/scanner/api.go +++ b/imageserver/scanner/api.go @@ -30,6 +30,7 @@ type Config struct { } type notifiers map[<-chan string]chan<- string + type makeDirectoryNotifiers map[<-chan image.Directory]chan<- image.Directory type ImageDataBase struct { @@ -42,6 +43,7 @@ type ImageDataBase struct { // Protected by main lock. directoryMap map[string]image.DirectoryMetadata imageMap map[string]*imageType // nil: write in progress. + imageNameIndex imageIndex // sorted keys of image names. addNotifiers notifiers deleteNotifiers notifiers mkdirNotifiers makeDirectoryNotifiers @@ -64,6 +66,16 @@ type Params struct { ObjectServer objectserver.FullObjectServer } +// imageIndex provides exact and prefix lookups over image names. +// Implementations are not safe for concurrent use; callers must hold the +// main lock. +type imageIndex interface { + Add(name string) + Delete(name string) + Get(name string) (string, bool) + GetByPrefix(prefix string) []string +} + func Load(config Config, params Params) (*ImageDataBase, error) { return loadImageDataBase(config, params) } @@ -187,6 +199,11 @@ func (imdb *ImageDataBase) ListSelectedImages( return imdb.listImages(request) } +func (imdb *ImageDataBase) ListImagesInDirectory( + directoryName string) []string { + return imdb.listImagesInDirectory(directoryName) +} + // ListUnreferencedObjects will return a map listing all the objects and their // corresponding sizes which are not referenced by an image. // Note that some objects may have been recently added and the referencing image diff --git a/imageserver/scanner/imdb.go b/imageserver/scanner/imdb.go index d413c915..383d3cca 100644 --- a/imageserver/scanner/imdb.go +++ b/imageserver/scanner/imdb.go @@ -92,6 +92,7 @@ func (imdb *ImageDataBase) addImage(img *image.Image, name string, if doCleanup { imdb.Lock() delete(imdb.imageMap, name) + imdb.imageNameIndex.Delete(name) imdb.Unlock() } }() @@ -392,6 +393,7 @@ func (imdb *ImageDataBase) deleteImageAndUpdateUnreferencedObjectsList( return } delete(imdb.imageMap, name) + imdb.imageNameIndex.Delete(name) imdb.Params.ObjectServer.AdjustRefcounts(false, img) } @@ -612,6 +614,13 @@ func (imdb *ImageDataBase) listImages( return names } +func (imdb *ImageDataBase) listImagesInDirectory( + directoryName string) []string { + imdb.RLock() + defer imdb.RUnlock() + return imdb.imageNameIndex.GetByPrefix(directoryName) +} + func (imdb *ImageDataBase) makeDirectory(directory image.Directory, authInfo *srpc.AuthInformation, userRpc bool) error { imdb.Lock() @@ -743,6 +752,7 @@ func (imdb *ImageDataBase) restoreImageFromArchive( if doCleanup { imdb.Lock() delete(imdb.imageMap, imageArchive.ImageName) + imdb.imageNameIndex.Delete(imageArchive.ImageName) imdb.Unlock() } }() @@ -814,6 +824,7 @@ func (imdb *ImageDataBase) writeImage(name string, img *image.Image, usageEstimate: usageEstimate, } imdb.addNotifiers.sendPlain(name, "add", imdb.Logger) + imdb.imageNameIndex.Add(name) imdb.Unlock() return imdb.Params.ObjectServer.AdjustRefcounts(true, img) } diff --git a/imageserver/scanner/index.go b/imageserver/scanner/index.go new file mode 100644 index 00000000..727fe5fd --- /dev/null +++ b/imageserver/scanner/index.go @@ -0,0 +1,62 @@ +package scanner + +import ( + "slices" + "strings" +) + +// sortedImageIndex is an imageIndex backed by a sorted slice of image names. +type sortedImageIndex struct { + index []string // sorted image names only. +} + +func newImageSortedIndex() *sortedImageIndex { + return &sortedImageIndex{index: make([]string, 0)} +} + +var _ imageIndex = &sortedImageIndex{} + +func (s *sortedImageIndex) Add(name string) { + i, found := slices.BinarySearch(s.index, name) + if found { + return + } + s.index = slices.Insert(s.index, i, name) +} + +func (s *sortedImageIndex) Delete(name string) { + i, found := slices.BinarySearch(s.index, name) + if !found { + return + } + s.index = slices.Delete(s.index, i, i+1) +} + +func (s *sortedImageIndex) GetByPrefix(prefix string) []string { + if !strings.HasSuffix(prefix, "/") { + prefix += "/" + } + start, _ := slices.BinarySearch(s.index, prefix) + if start == len(s.index) || !strings.HasPrefix(s.index[start], prefix) { + return nil + } + relativeEnd, _ := slices.BinarySearchFunc(s.index[start:], prefix, + func(e, t string) int { + if strings.HasPrefix(e, t) { + // Move the search outside of prefix block. + return -1 + } + return strings.Compare(e, t) + }, + ) + end := start + relativeEnd + return slices.Clone(s.index[start:end]) +} + +func (s *sortedImageIndex) Get(name string) (string, bool) { + i, found := slices.BinarySearch(s.index, name) + if !found { + return "", false + } + return s.index[i], true +} diff --git a/imageserver/scanner/load.go b/imageserver/scanner/load.go index cdd63708..47514174 100644 --- a/imageserver/scanner/load.go +++ b/imageserver/scanner/load.go @@ -46,6 +46,7 @@ func loadImageDataBase(config Config, params Params) (*ImageDataBase, error) { Params: params, directoryMap: make(map[string]image.DirectoryMetadata), imageMap: make(map[string]*imageType), + imageNameIndex: newImageSortedIndex(), addNotifiers: make(notifiers), deleteNotifiers: make(notifiers), mkdirNotifiers: make(makeDirectoryNotifiers), @@ -200,6 +201,7 @@ func (imdb *ImageDataBase) loadFile(filename string) error { imdb.Lock() defer imdb.Unlock() imdb.imageMap[filename] = imageEntry + imdb.imageNameIndex.Add(filename) return nil }