diff --git a/backend/benchmark_results.txt b/backend/benchmark_results.txt index 5c7697d9..de163b05 100644 --- a/backend/benchmark_results.txt +++ b/backend/benchmark_results.txt @@ -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 -2023/09/24 12:52:05 h: 401 -2023/09/24 12:52:05 h: 401 -2023/09/24 12:52:05 h: 401 -2023/09/24 12:52:05 h: 401 -2023/09/24 12:52:05 h: 401 +2023/09/29 19:48:56 h: 401 +2023/09/29 19:48:56 h: 401 +2023/09/29 19:48:56 h: 401 +2023/09/29 19:48:56 h: 401 +2023/09/29 19:48:56 h: 401 +2023/09/29 19:48:56 h: 401 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] diff --git a/backend/cmd/root.go b/backend/cmd/root.go index a1d3aa48..8120360a 100644 --- a/backend/cmd/root.go +++ b/backend/cmd/root.go @@ -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 diff --git a/backend/filebrowser.yaml b/backend/filebrowser.yaml index 4a36e28f..7e2d4f48 100644 --- a/backend/filebrowser.yaml +++ b/backend/filebrowser.yaml @@ -1,6 +1,7 @@ server: port: 8080 baseURL: "/" + root: "/srv" auth: method: noauth signup: true diff --git a/backend/http/search.go b/backend/http/search.go index 22b6df6b..306506de 100644 --- a/backend/http/search.go +++ b/backend/http/search.go @@ -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, diff --git a/backend/search/conditions.go b/backend/index/conditions.go similarity index 99% rename from backend/search/conditions.go rename to backend/index/conditions.go index b6791433..1a1cff28 100644 --- a/backend/search/conditions.go +++ b/backend/index/conditions.go @@ -1,4 +1,4 @@ -package search +package index import ( "regexp" diff --git a/backend/index/indexing.go b/backend/index/indexing.go new file mode 100644 index 00000000..eea575f1 --- /dev/null +++ b/backend/index/indexing.go @@ -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) + } +} diff --git a/backend/index/indexing_test.go b/backend/index/indexing_test.go new file mode 100644 index 00000000..f1d081e3 --- /dev/null +++ b/backend/index/indexing_test.go @@ -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) + }) + } +} diff --git a/backend/search/indexing.go b/backend/index/search_index.go similarity index 54% rename from backend/search/indexing.go rename to backend/index/search_index.go index 8c06d91a..34f644a4 100644 --- a/backend/search/indexing.go +++ b/backend/index/search_index.go @@ -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] } diff --git a/backend/index/search_index_test.go b/backend/index/search_index_test.go new file mode 100644 index 00000000..1069a264 --- /dev/null +++ b/backend/index/search_index_test.go @@ -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) + } + }) + } +} diff --git a/backend/search/search_test.go b/backend/search/search_test.go deleted file mode 100644 index 18deee27..00000000 --- a/backend/search/search_test.go +++ /dev/null @@ -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 -} diff --git a/backend/settings/config.go b/backend/settings/config.go index 39cd908b..d201ef58 100644 --- a/backend/settings/config.go +++ b/backend/settings/config.go @@ -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 { diff --git a/backend/settings/settings_test.go b/backend/settings/config_test.go similarity index 60% rename from backend/settings/settings_test.go rename to backend/settings/config_test.go index 873ab6b7..fb51566c 100644 --- a/backend/settings/settings_test.go +++ b/backend/settings/config_test.go @@ -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) + } + }) + } +} diff --git a/backend/settings/dir_test.go b/backend/settings/dir_test.go new file mode 100644 index 00000000..1b3abc4e --- /dev/null +++ b/backend/settings/dir_test.go @@ -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) + } + }) + } +} diff --git a/frontend/src/views/Files.vue b/frontend/src/views/Files.vue index 18c5d484..6a5db6f8 100644 --- a/frontend/src/views/Files.vue +++ b/frontend/src/views/Files.vue @@ -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 () { diff --git a/frontend/src/views/bars/Default.vue b/frontend/src/views/bars/Default.vue index 8f54df6b..8e910a06 100644 --- a/frontend/src/views/bars/Default.vue +++ b/frontend/src/views/bars/Default.vue @@ -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; diff --git a/frontend/src/views/bars/ListingBar.vue b/frontend/src/views/bars/ListingBar.vue index 1b6eb62a..88c66350 100644 --- a/frontend/src/views/bars/ListingBar.vue +++ b/frontend/src/views/bars/ListingBar.vue @@ -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; diff --git a/frontend/src/views/files/Listing.vue b/frontend/src/views/files/Listing.vue index 17979b10..3929c1fb 100644 --- a/frontend/src/views/files/Listing.vue +++ b/frontend/src/views/files/Listing.vue @@ -87,7 +87,7 @@ multiple /> -
+
@@ -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;