Steffag/refactor slices (#38)

Co-authored-by: Graham Steffaniak <graham.steffaniak@autodesk.com>
This commit is contained in:
Graham Steffaniak 2023-09-30 09:18:21 -05:00 committed by GitHub
parent d6e9ed6272
commit 65159848c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 810 additions and 325 deletions

View File

@ -5,33 +5,33 @@
? github.com/gtsteffaniak/filebrowser/auth [no test files]
? github.com/gtsteffaniak/filebrowser/cmd [no test files]
PASS
ok github.com/gtsteffaniak/filebrowser/diskcache 0.003s
ok github.com/gtsteffaniak/filebrowser/diskcache 0.011s
? github.com/gtsteffaniak/filebrowser/errors [no test files]
? github.com/gtsteffaniak/filebrowser/files [no test files]
PASS
ok github.com/gtsteffaniak/filebrowser/fileutils 0.003s
2023/09/24 12:52:05 h: 401 <nil>
2023/09/24 12:52:05 h: 401 <nil>
2023/09/24 12:52:05 h: 401 <nil>
2023/09/24 12:52:05 h: 401 <nil>
2023/09/24 12:52:05 h: 401 <nil>
2023/09/24 12:52:05 h: 401 <nil>
2023/09/29 19:48:56 h: 401 <nil>
2023/09/29 19:48:56 h: 401 <nil>
2023/09/29 19:48:56 h: 401 <nil>
2023/09/29 19:48:56 h: 401 <nil>
2023/09/29 19:48:56 h: 401 <nil>
2023/09/29 19:48:56 h: 401 <nil>
PASS
ok github.com/gtsteffaniak/filebrowser/http 0.094s
ok github.com/gtsteffaniak/filebrowser/http 0.095s
PASS
ok github.com/gtsteffaniak/filebrowser/img 0.144s
PASS
ok github.com/gtsteffaniak/filebrowser/rules 0.002s
PASS
ok github.com/gtsteffaniak/filebrowser/runner 0.003s
ok github.com/gtsteffaniak/filebrowser/img 0.141s
goos: linux
goarch: amd64
pkg: github.com/gtsteffaniak/filebrowser/search
pkg: github.com/gtsteffaniak/filebrowser/index
cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz
BenchmarkSearchAllIndexes-8 10 5912685 ns/op 3003976 B/op 46139 allocs/op
BenchmarkFillIndex-8 10 3157995 ns/op 18370 B/op 449 allocs/op
BenchmarkFillIndex-8 10 3232060 ns/op 12384 B/op 451 allocs/op
BenchmarkSearchAllIndexes-8 10 5611219 ns/op 3262723 B/op 42857 allocs/op
PASS
ok github.com/gtsteffaniak/filebrowser/search 0.116s
ok github.com/gtsteffaniak/filebrowser/index 0.124s
PASS
ok github.com/gtsteffaniak/filebrowser/rules 0.003s
PASS
ok github.com/gtsteffaniak/filebrowser/runner 0.005s
PASS
ok github.com/gtsteffaniak/filebrowser/settings 0.005s
? github.com/gtsteffaniak/filebrowser/share [no test files]

View File

@ -23,7 +23,7 @@ import (
"github.com/gtsteffaniak/filebrowser/diskcache"
fbhttp "github.com/gtsteffaniak/filebrowser/http"
"github.com/gtsteffaniak/filebrowser/img"
"github.com/gtsteffaniak/filebrowser/search"
"github.com/gtsteffaniak/filebrowser/index"
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/users"
)
@ -67,7 +67,7 @@ var rootCmd = &cobra.Command{
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
}
// initialize indexing and schedule indexing ever n minutes (default 5)
go search.InitializeIndex(serverConfig.IndexingInterval)
index.Initialize(serverConfig.IndexingInterval)
_, err := os.Stat(serverConfig.Root)
checkErr(err)
var listener net.Listener

View File

@ -1,6 +1,7 @@
server:
port: 8080
baseURL: "/"
root: "/srv"
auth:
method: noauth
signup: true

View File

@ -3,7 +3,7 @@ package http
import (
"net/http"
"github.com/gtsteffaniak/filebrowser/search"
"github.com/gtsteffaniak/filebrowser/index"
)
var searchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
@ -12,8 +12,9 @@ var searchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *dat
// Retrieve the User-Agent and X-Auth headers from the request
sessionId := r.Header.Get("SessionId")
indexInfo, fileTypes := search.SearchAllIndexes(query, r.URL.Path, sessionId)
for _, path := range indexInfo {
index := *index.GetIndex()
results, fileTypes := index.Search(query, r.URL.Path, sessionId)
for _, path := range results {
responseObj := map[string]interface{}{
"path": path,
"dir": true,

View File

@ -1,4 +1,4 @@
package search
package index
import (
"regexp"

142
backend/index/indexing.go Normal file
View File

@ -0,0 +1,142 @@
package index
import (
"log"
"os"
"slices"
"strings"
"sync"
"time"
"github.com/gtsteffaniak/filebrowser/settings"
)
const (
maxIndexSize = 1000
)
type Index struct {
Dirs []string
Files []string
}
var (
rootPath string = "/rootPath"
indexes Index
indexMutex sync.RWMutex
lastIndexed time.Time
)
func GetIndex() *Index {
return &indexes
}
func Initialize(intervalMinutes uint32) {
// Initialize the index
indexes = Index{
Dirs: make([]string, 0, maxIndexSize),
Files: make([]string, 0, maxIndexSize),
}
rootPath = settings.GlobalConfiguration.Server.Root
var numFiles, numDirs int
log.Println("Indexing files...")
lastIndexedStart := time.Now()
// Call the function to index files and directories
totalNumFiles, totalNumDirs, err := indexFiles(rootPath, &numFiles, &numDirs)
if err != nil {
log.Fatal(err)
}
lastIndexed = lastIndexedStart
go indexingScheduler(intervalMinutes)
log.Println("Successfully indexed files.")
log.Println("Files found :", totalNumFiles)
log.Println("Directories found :", totalNumDirs)
}
func indexingScheduler(intervalMinutes uint32) {
log.Printf("Indexing scheduler will run every %v minutes", intervalMinutes)
for {
indexes.Dirs = slices.Compact(indexes.Dirs)
indexes.Files = slices.Compact(indexes.Files)
time.Sleep(time.Duration(intervalMinutes) * time.Minute)
var numFiles, numDirs int
lastIndexedStart := time.Now()
totalNumFiles, totalNumDirs, err := indexFiles(rootPath, &numFiles, &numDirs)
if err != nil {
log.Fatal(err)
}
lastIndexed = lastIndexedStart
if totalNumFiles+totalNumDirs > 0 {
log.Println("re-indexing found changes and updated the index.")
}
}
}
func removeFromSlice(slice []string, target string) []string {
for i, s := range slice {
if s == target {
// Swap the target element with the last element
slice[i], slice[len(slice)-1] = slice[len(slice)-1], slice[i]
// Resize the slice to exclude the last element
slice = slice[:len(slice)-1]
break // Exit the loop, assuming there's only one target element
}
}
return slice
}
// Define a function to recursively index files and directories
func indexFiles(path string, numFiles *int, numDirs *int) (int, int, error) {
// Check if the current directory has been modified since last indexing
dir, err := os.Open(path)
if err != nil {
// directory must have been deleted, remove from index
indexes.Dirs = removeFromSlice(indexes.Dirs, path)
indexes.Files = removeFromSlice(indexes.Files, path)
}
defer dir.Close()
dirInfo, err := dir.Stat()
if err != nil {
return *numFiles, *numDirs, err
}
// Compare the last modified time of the directory with the last indexed time
if dirInfo.ModTime().Before(lastIndexed) {
return *numFiles, *numDirs, nil
}
// Read the directory contents
files, err := dir.Readdir(-1)
if err != nil {
return *numFiles, *numDirs, err
}
// Iterate over the files and directories
for _, file := range files {
if file.IsDir() {
*numDirs++
addToIndex(path, file.Name(), true)
_, _, err := indexFiles(path+"/"+file.Name(), numFiles, numDirs) // recursive
if err != nil {
log.Println("Could not index :", err)
}
} else {
*numFiles++
addToIndex(path, file.Name(), false)
}
}
return *numFiles, *numDirs, nil
}
func addToIndex(path string, fileName string, isDir bool) {
indexMutex.Lock()
defer indexMutex.Unlock()
path = strings.TrimPrefix(path, rootPath+"/")
path = strings.TrimSuffix(path, "/")
adjustedPath := path + "/" + fileName
if path == rootPath {
adjustedPath = fileName
}
if isDir {
indexes.Dirs = append(indexes.Dirs, adjustedPath)
} else {
indexes.Files = append(indexes.Files, adjustedPath)
}
}

View File

@ -0,0 +1,184 @@
package index
import (
"encoding/json"
"math/rand"
"reflect"
"testing"
"time"
)
func BenchmarkFillIndex(b *testing.B) {
indexes = Index{
Dirs: make([]string, 0, 1000),
Files: make([]string, 0, 1000),
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
createMockData(50, 3) // 1000 dirs, 3 files per dir
}
}
func createMockData(numDirs, numFilesPerDir int) {
for i := 0; i < numDirs; i++ {
dirName := generateRandomPath(rand.Intn(3) + 1)
addToIndex("/", dirName, true)
for j := 0; j < numFilesPerDir; j++ {
fileName := "file-" + getRandomTerm() + getRandomExtension()
addToIndex("/"+dirName, fileName, false)
}
}
}
func generateRandomPath(levels int) string {
rand.New(rand.NewSource(time.Now().UnixNano()))
dirName := "srv"
for i := 0; i < levels; i++ {
dirName += "/" + getRandomTerm()
}
return dirName
}
func getRandomTerm() string {
wordbank := []string{
"hi", "test", "other", "name",
"cool", "things", "more", "items",
}
rand.New(rand.NewSource(time.Now().UnixNano()))
index := rand.Intn(len(wordbank))
return wordbank[index]
}
func getRandomExtension() string {
wordbank := []string{
".txt", ".mp3", ".mov", ".doc",
".mp4", ".bak", ".zip", ".jpg",
}
rand.New(rand.NewSource(time.Now().UnixNano()))
index := rand.Intn(len(wordbank))
return wordbank[index]
}
func generateRandomSearchTerms(numTerms int) []string {
// Generate random search terms
searchTerms := make([]string, numTerms)
for i := 0; i < numTerms; i++ {
searchTerms[i] = getRandomTerm()
}
return searchTerms
}
// JSONBytesEqual compares the JSON in two byte slices.
func JSONBytesEqual(a, b []byte) (bool, error) {
var j, j2 interface{}
if err := json.Unmarshal(a, &j); err != nil {
return false, err
}
if err := json.Unmarshal(b, &j2); err != nil {
return false, err
}
return reflect.DeepEqual(j2, j), nil
}
func TestGetIndex(t *testing.T) {
tests := []struct {
name string
want *map[string][]string
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetIndex(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetIndex() = %v, want %v", got, tt.want)
}
})
}
}
func TestInitializeIndex(t *testing.T) {
type args struct {
intervalMinutes uint32
}
tests := []struct {
name string
args args
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Initialize(tt.args.intervalMinutes)
})
}
}
func Test_indexingScheduler(t *testing.T) {
type args struct {
intervalMinutes uint32
}
tests := []struct {
name string
args args
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
indexingScheduler(tt.args.intervalMinutes)
})
}
}
func Test_indexFiles(t *testing.T) {
type args struct {
path string
numFiles *int
numDirs *int
}
tests := []struct {
name string
args args
want int
want1 int
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := indexFiles(tt.args.path, tt.args.numFiles, tt.args.numDirs)
if (err != nil) != tt.wantErr {
t.Errorf("indexFiles() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("indexFiles() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("indexFiles() got1 = %v, want %v", got1, tt.want1)
}
})
}
}
func Test_addToIndex(t *testing.T) {
type args struct {
path string
fileName string
isDir bool
}
tests := []struct {
name string
args args
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
addToIndex(tt.args.path, tt.args.fileName, tt.args.isDir)
})
}
}

View File

@ -1,7 +1,6 @@
package search
package index
import (
"log"
"math/rand"
"mime"
"os"
@ -13,142 +12,55 @@ import (
)
var (
sessionInProgress sync.Map // Track session with requests in progress
rootPath string = "/srv"
indexes map[string][]string
sessionInProgress sync.Map
mutex sync.RWMutex
lastIndexed time.Time
maxSearchResults = 100
bytesInMegabyte int64 = 1000000
)
func InitializeIndex(intervalMinutes uint32) {
// Initialize the indexes map
indexes = make(map[string][]string)
indexes["dirs"] = []string{}
indexes["files"] = []string{}
var numFiles, numDirs int
log.Println("Indexing files...")
lastIndexedStart := time.Now()
// Call the function to index files and directories
totalNumFiles, totalNumDirs, err := indexFiles(rootPath, &numFiles, &numDirs)
if err != nil {
log.Fatal(err)
}
lastIndexed = lastIndexedStart
go indexingScheduler(intervalMinutes)
log.Println("Successfully indexed files.")
log.Println("Files found :", totalNumFiles)
log.Println("Directories found :", totalNumDirs)
}
func indexingScheduler(intervalMinutes uint32) {
log.Printf("Indexing scheduler will run every %v minutes", intervalMinutes)
for {
time.Sleep(time.Duration(intervalMinutes) * time.Minute)
var numFiles, numDirs int
lastIndexedStart := time.Now()
totalNumFiles, totalNumDirs, err := indexFiles(rootPath, &numFiles, &numDirs)
if err != nil {
log.Fatal(err)
}
lastIndexed = lastIndexedStart
if totalNumFiles+totalNumDirs > 0 {
log.Println("re-indexing found changes and updated the index.")
}
}
}
// Define a function to recursively index files and directories
func indexFiles(path string, numFiles *int, numDirs *int) (int, int, error) {
// Check if the current directory has been modified since last indexing
dir, err := os.Open(path)
if err != nil {
// directory must have been deleted, remove from index
delete(indexes, path)
}
defer dir.Close()
dirInfo, err := dir.Stat()
if err != nil {
return *numFiles, *numDirs, err
}
// Compare the last modified time of the directory with the last indexed time
if dirInfo.ModTime().Before(lastIndexed) {
return *numFiles, *numDirs, nil
}
// Read the directory contents
files, err := dir.Readdir(-1)
if err != nil {
return *numFiles, *numDirs, err
}
// Iterate over the files and directories
for _, file := range files {
if file.IsDir() {
*numDirs++
addToIndex(path, file.Name(), true)
_, _, err := indexFiles(path+"/"+file.Name(), numFiles, numDirs) // recursive
if err != nil {
log.Println("Could not index :", err)
return 0, 0, nil
}
} else {
*numFiles++
addToIndex(path, file.Name(), false)
}
}
return *numFiles, *numDirs, nil
}
func addToIndex(path string, fileName string, isDir bool) {
mutex.Lock()
defer mutex.Unlock()
path = strings.TrimPrefix(path, rootPath+"/")
path = strings.TrimSuffix(path, "/")
adjustedPath := path + "/" + fileName
if path == rootPath {
adjustedPath = fileName
}
if isDir {
indexes["dirs"] = append(indexes["dirs"], adjustedPath)
} else {
indexes["files"] = append(indexes["files"], adjustedPath)
}
}
func SearchAllIndexes(search string, scope string, sourceSession string) ([]string, map[string]map[string]bool) {
func (si *Index) Search(search string, scope string, sourceSession string) ([]string, map[string]map[string]bool) {
runningHash := generateRandomHash(4)
sessionInProgress.Store(sourceSession, runningHash) // Store the value in the sync.Map
searchOptions := ParseSearch(search)
mutex.RLock()
defer mutex.RUnlock()
fileListTypes := make(map[string]map[string]bool)
var matching []string
maximum := 100
for _, searchTerm := range searchOptions.Terms {
if searchTerm == "" {
continue
}
// Iterate over the indexes
for _, i := range []string{"dirs", "files"} {
isdir := i == "dirs"
// Iterate over the embedded index.Index fields Dirs and Files
for _, i := range []string{"Dirs", "Files"} {
isDir := false
count := 0
for _, path := range indexes[i] {
var paths []string
switch i {
case "Dirs":
isDir = true
paths = si.Dirs
case "Files":
paths = si.Files
}
for _, path := range paths {
value, found := sessionInProgress.Load(sourceSession)
if !found || value != runningHash {
return []string{}, map[string]map[string]bool{}
}
if count > maximum {
if count > maxSearchResults {
break
}
pathName := scopedPathNameFilter(path, scope)
if pathName == "" {
continue
}
matches, fileType := containsSearchTerm(path, searchTerm, *searchOptions, isdir)
matches, fileType := containsSearchTerm(path, searchTerm, *searchOptions, isDir)
if !matches {
continue
}
if isdir {
if isDir {
pathName = pathName + "/"
}
matching = append(matching, pathName)
@ -216,9 +128,9 @@ func containsSearchTerm(pathName string, searchTerm string, options SearchOption
var matchesCondition bool
switch t {
case "larger":
matchesCondition = fileSize > int64(options.LargerThan)*1000000
matchesCondition = fileSize > int64(options.LargerThan)*bytesInMegabyte
case "smaller":
matchesCondition = fileSize < int64(options.SmallerThan)*1000000
matchesCondition = fileSize < int64(options.SmallerThan)*bytesInMegabyte
default:
matchesCondition = v == fileTypes[t]
}

View File

@ -0,0 +1,251 @@
package index
import (
"reflect"
"testing"
)
func BenchmarkSearchAllIndexes(b *testing.B) {
indexes = Index{
Dirs: make([]string, 0, 1000),
Files: make([]string, 0, 1000),
}
// Create mock data
createMockData(50, 3) // 1000 dirs, 3 files per dir
// Generate 100 random search terms
searchTerms := generateRandomSearchTerms(100)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// Execute the SearchAllIndexes function
for _, term := range searchTerms {
indexes.Search(term, "/", "test")
}
}
}
// loop over test files and compare output
func TestParseSearch(t *testing.T) {
value := ParseSearch("my test search")
want := &SearchOptions{
Conditions: map[string]bool{
"exact": false,
},
Terms: []string{"my test search"},
}
if !reflect.DeepEqual(value, want) {
t.Fatalf("\n got: %+v\n want: %+v", value, want)
}
value = ParseSearch("case:exact my|test|search")
want = &SearchOptions{
Conditions: map[string]bool{
"exact": true,
},
Terms: []string{"my", "test", "search"},
}
if !reflect.DeepEqual(value, want) {
t.Fatalf("\n got: %+v\n want: %+v", value, want)
}
value = ParseSearch("type:largerThan=100 type:smallerThan=1000 test")
want = &SearchOptions{
Conditions: map[string]bool{
"exact": false,
"larger": true,
},
Terms: []string{"test"},
LargerThan: 100,
SmallerThan: 1000,
}
if !reflect.DeepEqual(value, want) {
t.Fatalf("\n got: %+v\n want: %+v", value, want)
}
value = ParseSearch("type:audio thisfile")
want = &SearchOptions{
Conditions: map[string]bool{
"exact": false,
"audio": true,
},
Terms: []string{"thisfile"},
}
if !reflect.DeepEqual(value, want) {
t.Fatalf("\n got: %+v\n want: %+v", value, want)
}
}
func TestSearchIndexes(t *testing.T) {
type args struct {
search string
scope string
sourceSession string
}
tests := []struct {
name string
args args
want []string
want1 map[string]map[string]bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1 := indexes.Search(tt.args.search, tt.args.scope, tt.args.sourceSession)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("SearchAllIndexes() got = %v, want %v", got, tt.want)
}
if !reflect.DeepEqual(got1, tt.want1) {
t.Errorf("SearchAllIndexes() got1 = %v, want %v", got1, tt.want1)
}
})
}
}
func Test_scopedPathNameFilter(t *testing.T) {
type args struct {
pathName string
scope string
}
tests := []struct {
name string
args args
want string
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := scopedPathNameFilter(tt.args.pathName, tt.args.scope); got != tt.want {
t.Errorf("scopedPathNameFilter() = %v, want %v", got, tt.want)
}
})
}
}
func Test_containsSearchTerm(t *testing.T) {
type args struct {
pathName string
searchTerm string
options SearchOptions
isDir bool
}
tests := []struct {
name string
args args
want bool
want1 map[string]bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1 := containsSearchTerm(tt.args.pathName, tt.args.searchTerm, tt.args.options, tt.args.isDir)
if got != tt.want {
t.Errorf("containsSearchTerm() got = %v, want %v", got, tt.want)
}
if !reflect.DeepEqual(got1, tt.want1) {
t.Errorf("containsSearchTerm() got1 = %v, want %v", got1, tt.want1)
}
})
}
}
func Test_isDoc(t *testing.T) {
type args struct {
extension string
}
tests := []struct {
name string
args args
want bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isDoc(tt.args.extension); got != tt.want {
t.Errorf("isDoc() = %v, want %v", got, tt.want)
}
})
}
}
func Test_getFileSize(t *testing.T) {
type args struct {
filepath string
}
tests := []struct {
name string
args args
want int64
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := getFileSize(tt.args.filepath); got != tt.want {
t.Errorf("getFileSize() = %v, want %v", got, tt.want)
}
})
}
}
func Test_isArchive(t *testing.T) {
type args struct {
extension string
}
tests := []struct {
name string
args args
want bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isArchive(tt.args.extension); got != tt.want {
t.Errorf("isArchive() = %v, want %v", got, tt.want)
}
})
}
}
func Test_getLastPathComponent(t *testing.T) {
type args struct {
path string
}
tests := []struct {
name string
args args
want string
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := getLastPathComponent(tt.args.path); got != tt.want {
t.Errorf("getLastPathComponent() = %v, want %v", got, tt.want)
}
})
}
}
func Test_generateRandomHash(t *testing.T) {
type args struct {
length int
}
tests := []struct {
name string
args args
want string
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := generateRandomHash(tt.args.length); got != tt.want {
t.Errorf("generateRandomHash() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -1,147 +0,0 @@
package search
import (
"encoding/json"
"math/rand"
"reflect"
"testing"
"time"
)
// loop over test files and compare output
func TestParseSearch(t *testing.T) {
value := ParseSearch("my test search")
want := &SearchOptions{
Conditions: map[string]bool{
"exact": false,
},
Terms: []string{"my test search"},
}
if !reflect.DeepEqual(value, want) {
t.Fatalf("\n got: %+v\n want: %+v", value, want)
}
value = ParseSearch("case:exact my|test|search")
want = &SearchOptions{
Conditions: map[string]bool{
"exact": true,
},
Terms: []string{"my", "test", "search"},
}
if !reflect.DeepEqual(value, want) {
t.Fatalf("\n got: %+v\n want: %+v", value, want)
}
value = ParseSearch("type:largerThan=100 type:smallerThan=1000 test")
want = &SearchOptions{
Conditions: map[string]bool{
"exact": false,
"larger": true,
},
Terms: []string{"test"},
LargerThan: 100,
SmallerThan: 1000,
}
if !reflect.DeepEqual(value, want) {
t.Fatalf("\n got: %+v\n want: %+v", value, want)
}
value = ParseSearch("type:audio thisfile")
want = &SearchOptions{
Conditions: map[string]bool{
"exact": false,
"audio": true,
},
Terms: []string{"thisfile"},
}
if !reflect.DeepEqual(value, want) {
t.Fatalf("\n got: %+v\n want: %+v", value, want)
}
}
func BenchmarkSearchAllIndexes(b *testing.B) {
indexes = make(map[string][]string)
// Create mock data
createMockData(50, 3) // 1000 dirs, 3 files per dir
// Generate 100 random search terms
searchTerms := generateRandomSearchTerms(100)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// Execute the SearchAllIndexes function
for _, term := range searchTerms {
SearchAllIndexes(term, "/", "test")
}
}
}
func BenchmarkFillIndex(b *testing.B) {
indexes = make(map[string][]string)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
createMockData(50, 3) // 1000 dirs, 3 files per dir
}
}
func createMockData(numDirs, numFilesPerDir int) {
for i := 0; i < numDirs; i++ {
dirName := generateRandomPath(rand.Intn(3) + 1)
addToIndex("/", dirName, true)
for j := 0; j < numFilesPerDir; j++ {
fileName := "file-" + getRandomTerm() + getRandomExtension()
addToIndex("/"+dirName, fileName, false)
}
}
}
func generateRandomPath(levels int) string {
rand.New(rand.NewSource(time.Now().UnixNano()))
dirName := "srv"
for i := 0; i < levels; i++ {
dirName += "/" + getRandomTerm()
}
return dirName
}
func getRandomTerm() string {
wordbank := []string{
"hi", "test", "other", "name",
"cool", "things", "more", "items",
}
rand.New(rand.NewSource(time.Now().UnixNano()))
index := rand.Intn(len(wordbank))
return wordbank[index]
}
func getRandomExtension() string {
wordbank := []string{
".txt", ".mp3", ".mov", ".doc",
".mp4", ".bak", ".zip", ".jpg",
}
rand.New(rand.NewSource(time.Now().UnixNano()))
index := rand.Intn(len(wordbank))
return wordbank[index]
}
func generateRandomSearchTerms(numTerms int) []string {
// Generate random search terms
searchTerms := make([]string, numTerms)
for i := 0; i < numTerms; i++ {
searchTerms[i] = getRandomTerm()
}
return searchTerms
}
// JSONBytesEqual compares the JSON in two byte slices.
func JSONBytesEqual(a, b []byte) (bool, error) {
var j, j2 interface{}
if err := json.Unmarshal(a, &j); err != nil {
return false, err
}
if err := json.Unmarshal(b, &j2); err != nil {
return false, err
}
return reflect.DeepEqual(j2, j), nil
}

View File

@ -18,7 +18,6 @@ func Initialize(configFile string) {
log.Fatalf("Error unmarshaling YAML data: %v", err)
}
GlobalConfiguration.UserDefaults.Perm = GlobalConfiguration.UserDefaults.Permissions
GlobalConfiguration.Server.Root = "/srv" // hardcoded for now. TODO allow changing
}
func loadConfigFile(configFile string) []byte {

View File

@ -2,6 +2,7 @@ package settings
import (
"log"
"reflect"
"testing"
"github.com/goccy/go-yaml"
@ -50,3 +51,56 @@ func TestConfigLoadSpecificValues(t *testing.T) {
}
}
}
func TestInitialize(t *testing.T) {
type args struct {
configFile string
}
tests := []struct {
name string
args args
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Initialize(tt.args.configFile)
})
}
}
func Test_loadConfigFile(t *testing.T) {
type args struct {
configFile string
}
tests := []struct {
name string
args args
want []byte
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := loadConfigFile(tt.args.configFile); !reflect.DeepEqual(got, tt.want) {
t.Errorf("loadConfigFile() = %v, want %v", got, tt.want)
}
})
}
}
func Test_setDefaults(t *testing.T) {
tests := []struct {
name string
want Settings
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := setDefaults(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("setDefaults() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -0,0 +1,88 @@
package settings
import (
"testing"
"github.com/gtsteffaniak/filebrowser/rules"
)
func TestSettings_MakeUserDir(t *testing.T) {
type fields struct {
Key []byte
Signup bool
CreateUserDir bool
UserHomeBasePath string
Commands map[string][]string
Shell []string
AdminUsername string
AdminPassword string
Rules []rules.Rule
Server Server
Auth Auth
Frontend Frontend
Users []UserDefaults
UserDefaults UserDefaults
}
type args struct {
username string
userScope string
serverRoot string
}
tests := []struct {
name string
fields fields
args args
want string
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Settings{
Key: tt.fields.Key,
Signup: tt.fields.Signup,
CreateUserDir: tt.fields.CreateUserDir,
UserHomeBasePath: tt.fields.UserHomeBasePath,
Commands: tt.fields.Commands,
Shell: tt.fields.Shell,
AdminUsername: tt.fields.AdminUsername,
AdminPassword: tt.fields.AdminPassword,
Rules: tt.fields.Rules,
Server: tt.fields.Server,
Auth: tt.fields.Auth,
Frontend: tt.fields.Frontend,
Users: tt.fields.Users,
UserDefaults: tt.fields.UserDefaults,
}
got, err := s.MakeUserDir(tt.args.username, tt.args.userScope, tt.args.serverRoot)
if (err != nil) != tt.wantErr {
t.Errorf("Settings.MakeUserDir() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Settings.MakeUserDir() = %v, want %v", got, tt.want)
}
})
}
}
func Test_cleanUsername(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want string
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := cleanUsername(tt.args.s); got != tt.want {
t.Errorf("cleanUsername() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -25,7 +25,7 @@ import HeaderBar from "@/components/header/HeaderBar";
import Breadcrumbs from "@/components/Breadcrumbs";
import Errors from "@/views/Errors";
import Preview from "@/views/files/Preview";
import ListingView from "@/views/files/Listing";
import Listing from "@/views/files/Listing";
function clean(path) {
return path.endsWith("/") ? path.slice(0, -1) : path;
@ -38,7 +38,7 @@ export default {
Breadcrumbs,
Errors,
Preview,
ListingView,
Listing,
Editor: () => import("@/views/files/Editor"),
},
data: function () {

View File

@ -26,7 +26,7 @@ import css from "@/utils/css";
import throttle from "lodash.throttle";
export default {
name: "listingView",
name: "listing",
components: {
HeaderBar,
Action,
@ -129,12 +129,12 @@ export default {
// Reset the show value
this.showLimit = 50;
// Ensures that the listingView is displayed
// Ensures that the listing is displayed
Vue.nextTick(() => {
// How much every listingView item affects the window height
// How much every listing item affects the window height
this.setItemWeight();
// Fill and fit the window with listingView items
// Fill and fit the window with listing items
this.fillWindow(true);
});
},
@ -143,10 +143,10 @@ export default {
// Check the columns size for the first time.
this.colunmsResize();
// How much every listingView item affects the window height
// How much every listing item affects the window height
this.setItemWeight();
// Fill and fit the window with listingView items
// Fill and fit the window with listing items
this.fillWindow(true);
// Add the needed event listeners to the window and document.
@ -375,7 +375,7 @@ export default {
let columns = Math.floor(
document.querySelector("main").offsetWidth / this.columnWidth
);
let items = css(["#listingView.mosaic .item", ".mosaic#listingView .item"]);
let items = css(["#listing.mosaic .item", ".mosaic#listing .item"]);
if (columns === 0) columns = 1;
items.style.width = `calc(${100 / columns}% - 1em)`;
},
@ -402,7 +402,7 @@ export default {
this.dragCounter++;
// When the user starts dragging an item, put every
// file on the listingView with 50% opacity.
// file on the listing with 50% opacity.
let items = document.getElementsByClassName("item");
Array.from(items).forEach((file) => {
@ -544,9 +544,9 @@ export default {
this.width = window.innerWidth;
// Listing element is not displayed
if (this.$refs.listingView == null) return;
if (this.$refs.listing == null) return;
// How much every listingView item affects the window height
// How much every listing item affects the window height
this.setItemWeight();
// Fill but not fit the window
@ -598,13 +598,13 @@ export default {
},
setItemWeight() {
// Listing element is not displayed
if (this.$refs.listingView == null) return;
if (this.$refs.listing == null) return;
let itemQuantity = this.req.numDirs + this.req.numFiles;
if (itemQuantity > this.showLimit) itemQuantity = this.showLimit;
// How much every listingView item affects the window height
this.itemWeight = this.$refs.listingView.offsetHeight / itemQuantity;
// How much every listing item affects the window height
this.itemWeight = this.$refs.listing.offsetHeight / itemQuantity;
},
fillWindow(fit = false) {
const totalItems = this.req.numDirs + this.req.numFiles;

View File

@ -37,7 +37,7 @@ import Search from "@/components/Search.vue";
export default {
name: "listingView",
name: "listing",
components: {
HeaderBar,
Action,
@ -138,12 +138,12 @@ export default {
// Reset the show value
this.showLimit = 50;
// Ensures that the listingView is displayed
// Ensures that the listing is displayed
Vue.nextTick(() => {
// How much every listingView item affects the window height
// How much every listing item affects the window height
this.setItemWeight();
// Fill and fit the window with listingView items
// Fill and fit the window with listing items
this.fillWindow(true);
});
},
@ -152,10 +152,10 @@ export default {
// Check the columns size for the first time.
this.colunmsResize();
// How much every listingView item affects the window height
// How much every listing item affects the window height
this.setItemWeight();
// Fill and fit the window with listingView items
// Fill and fit the window with listing items
this.fillWindow(true);
// Add the needed event listeners to the window and document.
@ -384,7 +384,7 @@ export default {
let columns = Math.floor(
document.querySelector("main").offsetWidth / this.columnWidth
);
let items = css(["#listingView.mosaic .item", ".mosaic#listingView .item"]);
let items = css(["#listing.mosaic .item", ".mosaic#listing .item"]);
if (columns === 0) columns = 1;
items.style.width = `calc(${100 / columns}% - 1em)`;
},
@ -411,7 +411,7 @@ export default {
this.dragCounter++;
// When the user starts dragging an item, put every
// file on the listingView with 50% opacity.
// file on the listing with 50% opacity.
let items = document.getElementsByClassName("item");
Array.from(items).forEach((file) => {
@ -553,9 +553,9 @@ export default {
this.width = window.innerWidth;
// Listing element is not displayed
if (this.$refs.listingView == null) return;
if (this.$refs.listing == null) return;
// How much every listingView item affects the window height
// How much every listing item affects the window height
this.setItemWeight();
// Fill but not fit the window
@ -597,13 +597,13 @@ export default {
},
setItemWeight() {
// Listing element is not displayed
if (this.$refs.listingView == null) return;
if (this.$refs.listing == null) return;
let itemQuantity = this.req.numDirs + this.req.numFiles;
if (itemQuantity > this.showLimit) itemQuantity = this.showLimit;
// How much every listingView item affects the window height
this.itemWeight = this.$refs.listingView.offsetHeight / itemQuantity;
// How much every listing item affects the window height
this.itemWeight = this.$refs.listing.offsetHeight / itemQuantity;
},
fillWindow(fit = false) {
const totalItems = this.req.numDirs + this.req.numFiles;

View File

@ -87,7 +87,7 @@
multiple
/>
</div>
<div v-else id="listingView" ref="listingView" :class="user.viewMode + ' file-icons'">
<div v-else id="listing" ref="listing" :class="user.viewMode + ' file-icons'">
<div>
<div class="item header">
<div></div>
@ -213,7 +213,7 @@ import Action from "@/components/header/Action";
import Item from "@/components/files/ListingItem";
export default {
name: "listingView",
name: "listing",
components: {
Action,
Item,
@ -313,12 +313,12 @@ export default {
// Reset the show value
this.showLimit = 50;
// Ensures that the listingView is displayed
// Ensures that the listing is displayed
Vue.nextTick(() => {
// How much every listingView item affects the window height
// How much every listing item affects the window height
this.setItemWeight();
// Fill and fit the window with listingView items
// Fill and fit the window with listing items
this.fillWindow(true);
});
},
@ -327,10 +327,10 @@ export default {
// Check the columns size for the first time.
this.colunmsResize();
// How much every listingView item affects the window height
// How much every listing item affects the window height
this.setItemWeight();
// Fill and fit the window with listingView items
// Fill and fit the window with listing items
this.fillWindow(true);
// Add the needed event listeners to the window and document.
@ -527,7 +527,7 @@ export default {
let columns = Math.floor(
document.querySelector("main").offsetWidth / this.columnWidth
);
let items = css(["#listingView.mosaic .item", ".mosaic#listingView .item"]);
let items = css(["#listing.mosaic .item", ".mosaic#listing .item"]);
if (columns === 0) columns = 1;
items.style.width = `calc(${100 / columns}% - 1em)`;
},
@ -554,7 +554,7 @@ export default {
this.dragCounter++;
// When the user starts dragging an item, put every
// file on the listingView with 50% opacity.
// file on the listing with 50% opacity.
let items = document.getElementsByClassName("item");
Array.from(items).forEach((file) => {
@ -696,9 +696,9 @@ export default {
this.width = window.innerWidth;
// Listing element is not displayed
if (this.$refs.listingView == null) return;
if (this.$refs.listing == null) return;
// How much every listingView item affects the window height
// How much every listing item affects the window height
this.setItemWeight();
// Fill but not fit the window
@ -740,13 +740,13 @@ export default {
},
setItemWeight() {
// Listing element is not displayed
if (this.$refs.listingView == null) return;
if (this.$refs.listing == null) return;
let itemQuantity = this.req.numDirs + this.req.numFiles;
if (itemQuantity > this.showLimit) itemQuantity = this.showLimit;
// How much every listingView item affects the window height
this.itemWeight = this.$refs.listingView.offsetHeight / itemQuantity;
// How much every listing item affects the window height
this.itemWeight = this.$refs.listing.offsetHeight / itemQuantity;
},
fillWindow(fit = false) {
const totalItems = this.req.numDirs + this.req.numFiles;