V0.2.10 (#220)
This commit is contained in:
		
							parent
							
								
									b4b92bf852
								
							
						
					
					
						commit
						1608877813
					
				
							
								
								
									
										17
									
								
								CHANGELOG.md
								
								
								
								
							
							
						
						
									
										17
									
								
								CHANGELOG.md
								
								
								
								
							|  | @ -2,6 +2,23 @@ | ||||||
| 
 | 
 | ||||||
| All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version). | All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version). | ||||||
| 
 | 
 | ||||||
|  | ## v0.2.10 | ||||||
|  | 
 | ||||||
|  |   **New Features**: | ||||||
|  |   - Allows user creation command line arguments https://github.com/gtsteffaniak/filebrowser/issues/196 | ||||||
|  |   - Folder sizes are always shown, leveraging the index. https://github.com/gtsteffaniak/filebrowser/issues/138 | ||||||
|  |   - Searching files based on filesize is no longer slower. | ||||||
|  | 
 | ||||||
|  |   **Bugfixes**: | ||||||
|  |   - fixes file selection usage when in single-click mode https://github.com/gtsteffaniak/filebrowser/issues/214 | ||||||
|  |   - Fixed displayed search context on root directory | ||||||
|  |   - Fixed issue searching "smaller than" actually returned files "larger than" | ||||||
|  | 
 | ||||||
|  |   **Notes**: | ||||||
|  |     - Memory usage from index is reduced by ~40% | ||||||
|  |     - Indexing time has increased 2x due to the extra processing time required to calculate directory sizes. | ||||||
|  |     - File size calcuations use 1024 base vs previous 1000 base (matching windows explorer) | ||||||
|  | 
 | ||||||
| ## v0.2.9 | ## v0.2.9 | ||||||
| 
 | 
 | ||||||
|   This release focused on UI navigation experience. Improving keyboard navigation and adds right click context menu. |   This release focused on UI navigation experience. Improving keyboard navigation and adds right click context menu. | ||||||
|  |  | ||||||
							
								
								
									
										45
									
								
								README.md
								
								
								
								
							
							
						
						
									
										45
									
								
								README.md
								
								
								
								
							|  | @ -6,7 +6,7 @@ | ||||||
| </p> | </p> | ||||||
| <h3 align="center">FileBrowser Quantum - A modern web-based file manager</h3> | <h3 align="center">FileBrowser Quantum - A modern web-based file manager</h3> | ||||||
| <p align="center"> | <p align="center"> | ||||||
|   <img width="800" src="https://github.com/user-attachments/assets/8ba93582-aba2-4996-8ac3-25f763a2e596" title="Main Screenshot"> |   <img width="800" src="https://private-user-images.githubusercontent.com/42989099/367975355-3d6f4619-4985-4ce3-952f-286510dff4f1.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjgxNTA2MjEsIm5iZiI6MTcyODE1MDMyMSwicGF0aCI6Ii80Mjk4OTA5OS8zNjc5NzUzNTUtM2Q2ZjQ2MTktNDk4NS00Y2UzLTk1MmYtMjg2NTEwZGZmNGYxLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEwMDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMDA1VDE3NDUyMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTg1OGNlMWM3M2I1ZmY3MDcxMGU1ODc3N2ZkMjI5YWQ3YzEyODRmNDU0ZDkxMjJhNTU0ZGY1MDQ2YmIwOWRmMTgmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.mOl0Hm70XmQEk-DPzx1FbwrpxNMDAqb-WDprs1HK-mc" title="Main Screenshot"> | ||||||
| </p> | </p> | ||||||
| 
 | 
 | ||||||
| > [!WARNING] | > [!WARNING] | ||||||
|  | @ -18,9 +18,9 @@ | ||||||
| FileBrowser Quantum is a fork of the filebrowser opensource project with the  | FileBrowser Quantum is a fork of the filebrowser opensource project with the  | ||||||
| following changes: | following changes: | ||||||
| 
 | 
 | ||||||
|   1. [x] Enhanced lightning fast indexed search |   1. [x] Efficiently indexed files | ||||||
|      - Real-time results as you type |      - Real-time search results as you type | ||||||
|      - Works with more type filters |      - Search Works with more type filters | ||||||
|      - Enhanced interactive results page. |      - Enhanced interactive results page. | ||||||
|   2. [x] Revamped and simplified GUI navbar and sidebar menu. |   2. [x] Revamped and simplified GUI navbar and sidebar menu. | ||||||
|      - Additional compact view mode as well as refreshed view mode |      - Additional compact view mode as well as refreshed view mode | ||||||
|  | @ -131,39 +131,30 @@ Not using docker (not recommended), download your binary from releases and run w | ||||||
| ./filebrowser -c <filebrowser.yml or other /path/to/config.yaml> | ./filebrowser -c <filebrowser.yml or other /path/to/config.yaml> | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | ## Command Line Usage | ||||||
|  | 
 | ||||||
|  | There are very few commands available. There are 3 actions done via command line: | ||||||
|  | 
 | ||||||
|  | 1. Running the program, as shown on install step. Only argument used is the config file, if you choose to override default "filebrowser.yaml" | ||||||
|  | 2. Checking the version info via `./filebrowser version` | ||||||
|  | 3. Updating the DB, which currently only supports adding users via `./filebrowser set -u username,password [-a] [-s "example/scope"]` | ||||||
|  | 
 | ||||||
| ## Configuration | ## Configuration | ||||||
| 
 | 
 | ||||||
| All configuration is now done via a single configuration file: | All configuration is now done via a single configuration file: | ||||||
| `filebrowser.yaml`, here is an example of minimal [configuration | `filebrowser.yaml`, here is an example of minimal [configuration | ||||||
| file](./backend/filebrowser.yaml). | file](./backend/filebrowser.yaml). | ||||||
| 
 | 
 | ||||||
| View the [Configuration Help Page](./configuration.md) for available | View the [Configuration Help Page](./docs/configuration.md) for available | ||||||
| configuration options and other help. | configuration options and other help. | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| ## Migration from filebrowser/filebrowser | ## Migration from filebrowser/filebrowser | ||||||
| 
 | 
 | ||||||
| If you currently use the original opensource filebrowser  | If you currently use the original filebrowser but want to try using this.  | ||||||
| but want to try using this. I recommend you start fresh without | I recommend you start fresh without reusing the database. If you want to  | ||||||
| reusing the database, but there are a few things you'll need to do if you | migrate your existing database to FileBrowser Quantum, visit the [migration  | ||||||
| must migrate: | readme](./docs/migration.md) | ||||||
| 
 |  | ||||||
| 1. Create a configuration file as mentioned above. |  | ||||||
| 2. Copy your database file from the original filebrowser to the path of |  | ||||||
|    the new one. |  | ||||||
| 3. Update the configuration file to use the database (under server in |  | ||||||
|    filebrowser.yml) |  | ||||||
| 4. If you are using docker, update the docker-compose file or docker run |  | ||||||
|    command to use the config file as described in the install section |  | ||||||
|    above. |  | ||||||
| 5. If you are not using docker, just make sure you run filebrowser -c |  | ||||||
|    filebrowser.yml and have a valid filebrowser config. |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| The filebrowser Quantum application should run with the same user and rules that |  | ||||||
| you have from the original. But keep in mind the differences that are |  | ||||||
| mentioned at the top of this readme. |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| ## Comparison Chart | ## Comparison Chart | ||||||
| 
 | 
 | ||||||
|  | @ -217,4 +208,4 @@ Chromecast support            | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | | ||||||
| 
 | 
 | ||||||
| ## Roadmap | ## Roadmap | ||||||
| 
 | 
 | ||||||
| see [Roadmap Page](./roadmap.md) | see [Roadmap Page](./docs/roadmap.md) | ||||||
|  |  | ||||||
|  | @ -7,26 +7,27 @@ | ||||||
| PASS | PASS | ||||||
| ok  	github.com/gtsteffaniak/filebrowser/diskcache	0.004s | ok  	github.com/gtsteffaniak/filebrowser/diskcache	0.004s | ||||||
| ?   	github.com/gtsteffaniak/filebrowser/errors	[no test files] | ?   	github.com/gtsteffaniak/filebrowser/errors	[no test files] | ||||||
|  | 2024/10/07 12:46:34 could not update unknown type:  unknown | ||||||
| goos: linux | goos: linux | ||||||
| goarch: amd64 | goarch: amd64 | ||||||
| pkg: github.com/gtsteffaniak/filebrowser/files | pkg: github.com/gtsteffaniak/filebrowser/files | ||||||
| cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz | cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz | ||||||
| BenchmarkFillIndex-8          	      10	   3559830 ns/op	  274639 B/op	    2026 allocs/op | BenchmarkFillIndex-8          	      10	   3847878 ns/op	  758424 B/op	    5567 allocs/op | ||||||
| BenchmarkSearchAllIndexes-8   	      10	  31912612 ns/op	20545741 B/op	  312477 allocs/op | BenchmarkSearchAllIndexes-8   	      10	    780431 ns/op	  173444 B/op	    2014 allocs/op | ||||||
| PASS | PASS | ||||||
| ok  	github.com/gtsteffaniak/filebrowser/files	0.417s | ok  	github.com/gtsteffaniak/filebrowser/files	0.073s | ||||||
| PASS | PASS | ||||||
| ok  	github.com/gtsteffaniak/filebrowser/fileutils	0.002s | ok  	github.com/gtsteffaniak/filebrowser/fileutils	0.003s | ||||||
| 2024/08/27 16:16:13 h: 401  <nil> | 2024/10/07 12:46:34 h: 401  <nil> | ||||||
| 2024/08/27 16:16:13 h: 401  <nil> | 2024/10/07 12:46:34 h: 401  <nil> | ||||||
| 2024/08/27 16:16:13 h: 401  <nil> | 2024/10/07 12:46:34 h: 401  <nil> | ||||||
| 2024/08/27 16:16:13 h: 401  <nil> | 2024/10/07 12:46:34 h: 401  <nil> | ||||||
| 2024/08/27 16:16:13 h: 401  <nil> | 2024/10/07 12:46:34 h: 401  <nil> | ||||||
| 2024/08/27 16:16:13 h: 401  <nil> | 2024/10/07 12:46:34 h: 401  <nil> | ||||||
| PASS | PASS | ||||||
| ok  	github.com/gtsteffaniak/filebrowser/http	0.100s | ok  	github.com/gtsteffaniak/filebrowser/http	0.080s | ||||||
| PASS | PASS | ||||||
| ok  	github.com/gtsteffaniak/filebrowser/img	0.124s | ok  	github.com/gtsteffaniak/filebrowser/img	0.137s | ||||||
| PASS | PASS | ||||||
| ok  	github.com/gtsteffaniak/filebrowser/rules	0.002s | ok  	github.com/gtsteffaniak/filebrowser/rules	0.002s | ||||||
| PASS | PASS | ||||||
|  | @ -38,4 +39,5 @@ ok  	github.com/gtsteffaniak/filebrowser/settings	0.004s | ||||||
| ?   	github.com/gtsteffaniak/filebrowser/storage/bolt	[no test files] | ?   	github.com/gtsteffaniak/filebrowser/storage/bolt	[no test files] | ||||||
| PASS | PASS | ||||||
| ok  	github.com/gtsteffaniak/filebrowser/users	0.002s | ok  	github.com/gtsteffaniak/filebrowser/users	0.002s | ||||||
|  | ?   	github.com/gtsteffaniak/filebrowser/utils	[no test files] | ||||||
| ?   	github.com/gtsteffaniak/filebrowser/version	[no test files] | ?   	github.com/gtsteffaniak/filebrowser/version	[no test files] | ||||||
|  |  | ||||||
|  | @ -11,28 +11,28 @@ import ( | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/signal" | 	"os/signal" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  | 	"strings" | ||||||
| 	"syscall" | 	"syscall" | ||||||
| 
 | 
 | ||||||
| 	"embed" | 	"embed" | ||||||
| 
 | 
 | ||||||
| 	"github.com/spf13/pflag" |  | ||||||
| 
 |  | ||||||
| 	"github.com/spf13/cobra" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gtsteffaniak/filebrowser/auth" |  | ||||||
| 	"github.com/gtsteffaniak/filebrowser/diskcache" | 	"github.com/gtsteffaniak/filebrowser/diskcache" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/files" | 	"github.com/gtsteffaniak/filebrowser/files" | ||||||
| 	fbhttp "github.com/gtsteffaniak/filebrowser/http" | 	fbhttp "github.com/gtsteffaniak/filebrowser/http" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/img" | 	"github.com/gtsteffaniak/filebrowser/img" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/settings" | 	"github.com/gtsteffaniak/filebrowser/settings" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/storage" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/users" | 	"github.com/gtsteffaniak/filebrowser/users" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/utils" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/version" | 	"github.com/gtsteffaniak/filebrowser/version" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| //go:embed dist/*
 | //go:embed dist/*
 | ||||||
| var assets embed.FS | var assets embed.FS | ||||||
| 
 | 
 | ||||||
| var nonEmbededFS = os.Getenv("FILEBROWSER_NO_EMBEDED") == "true" | var ( | ||||||
|  | 	nonEmbededFS = os.Getenv("FILEBROWSER_NO_EMBEDED") == "true" | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| type dirFS struct { | type dirFS struct { | ||||||
| 	http.Dir | 	http.Dir | ||||||
|  | @ -42,102 +42,119 @@ func (d dirFS) Open(name string) (fs.File, error) { | ||||||
| 	return d.Dir.Open(name) | 	return d.Dir.Open(name) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func init() { | func getStore(config string) (*storage.Storage, bool) { | ||||||
| 	// Define a flag for the config option (-c or --config)
 | 	// Use the config file (global flag)
 | ||||||
| 	configFlag := pflag.StringP("config", "c", "filebrowser.yaml", "Path to the config file") | 	log.Printf("Using Config file        : %v", config) | ||||||
| 	// Bind the flags to the pflag command line parser
 | 	settings.Initialize(config) | ||||||
| 	pflag.CommandLine.AddGoFlagSet(flag.CommandLine) | 	store, hasDB, err := storage.InitializeDb(settings.Config.Server.Database) | ||||||
| 	pflag.Parse() | 	if err != nil { | ||||||
| 	log.Printf("Initializing FileBrowser Quantum (%v) with config file: %v \n", version.Version, *configFlag) | 		log.Fatal("could not load db info: ", err) | ||||||
| 	log.Println("Embeded Frontend:", !nonEmbededFS) | 	} | ||||||
| 	settings.Initialize(*configFlag) | 	return store, hasDB | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var rootCmd = &cobra.Command{ | func generalUsage() { | ||||||
| 	Use: "filebrowser", | 	fmt.Printf(`usage: ./html-web-crawler <command> [options] --urls <urls> | ||||||
| 	Run: python(func(cmd *cobra.Command, args []string, d pythonData) { |   commands: | ||||||
| 		serverConfig := settings.Config.Server |     collect  Collect data from URLs | ||||||
| 		if !d.hadDB { |     crawl    Crawl URLs and collect data | ||||||
| 			quickSetup(d) |     install  Install chrome browser for javascript enabled scraping. | ||||||
| 		} |                Note: Consider instead to install via native package manager, | ||||||
| 		if serverConfig.NumImageProcessors < 1 { |                      then set "CHROME_EXECUTABLE" in the environment | ||||||
| 			log.Fatal("Image resize workers count could not be < 1") | 	` + "\n") | ||||||
| 		} |  | ||||||
| 		imgSvc := img.New(serverConfig.NumImageProcessors) |  | ||||||
| 
 |  | ||||||
| 		cacheDir := "/tmp" |  | ||||||
| 		var fileCache diskcache.Interface |  | ||||||
| 
 |  | ||||||
| 		// Use file cache if cacheDir is specified
 |  | ||||||
| 		if cacheDir != "" { |  | ||||||
| 			var err error |  | ||||||
| 			fileCache, err = diskcache.NewFileCache(cacheDir) |  | ||||||
| 			if err != nil { |  | ||||||
| 				log.Fatalf("failed to create file cache: %v", err) |  | ||||||
| 			} |  | ||||||
| 		} else { |  | ||||||
| 			// No-op cache if no cacheDir is specified
 |  | ||||||
| 			fileCache = diskcache.NewNoOp() |  | ||||||
| 		} |  | ||||||
| 		// initialize indexing and schedule indexing ever n minutes (default 5)
 |  | ||||||
| 		go files.InitializeIndex(serverConfig.IndexingInterval, serverConfig.Indexing) |  | ||||||
| 		_, err := os.Stat(serverConfig.Root) |  | ||||||
| 		checkErr(fmt.Sprint("cmd os.Stat ", serverConfig.Root), err) |  | ||||||
| 		var listener net.Listener |  | ||||||
| 		address := serverConfig.Address + ":" + strconv.Itoa(serverConfig.Port) |  | ||||||
| 		switch { |  | ||||||
| 		case serverConfig.Socket != "": |  | ||||||
| 			listener, err = net.Listen("unix", serverConfig.Socket) |  | ||||||
| 			checkErr("net.Listen", err) |  | ||||||
| 			socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
 |  | ||||||
| 			checkErr("cmd.Flags().GetUint32", err) |  | ||||||
| 			err = os.Chmod(serverConfig.Socket, os.FileMode(socketPerm)) |  | ||||||
| 			checkErr("os.Chmod", err) |  | ||||||
| 		case serverConfig.TLSKey != "" && serverConfig.TLSCert != "": |  | ||||||
| 			cer, err := tls.LoadX509KeyPair(serverConfig.TLSCert, serverConfig.TLSKey) //nolint:govet
 |  | ||||||
| 			checkErr("tls.LoadX509KeyPair", err) |  | ||||||
| 			listener, err = tls.Listen("tcp", address, &tls.Config{ |  | ||||||
| 				MinVersion:   tls.VersionTLS12, |  | ||||||
| 				Certificates: []tls.Certificate{cer}}, |  | ||||||
| 			) |  | ||||||
| 			checkErr("tls.Listen", err) |  | ||||||
| 		default: |  | ||||||
| 			listener, err = net.Listen("tcp", address) |  | ||||||
| 			checkErr("net.Listen", err) |  | ||||||
| 		} |  | ||||||
| 		sigc := make(chan os.Signal, 1) |  | ||||||
| 		signal.Notify(sigc, os.Interrupt, syscall.SIGTERM) |  | ||||||
| 		go cleanupHandler(listener, sigc) |  | ||||||
| 		if !nonEmbededFS { |  | ||||||
| 			assetsFs, err := fs.Sub(assets, "dist") |  | ||||||
| 			if err != nil { |  | ||||||
| 				log.Fatal("Could not embed frontend. Does backend/cmd/dist exist? Must be built and exist first") |  | ||||||
| 			} |  | ||||||
| 			handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, &serverConfig, assetsFs) |  | ||||||
| 			checkErr("fbhttp.NewHandler", err) |  | ||||||
| 			defer listener.Close() |  | ||||||
| 			log.Println("Listening on", listener.Addr().String()) |  | ||||||
| 			//nolint: gosec
 |  | ||||||
| 			if err := http.Serve(listener, handler); err != nil { |  | ||||||
| 				log.Fatalf("Could not start server on port %d: %v", serverConfig.Port, err) |  | ||||||
| 			} |  | ||||||
| 		} else { |  | ||||||
| 			assetsFs := dirFS{Dir: http.Dir("frontend/dist")} |  | ||||||
| 			handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, &serverConfig, assetsFs) |  | ||||||
| 			checkErr("fbhttp.NewHandler", err) |  | ||||||
| 			defer listener.Close() |  | ||||||
| 			log.Println("Listening on", listener.Addr().String()) |  | ||||||
| 			//nolint: gosec
 |  | ||||||
| 			if err := http.Serve(listener, handler); err != nil { |  | ||||||
| 				log.Fatalf("Could not start server on port %d: %v", serverConfig.Port, err) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 	}, pythonConfig{allowNoDB: true}), |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func StartFilebrowser() { | func StartFilebrowser() { | ||||||
| 	if err := rootCmd.Execute(); err != nil { | 	// Global flags
 | ||||||
|  | 	var configPath string | ||||||
|  | 	var help bool | ||||||
|  | 	// Override the default usage output to use generalUsage()
 | ||||||
|  | 	flag.Usage = generalUsage | ||||||
|  | 	flag.StringVar(&configPath, "c", "filebrowser.yaml", "Path to the config file.") | ||||||
|  | 	flag.BoolVar(&help, "h", false, "Get help about commands") | ||||||
|  | 
 | ||||||
|  | 	// Parse global flags (before subcommands)
 | ||||||
|  | 	flag.Parse() // print generalUsage on error
 | ||||||
|  | 
 | ||||||
|  | 	// Show help if requested
 | ||||||
|  | 	if help { | ||||||
|  | 		generalUsage() | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Create a new FlagSet for the 'set' subcommand
 | ||||||
|  | 	setCmd := flag.NewFlagSet("set", flag.ExitOnError) | ||||||
|  | 	var user, scope, dbConfig string | ||||||
|  | 	var asAdmin bool | ||||||
|  | 
 | ||||||
|  | 	setCmd.StringVar(&user, "u", "", "Comma-separated username and password: \"set -u <username>,<password>\"") | ||||||
|  | 	setCmd.BoolVar(&asAdmin, "a", false, "Create user as admin user, used in combination with -u") | ||||||
|  | 	setCmd.StringVar(&scope, "s", "", "Specify a user scope, otherwise default user config scope is used") | ||||||
|  | 	setCmd.StringVar(&dbConfig, "c", "filebrowser.yaml", "Path to the config file.") | ||||||
|  | 
 | ||||||
|  | 	// Parse subcommand flags only if a subcommand is specified
 | ||||||
|  | 	if len(os.Args) > 1 { | ||||||
|  | 		switch os.Args[1] { | ||||||
|  | 		case "set": | ||||||
|  | 			err := setCmd.Parse(os.Args) | ||||||
|  | 			if err != nil { | ||||||
|  | 				setCmd.PrintDefaults() | ||||||
|  | 				os.Exit(1) | ||||||
|  | 			} | ||||||
|  | 			userInfo := strings.Split(user, ",") | ||||||
|  | 			if len(userInfo) < 2 { | ||||||
|  | 				fmt.Println("not enough info to create user: \"set -u username,password\"") | ||||||
|  | 				setCmd.PrintDefaults() | ||||||
|  | 				os.Exit(1) | ||||||
|  | 			} | ||||||
|  | 			username := userInfo[0] | ||||||
|  | 			password := userInfo[1] | ||||||
|  | 			getStore(dbConfig) | ||||||
|  | 			// Create the user logic
 | ||||||
|  | 			if asAdmin { | ||||||
|  | 				log.Printf("Creating user as admin: %s\n", username) | ||||||
|  | 			} else { | ||||||
|  | 				log.Printf("Creating user: %s\n", username) | ||||||
|  | 			} | ||||||
|  | 			newUser := users.User{ | ||||||
|  | 				Username: username, | ||||||
|  | 				Password: password, | ||||||
|  | 			} | ||||||
|  | 			if scope != "" { | ||||||
|  | 				newUser.Scope = scope | ||||||
|  | 			} | ||||||
|  | 			err = storage.CreateUser(newUser, asAdmin) | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Fatal("Could not create user: ", err) | ||||||
|  | 			} | ||||||
|  | 			return | ||||||
|  | 		case "version": | ||||||
|  | 			fmt.Println("FileBrowser Quantum - A modern web-based file manager") | ||||||
|  | 			fmt.Printf("Version        : %v\n", version.Version) | ||||||
|  | 			fmt.Printf("Commit         : %v\n", version.CommitSHA) | ||||||
|  | 			fmt.Printf("Release Info   : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v\n", version.Version) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	store, dbExists := getStore(configPath) | ||||||
|  | 	indexingInterval := fmt.Sprint(settings.Config.Server.IndexingInterval, " minutes") | ||||||
|  | 	if !settings.Config.Server.Indexing { | ||||||
|  | 		indexingInterval = "disabled" | ||||||
|  | 	} | ||||||
|  | 	database := fmt.Sprintf("Using existing database  : %v", settings.Config.Server.Database) | ||||||
|  | 	if !dbExists { | ||||||
|  | 		database = fmt.Sprintf("Creating new database    : %v", settings.Config.Server.Database) | ||||||
|  | 	} | ||||||
|  | 	log.Printf("Initializing FileBrowser Quantum (%v)\n", version.Version) | ||||||
|  | 	log.Println("Embeded frontend         :", !nonEmbededFS) | ||||||
|  | 	log.Println(database) | ||||||
|  | 	log.Println("Sources                  :", settings.Config.Server.Root) | ||||||
|  | 	log.Print("Indexing interval        : ", indexingInterval) | ||||||
|  | 
 | ||||||
|  | 	serverConfig := settings.Config.Server | ||||||
|  | 	// initialize indexing and schedule indexing ever n minutes (default 5)
 | ||||||
|  | 	go files.InitializeIndex(serverConfig.IndexingInterval, serverConfig.Indexing) | ||||||
|  | 	if err := rootCMD(store, &serverConfig); err != nil { | ||||||
| 		log.Fatal("Error starting filebrowser:", err) | 		log.Fatal("Error starting filebrowser:", err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -149,37 +166,77 @@ func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfac | ||||||
| 	os.Exit(0) | 	os.Exit(0) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func quickSetup(d pythonData) { | func rootCMD(store *storage.Storage, serverConfig *settings.Server) error { | ||||||
| 	settings.Config.Auth.Key = generateKey() | 	if serverConfig.NumImageProcessors < 1 { | ||||||
| 	if settings.Config.Auth.Method == "noauth" { | 		log.Fatal("Image resize workers count could not be < 1") | ||||||
| 		err := d.store.Auth.Save(&auth.NoAuth{}) | 	} | ||||||
| 		checkErr("d.store.Auth.Save", err) | 	imgSvc := img.New(serverConfig.NumImageProcessors) | ||||||
|  | 
 | ||||||
|  | 	cacheDir := "/tmp" | ||||||
|  | 	var fileCache diskcache.Interface | ||||||
|  | 
 | ||||||
|  | 	// Use file cache if cacheDir is specified
 | ||||||
|  | 	if cacheDir != "" { | ||||||
|  | 		var err error | ||||||
|  | 		fileCache, err = diskcache.NewFileCache(cacheDir) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Fatalf("failed to create file cache: %v", err) | ||||||
|  | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		settings.Config.Auth.Method = "password" | 		// No-op cache if no cacheDir is specified
 | ||||||
| 		err := d.store.Auth.Save(&auth.JSONAuth{}) | 		fileCache = diskcache.NewNoOp() | ||||||
| 		checkErr("d.store.Auth.Save", err) |  | ||||||
| 	} | 	} | ||||||
| 	err := d.store.Settings.Save(&settings.Config) | 
 | ||||||
| 	checkErr("d.store.Settings.Save", err) | 	fbhttp.SetupEnv(store, serverConfig, fileCache) | ||||||
| 	err = d.store.Settings.SaveServer(&settings.Config.Server) | 
 | ||||||
| 	checkErr("d.store.Settings.SaveServer", err) | 	_, err := os.Stat(serverConfig.Root) | ||||||
| 	user := users.ApplyDefaults(users.User{}) | 	utils.CheckErr(fmt.Sprint("cmd os.Stat ", serverConfig.Root), err) | ||||||
| 	user.Username = settings.Config.Auth.AdminUsername | 	var listener net.Listener | ||||||
| 	user.Password = settings.Config.Auth.AdminPassword | 	address := serverConfig.Address + ":" + strconv.Itoa(serverConfig.Port) | ||||||
| 	user.Perm.Admin = true | 	switch { | ||||||
| 	user.Scope = "./" | 	case serverConfig.Socket != "": | ||||||
| 	user.DarkMode = true | 		listener, err = net.Listen("unix", serverConfig.Socket) | ||||||
| 	user.ViewMode = "normal" | 		utils.CheckErr("net.Listen", err) | ||||||
| 	user.LockPassword = false | 		err = os.Chmod(serverConfig.Socket, os.FileMode(0666)) // socket-perm
 | ||||||
| 	user.Perm = settings.Permissions{ | 		utils.CheckErr("os.Chmod", err) | ||||||
| 		Create:   true, | 	case serverConfig.TLSKey != "" && serverConfig.TLSCert != "": | ||||||
| 		Rename:   true, | 		cer, err := tls.LoadX509KeyPair(serverConfig.TLSCert, serverConfig.TLSKey) //nolint:govet
 | ||||||
| 		Modify:   true, | 		utils.CheckErr("tls.LoadX509KeyPair", err) | ||||||
| 		Delete:   true, | 		listener, err = tls.Listen("tcp", address, &tls.Config{ | ||||||
| 		Share:    true, | 			MinVersion:   tls.VersionTLS12, | ||||||
| 		Download: true, | 			Certificates: []tls.Certificate{cer}}, | ||||||
| 		Admin:    true, | 		) | ||||||
|  | 		utils.CheckErr("tls.Listen", err) | ||||||
|  | 	default: | ||||||
|  | 		listener, err = net.Listen("tcp", address) | ||||||
|  | 		utils.CheckErr("net.Listen", err) | ||||||
| 	} | 	} | ||||||
| 	err = d.store.Users.Save(&user) | 	sigc := make(chan os.Signal, 1) | ||||||
| 	checkErr("d.store.Users.Save", err) | 	signal.Notify(sigc, os.Interrupt, syscall.SIGTERM) | ||||||
|  | 	go cleanupHandler(listener, sigc) | ||||||
|  | 	if !nonEmbededFS { | ||||||
|  | 		assetsFs, err := fs.Sub(assets, "dist") | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Fatal("Could not embed frontend. Does backend/cmd/dist exist? Must be built and exist first") | ||||||
|  | 		} | ||||||
|  | 		handler, err := fbhttp.NewHandler(imgSvc, assetsFs) | ||||||
|  | 		utils.CheckErr("fbhttp.NewHandler", err) | ||||||
|  | 		defer listener.Close() | ||||||
|  | 		log.Println("Listening on", listener.Addr().String()) | ||||||
|  | 		//nolint: gosec
 | ||||||
|  | 		if err := http.Serve(listener, handler); err != nil { | ||||||
|  | 			log.Fatalf("Could not start server on port %d: %v", serverConfig.Port, err) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		assetsFs := dirFS{Dir: http.Dir("frontend/dist")} | ||||||
|  | 		handler, err := fbhttp.NewHandler(imgSvc, assetsFs) | ||||||
|  | 		utils.CheckErr("fbhttp.NewHandler", err) | ||||||
|  | 		defer listener.Close() | ||||||
|  | 		log.Println("Listening on", listener.Addr().String()) | ||||||
|  | 		//nolint: gosec
 | ||||||
|  | 		if err := http.Serve(listener, handler); err != nil { | ||||||
|  | 			log.Fatalf("Could not start server on port %d: %v", serverConfig.Port, err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -6,7 +6,9 @@ import ( | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gtsteffaniak/filebrowser/settings" | 	"github.com/gtsteffaniak/filebrowser/settings" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/storage" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/users" | 	"github.com/gtsteffaniak/filebrowser/users" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/utils" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
|  | @ -40,27 +42,27 @@ including 'index_end'.`, | ||||||
| 
 | 
 | ||||||
| 		return nil | 		return nil | ||||||
| 	}, | 	}, | ||||||
| 	Run: python(func(cmd *cobra.Command, args []string, d pythonData) { | 	Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) { | ||||||
| 		i, err := strconv.Atoi(args[0]) | 		i, err := strconv.Atoi(args[0]) | ||||||
| 		checkErr("strconv.Atoi", err) | 		utils.CheckErr("strconv.Atoi", err) | ||||||
| 		f := i | 		f := i | ||||||
| 		if len(args) == 2 { //nolint:gomnd
 | 		if len(args) == 2 { //nolint:gomnd
 | ||||||
| 			f, err = strconv.Atoi(args[1]) | 			f, err = strconv.Atoi(args[1]) | ||||||
| 			checkErr("strconv.Atoi", err) | 			utils.CheckErr("strconv.Atoi", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		user := func(u *users.User) { | 		user := func(u *users.User) { | ||||||
| 			u.Rules = append(u.Rules[:i], u.Rules[f+1:]...) | 			u.Rules = append(u.Rules[:i], u.Rules[f+1:]...) | ||||||
| 			err := d.store.Users.Save(u) | 			err := store.Users.Save(u) | ||||||
| 			checkErr("d.store.Users.Save", err) | 			utils.CheckErr("store.Users.Save", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		global := func(s *settings.Settings) { | 		global := func(s *settings.Settings) { | ||||||
| 			s.Rules = append(s.Rules[:i], s.Rules[f+1:]...) | 			s.Rules = append(s.Rules[:i], s.Rules[f+1:]...) | ||||||
| 			err := d.store.Settings.Save(s) | 			err := store.Settings.Save(s) | ||||||
| 			checkErr("d.store.Settings.Save", err) | 			utils.CheckErr("store.Settings.Save", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		runRules(d.store, cmd, user, global) | 		runRules(store, cmd, user, global) | ||||||
| 	}, pythonConfig{}), | 	}), | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -10,10 +10,10 @@ import ( | ||||||
| 	"github.com/gtsteffaniak/filebrowser/settings" | 	"github.com/gtsteffaniak/filebrowser/settings" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/storage" | 	"github.com/gtsteffaniak/filebrowser/storage" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/users" | 	"github.com/gtsteffaniak/filebrowser/users" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/utils" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
| 	rootCmd.AddCommand(rulesCmd) |  | ||||||
| 	rulesCmd.PersistentFlags().StringP("username", "u", "", "username of user to which the rules apply") | 	rulesCmd.PersistentFlags().StringP("username", "u", "", "username of user to which the rules apply") | ||||||
| 	rulesCmd.PersistentFlags().UintP("id", "i", 0, "id of user to which the rules apply") | 	rulesCmd.PersistentFlags().UintP("id", "i", 0, "id of user to which the rules apply") | ||||||
| } | } | ||||||
|  | @ -33,7 +33,7 @@ func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User) | ||||||
| 	id := getUserIdentifier(cmd.Flags()) | 	id := getUserIdentifier(cmd.Flags()) | ||||||
| 	if id != nil { | 	if id != nil { | ||||||
| 		user, err := st.Users.Get("", id) | 		user, err := st.Users.Get("", id) | ||||||
| 		checkErr("st.Users.Get", err) | 		utils.CheckErr("st.Users.Get", err) | ||||||
| 
 | 
 | ||||||
| 		if usersFn != nil { | 		if usersFn != nil { | ||||||
| 			usersFn(user) | 			usersFn(user) | ||||||
|  | @ -44,7 +44,7 @@ func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	s, err := st.Settings.Get() | 	s, err := st.Settings.Get() | ||||||
| 	checkErr("st.Settings.Get", err) | 	utils.CheckErr("st.Settings.Get", err) | ||||||
| 
 | 
 | ||||||
| 	if globalFn != nil { | 	if globalFn != nil { | ||||||
| 		globalFn(s) | 		globalFn(s) | ||||||
|  |  | ||||||
|  | @ -7,7 +7,9 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/gtsteffaniak/filebrowser/rules" | 	"github.com/gtsteffaniak/filebrowser/rules" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/settings" | 	"github.com/gtsteffaniak/filebrowser/settings" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/storage" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/users" | 	"github.com/gtsteffaniak/filebrowser/users" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/utils" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
|  | @ -21,7 +23,7 @@ var rulesAddCmd = &cobra.Command{ | ||||||
| 	Short: "Add a global rule or user rule", | 	Short: "Add a global rule or user rule", | ||||||
| 	Long:  `Add a global rule or user rule.`, | 	Long:  `Add a global rule or user rule.`, | ||||||
| 	Args:  cobra.ExactArgs(1), | 	Args:  cobra.ExactArgs(1), | ||||||
| 	Run: python(func(cmd *cobra.Command, args []string, d pythonData) { | 	Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) { | ||||||
| 		allow := mustGetBool(cmd.Flags(), "allow") | 		allow := mustGetBool(cmd.Flags(), "allow") | ||||||
| 		regex := mustGetBool(cmd.Flags(), "regex") | 		regex := mustGetBool(cmd.Flags(), "regex") | ||||||
| 		exp := args[0] | 		exp := args[0] | ||||||
|  | @ -43,16 +45,16 @@ var rulesAddCmd = &cobra.Command{ | ||||||
| 
 | 
 | ||||||
| 		user := func(u *users.User) { | 		user := func(u *users.User) { | ||||||
| 			u.Rules = append(u.Rules, rule) | 			u.Rules = append(u.Rules, rule) | ||||||
| 			err := d.store.Users.Save(u) | 			err := store.Users.Save(u) | ||||||
| 			checkErr("d.store.Users.Save", err) | 			utils.CheckErr("store.Users.Save", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		global := func(s *settings.Settings) { | 		global := func(s *settings.Settings) { | ||||||
| 			s.Rules = append(s.Rules, rule) | 			s.Rules = append(s.Rules, rule) | ||||||
| 			err := d.store.Settings.Save(s) | 			err := store.Settings.Save(s) | ||||||
| 			checkErr("d.store.Settings.Save", err) | 			utils.CheckErr("store.Settings.Save", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		runRules(d.store, cmd, user, global) | 		runRules(store, cmd, user, global) | ||||||
| 	}, pythonConfig{}), | 	}), | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| package cmd | package cmd | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/storage" | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -13,7 +14,7 @@ var rulesLsCommand = &cobra.Command{ | ||||||
| 	Short: "List global rules or user specific rules", | 	Short: "List global rules or user specific rules", | ||||||
| 	Long:  `List global rules or user specific rules.`, | 	Long:  `List global rules or user specific rules.`, | ||||||
| 	Args:  cobra.NoArgs, | 	Args:  cobra.NoArgs, | ||||||
| 	Run: python(func(cmd *cobra.Command, args []string, d pythonData) { | 	Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) { | ||||||
| 		runRules(d.store, cmd, nil, nil) | 		runRules(store, cmd, nil, nil) | ||||||
| 	}, pythonConfig{}), | 	}), | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -11,10 +11,6 @@ import ( | ||||||
| 	"github.com/gtsteffaniak/filebrowser/users" | 	"github.com/gtsteffaniak/filebrowser/users" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { |  | ||||||
| 	rootCmd.AddCommand(usersCmd) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var usersCmd = &cobra.Command{ | var usersCmd = &cobra.Command{ | ||||||
| 	Use:   "users", | 	Use:   "users", | ||||||
| 	Short: "Users management utility", | 	Short: "Users management utility", | ||||||
|  |  | ||||||
|  | @ -3,7 +3,9 @@ package cmd | ||||||
| import ( | import ( | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/storage" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/users" | 	"github.com/gtsteffaniak/filebrowser/users" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/utils" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
|  | @ -15,26 +17,26 @@ var usersAddCmd = &cobra.Command{ | ||||||
| 	Short: "Create a new user", | 	Short: "Create a new user", | ||||||
| 	Long:  `Create a new user and add it to the database.`, | 	Long:  `Create a new user and add it to the database.`, | ||||||
| 	Args:  cobra.ExactArgs(2), //nolint:gomnd
 | 	Args:  cobra.ExactArgs(2), //nolint:gomnd
 | ||||||
| 	Run: python(func(cmd *cobra.Command, args []string, d pythonData) { | 	Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) { | ||||||
| 		user := &users.User{ | 		user := &users.User{ | ||||||
| 			Username:     args[0], | 			Username:     args[0], | ||||||
| 			Password:     args[1], | 			Password:     args[1], | ||||||
| 			LockPassword: mustGetBool(cmd.Flags(), "lockPassword"), | 			LockPassword: mustGetBool(cmd.Flags(), "lockPassword"), | ||||||
| 		} | 		} | ||||||
| 		servSettings, err := d.store.Settings.GetServer() | 		servSettings, err := store.Settings.GetServer() | ||||||
| 		checkErr("d.store.Settings.GetServer()", err) | 		utils.CheckErr("store.Settings.GetServer()", err) | ||||||
| 		// since getUserDefaults() polluted s.Defaults.Scope
 | 		// since getUserDefaults() polluted s.Defaults.Scope
 | ||||||
| 		// which makes the Scope not the one saved in the db
 | 		// which makes the Scope not the one saved in the db
 | ||||||
| 		// we need the right s.Defaults.Scope here
 | 		// we need the right s.Defaults.Scope here
 | ||||||
| 		s2, err := d.store.Settings.Get() | 		s2, err := store.Settings.Get() | ||||||
| 		checkErr("d.store.Settings.Get()", err) | 		utils.CheckErr("store.Settings.Get()", err) | ||||||
| 
 | 
 | ||||||
| 		userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root) | 		userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root) | ||||||
| 		checkErr("s2.MakeUserDir", err) | 		utils.CheckErr("s2.MakeUserDir", err) | ||||||
| 		user.Scope = userHome | 		user.Scope = userHome | ||||||
| 
 | 
 | ||||||
| 		err = d.store.Users.Save(user) | 		err = store.Users.Save(user) | ||||||
| 		checkErr("d.store.Users.Save", err) | 		utils.CheckErr("store.Users.Save", err) | ||||||
| 		printUsers([]*users.User{user}) | 		printUsers([]*users.User{user}) | ||||||
| 	}, pythonConfig{}), | 	}), | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| package cmd | package cmd | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/storage" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/utils" | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -14,11 +16,11 @@ var usersExportCmd = &cobra.Command{ | ||||||
| 	Long: `Export all users to a json or yaml file. Please indicate the | 	Long: `Export all users to a json or yaml file. Please indicate the | ||||||
| path to the file where you want to write the users.`, | path to the file where you want to write the users.`, | ||||||
| 	Args: jsonYamlArg, | 	Args: jsonYamlArg, | ||||||
| 	Run: python(func(cmd *cobra.Command, args []string, d pythonData) { | 	Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) { | ||||||
| 		list, err := d.store.Users.Gets("") | 		list, err := store.Users.Gets("") | ||||||
| 		checkErr("d.store.Users.Gets", err) | 		utils.CheckErr("store.Users.Gets", err) | ||||||
| 
 | 
 | ||||||
| 		err = marshal(args[0], list) | 		err = marshal(args[0], list) | ||||||
| 		checkErr("marshal", err) | 		utils.CheckErr("marshal", err) | ||||||
| 	}, pythonConfig{}), | 	}), | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,7 +3,9 @@ package cmd | ||||||
| import ( | import ( | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/storage" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/users" | 	"github.com/gtsteffaniak/filebrowser/users" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/utils" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
|  | @ -26,7 +28,7 @@ var usersLsCmd = &cobra.Command{ | ||||||
| 	Run:   findUsers, | 	Run:   findUsers, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) { | var findUsers = cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) { | ||||||
| 	var ( | 	var ( | ||||||
| 		list []*users.User | 		list []*users.User | ||||||
| 		user *users.User | 		user *users.User | ||||||
|  | @ -36,16 +38,16 @@ var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) { | ||||||
| 	if len(args) == 1 { | 	if len(args) == 1 { | ||||||
| 		username, id := parseUsernameOrID(args[0]) | 		username, id := parseUsernameOrID(args[0]) | ||||||
| 		if username != "" { | 		if username != "" { | ||||||
| 			user, err = d.store.Users.Get("", username) | 			user, err = store.Users.Get("", username) | ||||||
| 		} else { | 		} else { | ||||||
| 			user, err = d.store.Users.Get("", id) | 			user, err = store.Users.Get("", id) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		list = []*users.User{user} | 		list = []*users.User{user} | ||||||
| 	} else { | 	} else { | ||||||
| 		list, err = d.store.Users.Gets("") | 		list, err = store.Users.Gets("") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	checkErr("findUsers", err) | 	utils.CheckErr("findUsers", err) | ||||||
| 	printUsers(list) | 	printUsers(list) | ||||||
| }, pythonConfig{}) | }) | ||||||
|  |  | ||||||
|  | @ -8,7 +8,9 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/storage" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/users" | 	"github.com/gtsteffaniak/filebrowser/users" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/utils" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
|  | @ -25,47 +27,47 @@ file. You can use this command to import new users to your | ||||||
| installation. For that, just don't place their ID on the files | installation. For that, just don't place their ID on the files | ||||||
| list or set it to 0.`, | list or set it to 0.`, | ||||||
| 	Args: jsonYamlArg, | 	Args: jsonYamlArg, | ||||||
| 	Run: python(func(cmd *cobra.Command, args []string, d pythonData) { | 	Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) { | ||||||
| 		fd, err := os.Open(args[0]) | 		fd, err := os.Open(args[0]) | ||||||
| 		checkErr("os.Open", err) | 		utils.CheckErr("os.Open", err) | ||||||
| 		defer fd.Close() | 		defer fd.Close() | ||||||
| 
 | 
 | ||||||
| 		list := []*users.User{} | 		list := []*users.User{} | ||||||
| 		err = unmarshal(args[0], &list) | 		err = unmarshal(args[0], &list) | ||||||
| 		checkErr("unmarshal", err) | 		utils.CheckErr("unmarshal", err) | ||||||
| 
 | 
 | ||||||
| 		if mustGetBool(cmd.Flags(), "replace") { | 		if mustGetBool(cmd.Flags(), "replace") { | ||||||
| 			oldUsers, err := d.store.Users.Gets("") | 			oldUsers, err := store.Users.Gets("") | ||||||
| 			checkErr("d.store.Users.Gets", err) | 			utils.CheckErr("store.Users.Gets", err) | ||||||
| 
 | 
 | ||||||
| 			err = marshal("users.backup.json", list) | 			err = marshal("users.backup.json", list) | ||||||
| 			checkErr("marshal users.backup.json", err) | 			utils.CheckErr("marshal users.backup.json", err) | ||||||
| 
 | 
 | ||||||
| 			for _, user := range oldUsers { | 			for _, user := range oldUsers { | ||||||
| 				err = d.store.Users.Delete(user.ID) | 				err = store.Users.Delete(user.ID) | ||||||
| 				checkErr("d.store.Users.Delete", err) | 				utils.CheckErr("store.Users.Delete", err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		overwrite := mustGetBool(cmd.Flags(), "overwrite") | 		overwrite := mustGetBool(cmd.Flags(), "overwrite") | ||||||
| 
 | 
 | ||||||
| 		for _, user := range list { | 		for _, user := range list { | ||||||
| 			onDB, err := d.store.Users.Get("", user.ID) | 			onDB, err := store.Users.Get("", user.ID) | ||||||
| 
 | 
 | ||||||
| 			// User exists in DB.
 | 			// User exists in DB.
 | ||||||
| 			if err == nil { | 			if err == nil { | ||||||
| 				if !overwrite { | 				if !overwrite { | ||||||
| 					newErr := errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered") | 					newErr := errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered") | ||||||
| 					checkErr("", newErr) | 					utils.CheckErr("", newErr) | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				// If the usernames mismatch, check if there is another one in the DB
 | 				// If the usernames mismatch, check if there is another one in the DB
 | ||||||
| 				// with the new username. If there is, print an error and cancel the
 | 				// with the new username. If there is, print an error and cancel the
 | ||||||
| 				// operation
 | 				// operation
 | ||||||
| 				if user.Username != onDB.Username { | 				if user.Username != onDB.Username { | ||||||
| 					if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
 | 					if conflictuous, err := store.Users.Get("", user.Username); err == nil { //nolint:govet
 | ||||||
| 						newErr := usernameConflictError(user.Username, conflictuous.ID, user.ID) | 						newErr := usernameConflictError(user.Username, conflictuous.ID, user.ID) | ||||||
| 						checkErr("usernameConflictError", newErr) | 						utils.CheckErr("usernameConflictError", newErr) | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 			} else { | 			} else { | ||||||
|  | @ -74,10 +76,10 @@ list or set it to 0.`, | ||||||
| 				user.ID = 0 | 				user.ID = 0 | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			err = d.store.Users.Save(user) | 			err = store.Users.Save(user) | ||||||
| 			checkErr("d.store.Users.Save", err) | 			utils.CheckErr("store.Users.Save", err) | ||||||
| 		} | 		} | ||||||
| 	}, pythonConfig{}), | 	}), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func usernameConflictError(username string, originalID, newID uint) error { | func usernameConflictError(username string, originalID, newID uint) error { | ||||||
|  |  | ||||||
|  | @ -3,6 +3,8 @@ package cmd | ||||||
| import ( | import ( | ||||||
| 	"log" | 	"log" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/storage" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/utils" | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -15,17 +17,17 @@ var usersRmCmd = &cobra.Command{ | ||||||
| 	Short: "Delete a user by username or id", | 	Short: "Delete a user by username or id", | ||||||
| 	Long:  `Delete a user by username or id`, | 	Long:  `Delete a user by username or id`, | ||||||
| 	Args:  cobra.ExactArgs(1), | 	Args:  cobra.ExactArgs(1), | ||||||
| 	Run: python(func(cmd *cobra.Command, args []string, d pythonData) { | 	Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) { | ||||||
| 		username, id := parseUsernameOrID(args[0]) | 		username, id := parseUsernameOrID(args[0]) | ||||||
| 		var err error | 		var err error | ||||||
| 
 | 
 | ||||||
| 		if username != "" { | 		if username != "" { | ||||||
| 			err = d.store.Users.Delete(username) | 			err = store.Users.Delete(username) | ||||||
| 		} else { | 		} else { | ||||||
| 			err = d.store.Users.Delete(id) | 			err = store.Users.Delete(id) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		checkErr("usersRmCmd", err) | 		utils.CheckErr("usersRmCmd", err) | ||||||
| 		log.Println("user deleted successfully") | 		log.Println("user deleted successfully") | ||||||
| 	}, pythonConfig{}), | 	}), | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,7 +3,9 @@ package cmd | ||||||
| import ( | import ( | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/storage" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/users" | 	"github.com/gtsteffaniak/filebrowser/users" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/utils" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
|  | @ -16,7 +18,7 @@ var usersUpdateCmd = &cobra.Command{ | ||||||
| 	Long: `Updates an existing user. Set the flags for the | 	Long: `Updates an existing user. Set the flags for the | ||||||
| options you want to change.`, | options you want to change.`, | ||||||
| 	Args: cobra.ExactArgs(1), | 	Args: cobra.ExactArgs(1), | ||||||
| 	Run: python(func(cmd *cobra.Command, args []string, d pythonData) { | 	Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) { | ||||||
| 		username, id := parseUsernameOrID(args[0]) | 		username, id := parseUsernameOrID(args[0]) | ||||||
| 
 | 
 | ||||||
| 		var ( | 		var ( | ||||||
|  | @ -25,14 +27,14 @@ options you want to change.`, | ||||||
| 		) | 		) | ||||||
| 
 | 
 | ||||||
| 		if id != 0 { | 		if id != 0 { | ||||||
| 			user, err = d.store.Users.Get("", id) | 			user, err = store.Users.Get("", id) | ||||||
| 		} else { | 		} else { | ||||||
| 			user, err = d.store.Users.Get("", username) | 			user, err = store.Users.Get("", username) | ||||||
| 		} | 		} | ||||||
| 		checkErr("d.store.Users.Get", err) | 		utils.CheckErr("store.Users.Get", err) | ||||||
| 
 | 
 | ||||||
| 		err = d.store.Users.Update(user) | 		err = store.Users.Update(user) | ||||||
| 		checkErr("d.store.Users.Update", err) | 		utils.CheckErr("store.Users.Update", err) | ||||||
| 		printUsers([]*users.User{user}) | 		printUsers([]*users.User{user}) | ||||||
| 	}, pythonConfig{}), | 	}), | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,113 +3,42 @@ package cmd | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" |  | ||||||
| 	"log" |  | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 
 | 
 | ||||||
| 	"github.com/asdine/storm/v3" |  | ||||||
| 	"github.com/goccy/go-yaml" | 	"github.com/goccy/go-yaml" | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| 	"github.com/spf13/pflag" | 	"github.com/spf13/pflag" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gtsteffaniak/filebrowser/settings" |  | ||||||
| 	"github.com/gtsteffaniak/filebrowser/storage" | 	"github.com/gtsteffaniak/filebrowser/storage" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/storage/bolt" | 	"github.com/gtsteffaniak/filebrowser/utils" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func checkErr(source string, err error) { |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Fatalf("%s: %v", source, err) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func mustGetString(flags *pflag.FlagSet, flag string) string { | func mustGetString(flags *pflag.FlagSet, flag string) string { | ||||||
| 	s, err := flags.GetString(flag) | 	s, err := flags.GetString(flag) | ||||||
| 	checkErr("mustGetString", err) | 	utils.CheckErr("mustGetString", err) | ||||||
| 	return s | 	return s | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func mustGetBool(flags *pflag.FlagSet, flag string) bool { | func mustGetBool(flags *pflag.FlagSet, flag string) bool { | ||||||
| 	b, err := flags.GetBool(flag) | 	b, err := flags.GetBool(flag) | ||||||
| 	checkErr("mustGetBool", err) | 	utils.CheckErr("mustGetBool", err) | ||||||
| 	return b | 	return b | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func mustGetUint(flags *pflag.FlagSet, flag string) uint { | func mustGetUint(flags *pflag.FlagSet, flag string) uint { | ||||||
| 	b, err := flags.GetUint(flag) | 	b, err := flags.GetUint(flag) | ||||||
| 	checkErr("mustGetUint", err) | 	utils.CheckErr("mustGetUint", err) | ||||||
| 	return b | 	return b | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func generateKey() []byte { |  | ||||||
| 	k, err := settings.GenerateKey() |  | ||||||
| 	checkErr("generateKey", err) |  | ||||||
| 	return k |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type cobraFunc func(cmd *cobra.Command, args []string) | type cobraFunc func(cmd *cobra.Command, args []string) | ||||||
| type pythonFunc func(cmd *cobra.Command, args []string, data pythonData) | type pythonFunc func(cmd *cobra.Command, args []string, store *storage.Storage) | ||||||
| 
 |  | ||||||
| type pythonConfig struct { |  | ||||||
| 	noDB      bool |  | ||||||
| 	allowNoDB bool |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type pythonData struct { |  | ||||||
| 	hadDB bool |  | ||||||
| 	store *storage.Storage |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func dbExists(path string) (bool, error) { |  | ||||||
| 	stat, err := os.Stat(path) |  | ||||||
| 	if err == nil { |  | ||||||
| 		return stat.Size() != 0, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if os.IsNotExist(err) { |  | ||||||
| 		d := filepath.Dir(path) |  | ||||||
| 		_, err = os.Stat(d) |  | ||||||
| 		if os.IsNotExist(err) { |  | ||||||
| 			if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet,gomnd
 |  | ||||||
| 				return false, err |  | ||||||
| 			} |  | ||||||
| 			return false, nil |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return false, err |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func python(fn pythonFunc, cfg pythonConfig) cobraFunc { |  | ||||||
| 	return func(cmd *cobra.Command, args []string) { |  | ||||||
| 		data := pythonData{hadDB: true} |  | ||||||
| 		path := settings.Config.Server.Database |  | ||||||
| 		exists, err := dbExists(path) |  | ||||||
| 
 |  | ||||||
| 		if err != nil { |  | ||||||
| 			panic(err) |  | ||||||
| 		} else if exists && cfg.noDB { |  | ||||||
| 			log.Fatal(path + " already exists") |  | ||||||
| 		} else if !exists && !cfg.noDB && !cfg.allowNoDB { |  | ||||||
| 			log.Fatal(path + " does not exist. Please run 'filebrowser config init' first.") |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		data.hadDB = exists |  | ||||||
| 		db, err := storm.Open(path) |  | ||||||
| 		checkErr(fmt.Sprintf("storm.Open path %v", path), err) |  | ||||||
| 
 |  | ||||||
| 		defer db.Close() |  | ||||||
| 		data.store, err = bolt.NewStorage(db) |  | ||||||
| 		checkErr("bolt.NewStorage", err) |  | ||||||
| 		fn(cmd, args, data) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| func marshal(filename string, data interface{}) error { | func marshal(filename string, data interface{}) error { | ||||||
| 	fd, err := os.Create(filename) | 	fd, err := os.Create(filename) | ||||||
| 
 | 
 | ||||||
| 	checkErr("os.Create", err) | 	utils.CheckErr("os.Create", err) | ||||||
| 	defer fd.Close() | 	defer fd.Close() | ||||||
| 
 | 
 | ||||||
| 	switch ext := filepath.Ext(filename); ext { | 	switch ext := filepath.Ext(filename); ext { | ||||||
|  | @ -127,7 +56,7 @@ func marshal(filename string, data interface{}) error { | ||||||
| 
 | 
 | ||||||
| func unmarshal(filename string, data interface{}) error { | func unmarshal(filename string, data interface{}) error { | ||||||
| 	fd, err := os.Open(filename) | 	fd, err := os.Open(filename) | ||||||
| 	checkErr("os.Open", err) | 	utils.CheckErr("os.Open", err) | ||||||
| 	defer fd.Close() | 	defer fd.Close() | ||||||
| 
 | 
 | ||||||
| 	switch ext := filepath.Ext(filename); ext { | 	switch ext := filepath.Ext(filename); ext { | ||||||
|  | @ -152,3 +81,8 @@ func jsonYamlArg(cmd *cobra.Command, args []string) error { | ||||||
| 		return errors.New("invalid format: " + ext) | 		return errors.New("invalid format: " + ext) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func cobraCmd(fn pythonFunc) cobraFunc { | ||||||
|  | 	return func(cmd *cobra.Command, args []string) { | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,21 +0,0 @@ | ||||||
| package cmd |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 
 |  | ||||||
| 	"github.com/spf13/cobra" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gtsteffaniak/filebrowser/version" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func init() { |  | ||||||
| 	rootCmd.AddCommand(versionCmd) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var versionCmd = &cobra.Command{ |  | ||||||
| 	Use:   "version", |  | ||||||
| 	Short: "Print the version number", |  | ||||||
| 	Run: func(cmd *cobra.Command, args []string) { |  | ||||||
| 		fmt.Println("File Browser " + version.Version + "/" + version.CommitSHA) |  | ||||||
| 	}, |  | ||||||
| } |  | ||||||
|  | @ -91,7 +91,7 @@ func ParseSearch(value string) *SearchOptions { | ||||||
| 			opts.LargerThan = updateSize(size) | 			opts.LargerThan = updateSize(size) | ||||||
| 		} | 		} | ||||||
| 		if strings.HasPrefix(filter, "smallerThan=") { | 		if strings.HasPrefix(filter, "smallerThan=") { | ||||||
| 			opts.Conditions["larger"] = true | 			opts.Conditions["smaller"] = true | ||||||
| 			size := strings.TrimPrefix(filter, "smallerThan=") | 			size := strings.TrimPrefix(filter, "smallerThan=") | ||||||
| 			opts.SmallerThan = updateSize(size) | 			opts.SmallerThan = updateSize(size) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -0,0 +1,154 @@ | ||||||
|  | package files | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Helper function to create error messages dynamically
 | ||||||
|  | func errorMsg(extension, expectedType string, expectedMatch bool) string { | ||||||
|  | 	matchStatus := "to match" | ||||||
|  | 	if !expectedMatch { | ||||||
|  | 		matchStatus = "to not match" | ||||||
|  | 	} | ||||||
|  | 	return fmt.Sprintf("Expected %s %s type '%s'", extension, matchStatus, expectedType) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestIsMatchingType(t *testing.T) { | ||||||
|  | 	// Test cases where IsMatchingType should return true
 | ||||||
|  | 	trueTestCases := []struct { | ||||||
|  | 		extension    string | ||||||
|  | 		expectedType string | ||||||
|  | 	}{ | ||||||
|  | 		{".pdf", "pdf"}, | ||||||
|  | 		{".doc", "doc"}, | ||||||
|  | 		{".docx", "doc"}, | ||||||
|  | 		{".json", "text"}, | ||||||
|  | 		{".sh", "text"}, | ||||||
|  | 		{".zip", "archive"}, | ||||||
|  | 		{".rar", "archive"}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tc := range trueTestCases { | ||||||
|  | 		assert.True(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, true)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Test cases where IsMatchingType should return false
 | ||||||
|  | 	falseTestCases := []struct { | ||||||
|  | 		extension    string | ||||||
|  | 		expectedType string | ||||||
|  | 	}{ | ||||||
|  | 		{".mp4", "doc"}, | ||||||
|  | 		{".mp4", "text"}, | ||||||
|  | 		{".mp4", "archive"}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tc := range falseTestCases { | ||||||
|  | 		assert.False(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, false)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestUpdateSize(t *testing.T) { | ||||||
|  | 	// Helper function for size error messages
 | ||||||
|  | 	sizeErrorMsg := func(input string, expected, actual int) string { | ||||||
|  | 		return fmt.Sprintf("Expected size for input '%s' to be %d, got %d", input, expected, actual) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Test cases for updateSize
 | ||||||
|  | 	testCases := []struct { | ||||||
|  | 		input    string | ||||||
|  | 		expected int | ||||||
|  | 	}{ | ||||||
|  | 		{"150", 150}, | ||||||
|  | 		{"invalid", 100}, | ||||||
|  | 		{"", 100}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tc := range testCases { | ||||||
|  | 		actual := updateSize(tc.input) | ||||||
|  | 		assert.Equal(t, tc.expected, actual, sizeErrorMsg(tc.input, tc.expected, actual)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestIsDoc(t *testing.T) { | ||||||
|  | 	// Test cases where IsMatchingType should return true for document types
 | ||||||
|  | 	docTrueTestCases := []struct { | ||||||
|  | 		extension    string | ||||||
|  | 		expectedType string | ||||||
|  | 	}{ | ||||||
|  | 		{".doc", "doc"}, | ||||||
|  | 		{".pdf", "doc"}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tc := range docTrueTestCases { | ||||||
|  | 		assert.True(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, true)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Test case where IsMatchingType should return false for document types
 | ||||||
|  | 	docFalseTestCases := []struct { | ||||||
|  | 		extension    string | ||||||
|  | 		expectedType string | ||||||
|  | 	}{ | ||||||
|  | 		{".mp4", "doc"}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tc := range docFalseTestCases { | ||||||
|  | 		assert.False(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, false)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestIsText(t *testing.T) { | ||||||
|  | 	// Test cases where IsMatchingType should return true for text types
 | ||||||
|  | 	textTrueTestCases := []struct { | ||||||
|  | 		extension    string | ||||||
|  | 		expectedType string | ||||||
|  | 	}{ | ||||||
|  | 		{".json", "text"}, | ||||||
|  | 		{".sh", "text"}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tc := range textTrueTestCases { | ||||||
|  | 		assert.True(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, true)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Test case where IsMatchingType should return false for text types
 | ||||||
|  | 	textFalseTestCases := []struct { | ||||||
|  | 		extension    string | ||||||
|  | 		expectedType string | ||||||
|  | 	}{ | ||||||
|  | 		{".mp4", "text"}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tc := range textFalseTestCases { | ||||||
|  | 		assert.False(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, false)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestIsArchive(t *testing.T) { | ||||||
|  | 	// Test cases where IsMatchingType should return true for archive types
 | ||||||
|  | 	archiveTrueTestCases := []struct { | ||||||
|  | 		extension    string | ||||||
|  | 		expectedType string | ||||||
|  | 	}{ | ||||||
|  | 		{".zip", "archive"}, | ||||||
|  | 		{".rar", "archive"}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tc := range archiveTrueTestCases { | ||||||
|  | 		assert.True(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, true)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Test case where IsMatchingType should return false for archive types
 | ||||||
|  | 	archiveFalseTestCases := []struct { | ||||||
|  | 		extension    string | ||||||
|  | 		expectedType string | ||||||
|  | 	}{ | ||||||
|  | 		{".mp4", "archive"}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tc := range archiveFalseTestCases { | ||||||
|  | 		assert.False(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, false)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -21,32 +21,42 @@ import ( | ||||||
| 	"github.com/gtsteffaniak/filebrowser/errors" | 	"github.com/gtsteffaniak/filebrowser/errors" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/rules" | 	"github.com/gtsteffaniak/filebrowser/rules" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/settings" | 	"github.com/gtsteffaniak/filebrowser/settings" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/users" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	bytesInMegabyte int64      = 1000000 | 	pathMutexes   = make(map[string]*sync.Mutex) | ||||||
| 	pathMutexes                = make(map[string]*sync.Mutex) | 	pathMutexesMu sync.Mutex // Mutex to protect the pathMutexes map
 | ||||||
| 	pathMutexesMu   sync.Mutex // Mutex to protect the pathMutexes map
 |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | type ReducedItem struct { | ||||||
|  | 	Name    string    `json:"name"` | ||||||
|  | 	Size    int64     `json:"size"` | ||||||
|  | 	ModTime time.Time `json:"modified"` | ||||||
|  | 	IsDir   bool      `json:"isDir,omitempty"` | ||||||
|  | 	Type    string    `json:"type"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // FileInfo describes a file.
 | // FileInfo describes a file.
 | ||||||
|  | // reduced item is non-recursive reduced "Items", used to pass flat items array
 | ||||||
| type FileInfo struct { | type FileInfo struct { | ||||||
| 	*Listing | 	Items        []*FileInfo       `json:"-"` | ||||||
| 	Path      string            `json:"path,omitempty"` | 	ReducedItems []ReducedItem     `json:"items,omitempty"` | ||||||
| 	Name      string            `json:"name"` | 	Path         string            `json:"path,omitempty"` | ||||||
| 	Size      int64             `json:"size"` | 	Name         string            `json:"name"` | ||||||
| 	Extension string            `json:"-"` | 	Size         int64             `json:"size"` | ||||||
| 	ModTime   time.Time         `json:"modified"` | 	Extension    string            `json:"-"` | ||||||
| 	CacheTime time.Time         `json:"-"` | 	ModTime      time.Time         `json:"modified"` | ||||||
| 	Mode      os.FileMode       `json:"-"` | 	CacheTime    time.Time         `json:"-"` | ||||||
| 	IsDir     bool              `json:"isDir,omitempty"` | 	Mode         os.FileMode       `json:"-"` | ||||||
| 	IsSymlink bool              `json:"isSymlink,omitempty"` | 	IsDir        bool              `json:"isDir,omitempty"` | ||||||
| 	Type      string            `json:"type"` | 	IsSymlink    bool              `json:"isSymlink,omitempty"` | ||||||
| 	Subtitles []string          `json:"subtitles,omitempty"` | 	Type         string            `json:"type"` | ||||||
| 	Content   string            `json:"content,omitempty"` | 	Subtitles    []string          `json:"subtitles,omitempty"` | ||||||
| 	Checksums map[string]string `json:"checksums,omitempty"` | 	Content      string            `json:"content,omitempty"` | ||||||
| 	Token     string            `json:"token,omitempty"` | 	Checksums    map[string]string `json:"checksums,omitempty"` | ||||||
|  | 	Token        string            `json:"token,omitempty"` | ||||||
|  | 	NumDirs      int               `json:"numDirs"` | ||||||
|  | 	NumFiles     int               `json:"numFiles"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // FileOptions are the options when getting a file info.
 | // FileOptions are the options when getting a file info.
 | ||||||
|  | @ -61,26 +71,11 @@ type FileOptions struct { | ||||||
| 	Content    bool | 	Content    bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Sorting constants
 | // Legacy file info method, only called on non-indexed directories.
 | ||||||
| const ( | // Once indexing completes for the first time, NewFileInfo is never called.
 | ||||||
| 	SortingByName     = "name" |  | ||||||
| 	SortingBySize     = "size" |  | ||||||
| 	SortingByModified = "modified" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Listing is a collection of files.
 |  | ||||||
| type Listing struct { |  | ||||||
| 	Items    []*FileInfo   `json:"items"` |  | ||||||
| 	Path     string        `json:"path"` |  | ||||||
| 	NumDirs  int           `json:"numDirs"` |  | ||||||
| 	NumFiles int           `json:"numFiles"` |  | ||||||
| 	Sorting  users.Sorting `json:"sorting"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // NewFileInfo creates a File object from a path and a given user. This File
 |  | ||||||
| // object will be automatically filled depending on if it is a directory
 |  | ||||||
| // or a file. If it's a video file, it will also detect any subtitles.
 |  | ||||||
| func NewFileInfo(opts FileOptions) (*FileInfo, error) { | func NewFileInfo(opts FileOptions) (*FileInfo, error) { | ||||||
|  | 
 | ||||||
|  | 	index := GetIndex(rootPath) | ||||||
| 	if !opts.Checker.Check(opts.Path) { | 	if !opts.Checker.Check(opts.Path) { | ||||||
| 		return nil, os.ErrPermission | 		return nil, os.ErrPermission | ||||||
| 	} | 	} | ||||||
|  | @ -93,6 +88,26 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) { | ||||||
| 			if err = file.readListing(opts.Path, opts.Checker, opts.ReadHeader); err != nil { | 			if err = file.readListing(opts.Path, opts.Checker, opts.ReadHeader); err != nil { | ||||||
| 				return nil, err | 				return nil, err | ||||||
| 			} | 			} | ||||||
|  | 			cleanedItems := []ReducedItem{} | ||||||
|  | 			for _, item := range file.Items { | ||||||
|  | 				// This is particularly useful for root of index, while indexing hasn't finished.
 | ||||||
|  | 				// adds the directory sizes for directories that have been indexed already.
 | ||||||
|  | 				if item.IsDir { | ||||||
|  | 					adjustedPath := index.makeIndexPath(opts.Path+"/"+item.Name, true) | ||||||
|  | 					info, _ := index.GetMetadataInfo(adjustedPath) | ||||||
|  | 					item.Size = info.Size | ||||||
|  | 				} | ||||||
|  | 				cleanedItems = append(cleanedItems, ReducedItem{ | ||||||
|  | 					Name:    item.Name, | ||||||
|  | 					Size:    item.Size, | ||||||
|  | 					IsDir:   item.IsDir, | ||||||
|  | 					ModTime: item.ModTime, | ||||||
|  | 					Type:    item.Type, | ||||||
|  | 				}) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			file.Items = nil | ||||||
|  | 			file.ReducedItems = cleanedItems | ||||||
| 			return file, nil | 			return file, nil | ||||||
| 		} | 		} | ||||||
| 		err = file.detectType(opts.Path, opts.Modify, opts.Content, true) | 		err = file.detectType(opts.Path, opts.Modify, opts.Content, true) | ||||||
|  | @ -102,6 +117,7 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) { | ||||||
| 	} | 	} | ||||||
| 	return file, err | 	return file, err | ||||||
| } | } | ||||||
|  | 
 | ||||||
| func FileInfoFaster(opts FileOptions) (*FileInfo, error) { | func FileInfoFaster(opts FileOptions) (*FileInfo, error) { | ||||||
| 	// Lock access for the specific path
 | 	// Lock access for the specific path
 | ||||||
| 	pathMutex := getMutex(opts.Path) | 	pathMutex := getMutex(opts.Path) | ||||||
|  | @ -133,12 +149,11 @@ func FileInfoFaster(opts FileOptions) (*FileInfo, error) { | ||||||
| 		file, err := NewFileInfo(opts) | 		file, err := NewFileInfo(opts) | ||||||
| 		return file, err | 		return file, err | ||||||
| 	} | 	} | ||||||
| 	info, exists := index.GetMetadataInfo(adjustedPath) | 	info, exists := index.GetMetadataInfo(adjustedPath + "/" + filepath.Base(opts.Path)) | ||||||
| 	if !exists || info.Name == "" { | 	if !exists || info.Name == "" { | ||||||
| 		return &FileInfo{}, errors.ErrEmptyKey | 		return NewFileInfo(opts) | ||||||
| 	} | 	} | ||||||
| 	return &info, nil | 	return &info, nil | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func RefreshFileInfo(opts FileOptions) error { | func RefreshFileInfo(opts FileOptions) error { | ||||||
|  | @ -491,9 +506,8 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	listing := &Listing{ | 	listing := &FileInfo{ | ||||||
| 		Items:    []*FileInfo{}, | 		Items:    []*FileInfo{}, | ||||||
| 		Path:     i.Path, |  | ||||||
| 		NumDirs:  0, | 		NumDirs:  0, | ||||||
| 		NumFiles: 0, | 		NumFiles: 0, | ||||||
| 	} | 	} | ||||||
|  | @ -548,7 +562,7 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo | ||||||
| 		listing.Items = append(listing.Items, file) | 		listing.Items = append(listing.Items, file) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	i.Listing = listing | 	i.Items = listing.Items | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| package files | package files | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" |  | ||||||
| 	"log" | 	"log" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
|  | @ -12,23 +11,12 @@ import ( | ||||||
| 	"github.com/gtsteffaniak/filebrowser/settings" | 	"github.com/gtsteffaniak/filebrowser/settings" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type Directory struct { |  | ||||||
| 	Metadata map[string]FileInfo |  | ||||||
| 	Files    string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type File struct { |  | ||||||
| 	Name  string |  | ||||||
| 	IsDir bool |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type Index struct { | type Index struct { | ||||||
| 	Root        string | 	Root        string | ||||||
| 	Directories map[string]Directory | 	Directories map[string]FileInfo | ||||||
| 	NumDirs     int | 	NumDirs     int | ||||||
| 	NumFiles    int | 	NumFiles    int | ||||||
| 	inProgress  bool | 	inProgress  bool | ||||||
| 	quickList   []File |  | ||||||
| 	LastIndexed time.Time | 	LastIndexed time.Time | ||||||
| 	mu          sync.RWMutex | 	mu          sync.RWMutex | ||||||
| } | } | ||||||
|  | @ -50,16 +38,12 @@ func indexingScheduler(intervalMinutes uint32) { | ||||||
| 		rootPath = settings.Config.Server.Root | 		rootPath = settings.Config.Server.Root | ||||||
| 	} | 	} | ||||||
| 	si := GetIndex(rootPath) | 	si := GetIndex(rootPath) | ||||||
| 	log.Printf("Indexing Files...") |  | ||||||
| 	log.Printf("Configured to run every %v minutes", intervalMinutes) |  | ||||||
| 	log.Printf("Indexing from root: %s", si.Root) |  | ||||||
| 	for { | 	for { | ||||||
| 		startTime := time.Now() | 		startTime := time.Now() | ||||||
| 		// Set the indexing flag to indicate that indexing is in progress
 | 		// Set the indexing flag to indicate that indexing is in progress
 | ||||||
| 		si.resetCount() | 		si.resetCount() | ||||||
| 		// Perform the indexing operation
 | 		// Perform the indexing operation
 | ||||||
| 		err := si.indexFiles(si.Root) | 		err := si.indexFiles(si.Root) | ||||||
| 		si.quickList = []File{} |  | ||||||
| 		// Reset the indexing flag to indicate that indexing has finished
 | 		// Reset the indexing flag to indicate that indexing has finished
 | ||||||
| 		si.inProgress = false | 		si.inProgress = false | ||||||
| 		// Update the LastIndexed time
 | 		// Update the LastIndexed time
 | ||||||
|  | @ -81,78 +65,114 @@ func indexingScheduler(intervalMinutes uint32) { | ||||||
| 
 | 
 | ||||||
| // Define a function to recursively index files and directories
 | // Define a function to recursively index files and directories
 | ||||||
| func (si *Index) indexFiles(path string) error { | func (si *Index) indexFiles(path string) error { | ||||||
| 	// Check if the current directory has been modified since the last indexing
 | 	// Ensure path is cleaned and normalized
 | ||||||
| 	adjustedPath := si.makeIndexPath(path, true) | 	adjustedPath := si.makeIndexPath(path, true) | ||||||
|  | 
 | ||||||
|  | 	// Open the directory
 | ||||||
| 	dir, err := os.Open(path) | 	dir, err := os.Open(path) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		// Directory must have been deleted, remove it from the index
 | 		// If the directory can't be opened (e.g., deleted), remove it from the index
 | ||||||
| 		si.RemoveDirectory(adjustedPath) | 		si.RemoveDirectory(adjustedPath) | ||||||
|  | 		return err | ||||||
| 	} | 	} | ||||||
|  | 	defer dir.Close() | ||||||
|  | 
 | ||||||
| 	dirInfo, err := dir.Stat() | 	dirInfo, err := dir.Stat() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		dir.Close() |  | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Compare the last modified time of the directory with the last indexed time
 | 	// Check if the directory is already up-to-date
 | ||||||
| 	lastIndexed := si.LastIndexed | 	if dirInfo.ModTime().Before(si.LastIndexed) { | ||||||
| 	if dirInfo.ModTime().Before(lastIndexed) { |  | ||||||
| 		dir.Close() |  | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Read the directory contents
 | 	// Read directory contents
 | ||||||
| 	files, err := dir.Readdir(-1) | 	files, err := dir.Readdir(-1) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	dir.Close() | 
 | ||||||
| 	si.UpdateQuickList(files) | 	// Recursively process files and directories
 | ||||||
| 	si.InsertFiles(path) | 	fileInfos := []*FileInfo{} | ||||||
| 	// done separately for memory efficiency on recursion
 | 	var totalSize int64 | ||||||
| 	si.InsertDirs(path) | 	var numDirs, numFiles int | ||||||
|  | 
 | ||||||
|  | 	for _, file := range files { | ||||||
|  | 		parentInfo := &FileInfo{ | ||||||
|  | 			Name:    file.Name(), | ||||||
|  | 			Size:    file.Size(), | ||||||
|  | 			ModTime: file.ModTime(), | ||||||
|  | 			IsDir:   file.IsDir(), | ||||||
|  | 		} | ||||||
|  | 		childInfo, err := si.InsertInfo(path, parentInfo) | ||||||
|  | 		if err != nil { | ||||||
|  | 			// Log error, but continue processing other files
 | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Accumulate directory size and items
 | ||||||
|  | 		totalSize += childInfo.Size | ||||||
|  | 		if childInfo.IsDir { | ||||||
|  | 			numDirs++ | ||||||
|  | 		} else { | ||||||
|  | 			numFiles++ | ||||||
|  | 		} | ||||||
|  | 		_ = childInfo.detectType(path, true, false, false) | ||||||
|  | 		fileInfos = append(fileInfos, childInfo) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Create FileInfo for the current directory
 | ||||||
|  | 	dirFileInfo := &FileInfo{ | ||||||
|  | 		Items:     fileInfos, | ||||||
|  | 		Name:      filepath.Base(path), | ||||||
|  | 		Size:      totalSize, | ||||||
|  | 		ModTime:   dirInfo.ModTime(), | ||||||
|  | 		CacheTime: time.Now(), | ||||||
|  | 		IsDir:     true, | ||||||
|  | 		NumDirs:   numDirs, | ||||||
|  | 		NumFiles:  numFiles, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Add directory to index
 | ||||||
|  | 	si.mu.Lock() | ||||||
|  | 	si.Directories[adjustedPath] = *dirFileInfo | ||||||
|  | 	si.NumDirs += numDirs | ||||||
|  | 	si.NumFiles += numFiles | ||||||
|  | 	si.mu.Unlock() | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (si *Index) InsertFiles(path string) { | // InsertInfo function to handle adding a file or directory into the index
 | ||||||
| 	adjustedPath := si.makeIndexPath(path, true) | func (si *Index) InsertInfo(parentPath string, file *FileInfo) (*FileInfo, error) { | ||||||
| 	subDirectory := Directory{} | 	filePath := filepath.Join(parentPath, file.Name) | ||||||
| 	buffer := bytes.Buffer{} |  | ||||||
| 
 | 
 | ||||||
| 	for _, f := range si.GetQuickList() { | 	// Check if it's a directory and recursively index it
 | ||||||
| 		if !f.IsDir { | 	if file.IsDir { | ||||||
| 			buffer.WriteString(f.Name + ";") | 		// Recursively index directory
 | ||||||
| 			si.UpdateCount("files") | 		err := si.indexFiles(filePath) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 	} |  | ||||||
| 	// Use GetMetadataInfo and SetFileMetadata for safer read and write operations
 |  | ||||||
| 	subDirectory.Files = buffer.String() |  | ||||||
| 	si.SetDirectoryInfo(adjustedPath, subDirectory) |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| func (si *Index) InsertDirs(path string) { | 		// Return directory info from the index
 | ||||||
| 	for _, f := range si.GetQuickList() { | 		adjustedPath := si.makeIndexPath(filePath, true) | ||||||
| 		if f.IsDir { | 		si.mu.RLock() | ||||||
| 			adjustedPath := si.makeIndexPath(path, true) | 		dirInfo := si.Directories[adjustedPath] | ||||||
| 			if _, exists := si.Directories[adjustedPath]; exists { | 		si.mu.RUnlock() | ||||||
| 				si.UpdateCount("dirs") | 		return &dirInfo, nil | ||||||
| 				// Add or update the directory in the map
 |  | ||||||
| 				if adjustedPath == "/" { |  | ||||||
| 					si.SetDirectoryInfo("/"+f.Name, Directory{}) |  | ||||||
| 				} else { |  | ||||||
| 					si.SetDirectoryInfo(adjustedPath+"/"+f.Name, Directory{}) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 			err := si.indexFiles(path + "/" + f.Name) |  | ||||||
| 			if err != nil { |  | ||||||
| 				if err.Error() == "invalid argument" { |  | ||||||
| 					log.Printf("Could not index \"%v\": %v \n", path, "Permission Denied") |  | ||||||
| 				} else { |  | ||||||
| 					log.Printf("Could not index \"%v\": %v \n", path, err) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	// Create FileInfo for regular files
 | ||||||
|  | 	fileInfo := &FileInfo{ | ||||||
|  | 		Path:    filePath, | ||||||
|  | 		Name:    file.Name, | ||||||
|  | 		Size:    file.Size, | ||||||
|  | 		ModTime: file.ModTime, | ||||||
|  | 		IsDir:   false, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return fileInfo, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (si *Index) makeIndexPath(subPath string, isDir bool) string { | func (si *Index) makeIndexPath(subPath string, isDir bool) string { | ||||||
|  | @ -171,5 +191,8 @@ func (si *Index) makeIndexPath(subPath string, isDir bool) string { | ||||||
| 	} else if !isDir { | 	} else if !isDir { | ||||||
| 		adjustedPath = filepath.Dir(adjustedPath) | 		adjustedPath = filepath.Dir(adjustedPath) | ||||||
| 	} | 	} | ||||||
|  | 	if !strings.HasPrefix(adjustedPath, "/") { | ||||||
|  | 		adjustedPath = "/" + adjustedPath | ||||||
|  | 	} | ||||||
| 	return adjustedPath | 	return adjustedPath | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ package files | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
| 	"math/rand" | 	"math/rand" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | @ -23,18 +24,26 @@ func BenchmarkFillIndex(b *testing.B) { | ||||||
| func (si *Index) createMockData(numDirs, numFilesPerDir int) { | func (si *Index) createMockData(numDirs, numFilesPerDir int) { | ||||||
| 	for i := 0; i < numDirs; i++ { | 	for i := 0; i < numDirs; i++ { | ||||||
| 		dirName := generateRandomPath(rand.Intn(3) + 1) | 		dirName := generateRandomPath(rand.Intn(3) + 1) | ||||||
| 		files := []File{} | 		files := []*FileInfo{} // Slice of FileInfo
 | ||||||
| 		// Append a new Directory to the slice
 | 
 | ||||||
|  | 		// Simulating files and directories with FileInfo
 | ||||||
| 		for j := 0; j < numFilesPerDir; j++ { | 		for j := 0; j < numFilesPerDir; j++ { | ||||||
| 			newFile := File{ | 			newFile := &FileInfo{ | ||||||
| 				Name:  "file-" + getRandomTerm() + getRandomExtension(), | 				Name:    "file-" + getRandomTerm() + getRandomExtension(), | ||||||
| 				IsDir: false, | 				IsDir:   false, | ||||||
|  | 				Size:    rand.Int63n(1000),                                          // Random size
 | ||||||
|  | 				ModTime: time.Now().Add(-time.Duration(rand.Intn(100)) * time.Hour), // Random mod time
 | ||||||
| 			} | 			} | ||||||
| 			files = append(files, newFile) | 			files = append(files, newFile) | ||||||
| 		} | 		} | ||||||
| 		si.UpdateQuickListForTests(files) | 
 | ||||||
| 		si.InsertFiles(dirName) | 		// Simulate inserting files into index
 | ||||||
| 		si.InsertDirs(dirName) | 		for _, file := range files { | ||||||
|  | 			_, err := si.InsertInfo(dirName, file) | ||||||
|  | 			if err != nil { | ||||||
|  | 				fmt.Println("Error inserting file:", err) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ package files | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"math/rand" | 	"math/rand" | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"sort" | 	"sort" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | @ -30,12 +29,17 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 		si.mu.Lock() | 		si.mu.Lock() | ||||||
| 		defer si.mu.Unlock() |  | ||||||
| 		for dirName, dir := range si.Directories { | 		for dirName, dir := range si.Directories { | ||||||
| 			isDir := true | 			isDir := true | ||||||
| 			files := strings.Split(dir.Files, ";") | 			files := []string{} | ||||||
|  | 			for _, item := range dir.Items { | ||||||
|  | 				if !item.IsDir { | ||||||
|  | 					files = append(files, item.Name) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
| 			value, found := sessionInProgress.Load(sourceSession) | 			value, found := sessionInProgress.Load(sourceSession) | ||||||
| 			if !found || value != runningHash { | 			if !found || value != runningHash { | ||||||
|  | 				si.mu.Unlock() | ||||||
| 				return []string{}, map[string]map[string]bool{} | 				return []string{}, map[string]map[string]bool{} | ||||||
| 			} | 			} | ||||||
| 			if count > maxSearchResults { | 			if count > maxSearchResults { | ||||||
|  | @ -46,7 +50,9 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st | ||||||
| 				continue // path not matched
 | 				continue // path not matched
 | ||||||
| 			} | 			} | ||||||
| 			fileTypes := map[string]bool{} | 			fileTypes := map[string]bool{} | ||||||
| 			matches, fileType := containsSearchTerm(dirName, searchTerm, *searchOptions, isDir, fileTypes) | 			si.mu.Unlock() | ||||||
|  | 			matches, fileType := si.containsSearchTerm(dirName, searchTerm, *searchOptions, isDir, fileTypes) | ||||||
|  | 			si.mu.Lock() | ||||||
| 			if matches { | 			if matches { | ||||||
| 				fileListTypes[pathName] = fileType | 				fileListTypes[pathName] = fileType | ||||||
| 				matching = append(matching, pathName) | 				matching = append(matching, pathName) | ||||||
|  | @ -67,8 +73,9 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st | ||||||
| 				} | 				} | ||||||
| 				fullName := strings.TrimLeft(pathName+file, "/") | 				fullName := strings.TrimLeft(pathName+file, "/") | ||||||
| 				fileTypes := map[string]bool{} | 				fileTypes := map[string]bool{} | ||||||
| 
 | 				si.mu.Unlock() | ||||||
| 				matches, fileType := containsSearchTerm(fullName, searchTerm, *searchOptions, isDir, fileTypes) | 				matches, fileType := si.containsSearchTerm(fullName, searchTerm, *searchOptions, isDir, fileTypes) | ||||||
|  | 				si.mu.Lock() | ||||||
| 				if !matches { | 				if !matches { | ||||||
| 					continue | 					continue | ||||||
| 				} | 				} | ||||||
|  | @ -77,6 +84,7 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st | ||||||
| 				count++ | 				count++ | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 		si.mu.Unlock() | ||||||
| 	} | 	} | ||||||
| 	// Sort the strings based on the number of elements after splitting by "/"
 | 	// Sort the strings based on the number of elements after splitting by "/"
 | ||||||
| 	sort.Slice(matching, func(i, j int) bool { | 	sort.Slice(matching, func(i, j int) bool { | ||||||
|  | @ -102,65 +110,88 @@ func scopedPathNameFilter(pathName string, scope string, isDir bool) string { | ||||||
| 	return pathName | 	return pathName | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func containsSearchTerm(pathName string, searchTerm string, options SearchOptions, isDir bool, fileTypes map[string]bool) (bool, map[string]bool) { | func (si *Index) containsSearchTerm(pathName string, searchTerm string, options SearchOptions, isDir bool, fileTypes map[string]bool) (bool, map[string]bool) { | ||||||
|  | 	largerThan := int64(options.LargerThan) * 1024 * 1024 | ||||||
|  | 	smallerThan := int64(options.SmallerThan) * 1024 * 1024 | ||||||
| 	conditions := options.Conditions | 	conditions := options.Conditions | ||||||
| 	path := getLastPathComponent(pathName) | 	fileName := filepath.Base(pathName) | ||||||
| 	// Convert to lowercase once
 | 	adjustedPath := si.makeIndexPath(pathName, isDir) | ||||||
|  | 
 | ||||||
|  | 	// Convert to lowercase if not exact match
 | ||||||
| 	if !conditions["exact"] { | 	if !conditions["exact"] { | ||||||
| 		path = strings.ToLower(path) | 		fileName = strings.ToLower(fileName) | ||||||
| 		searchTerm = strings.ToLower(searchTerm) | 		searchTerm = strings.ToLower(searchTerm) | ||||||
| 	} | 	} | ||||||
| 	if strings.Contains(path, searchTerm) { | 
 | ||||||
| 		// Calculate fileSize only if needed
 | 	// Check if the file name contains the search term
 | ||||||
| 		var fileSize int64 | 	if !strings.Contains(fileName, searchTerm) { | ||||||
| 		matchesAllConditions := true | 		return false, map[string]bool{} | ||||||
| 		extension := filepath.Ext(path) | 	} | ||||||
| 		for _, k := range AllFiletypeOptions { | 
 | ||||||
| 			if IsMatchingType(extension, k) { | 	// Initialize file size and fileTypes map
 | ||||||
| 				fileTypes[k] = true | 	var fileSize int64 | ||||||
|  | 	extension := filepath.Ext(fileName) | ||||||
|  | 
 | ||||||
|  | 	// Collect file types
 | ||||||
|  | 	for _, k := range AllFiletypeOptions { | ||||||
|  | 		if IsMatchingType(extension, k) { | ||||||
|  | 			fileTypes[k] = true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	fileTypes["dir"] = isDir | ||||||
|  | 	// Get file info if needed for size-related conditions
 | ||||||
|  | 	if largerThan > 0 || smallerThan > 0 { | ||||||
|  | 		fileInfo, exists := si.GetMetadataInfo(adjustedPath) | ||||||
|  | 		if !exists { | ||||||
|  | 			return false, fileTypes | ||||||
|  | 		} else if !isDir { | ||||||
|  | 			// Look for specific file in ReducedItems
 | ||||||
|  | 			for _, item := range fileInfo.ReducedItems { | ||||||
|  | 				lower := strings.ToLower(item.Name) | ||||||
|  | 				if strings.Contains(lower, searchTerm) { | ||||||
|  | 					if item.Size == 0 { | ||||||
|  | 						return false, fileTypes | ||||||
|  | 					} | ||||||
|  | 					fileSize = item.Size | ||||||
|  | 					break | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			fileSize = fileInfo.Size | ||||||
|  | 		} | ||||||
|  | 		if fileSize == 0 { | ||||||
|  | 			return false, fileTypes | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Evaluate all conditions
 | ||||||
|  | 	for t, v := range conditions { | ||||||
|  | 		if t == "exact" { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		switch t { | ||||||
|  | 		case "larger": | ||||||
|  | 			if largerThan > 0 { | ||||||
|  | 				if fileSize <= largerThan { | ||||||
|  | 					return false, fileTypes | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		case "smaller": | ||||||
|  | 			if smallerThan > 0 { | ||||||
|  | 				if fileSize >= smallerThan { | ||||||
|  | 					return false, fileTypes | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		default: | ||||||
|  | 			// Handle other file type conditions
 | ||||||
|  | 			notMatchType := v != fileTypes[t] | ||||||
|  | 			if notMatchType { | ||||||
|  | 				return false, fileTypes | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		fileTypes["dir"] = isDir |  | ||||||
| 		for t, v := range conditions { |  | ||||||
| 			if t == "exact" { |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 			var matchesCondition bool |  | ||||||
| 			switch t { |  | ||||||
| 			case "larger": |  | ||||||
| 				if fileSize == 0 { |  | ||||||
| 					fileSize = getFileSize(pathName) |  | ||||||
| 				} |  | ||||||
| 				matchesCondition = fileSize > int64(options.LargerThan)*bytesInMegabyte |  | ||||||
| 			case "smaller": |  | ||||||
| 				if fileSize == 0 { |  | ||||||
| 					fileSize = getFileSize(pathName) |  | ||||||
| 				} |  | ||||||
| 				matchesCondition = fileSize < int64(options.SmallerThan)*bytesInMegabyte |  | ||||||
| 			default: |  | ||||||
| 				matchesCondition = v == fileTypes[t] |  | ||||||
| 			} |  | ||||||
| 			if !matchesCondition { |  | ||||||
| 				matchesAllConditions = false |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		return matchesAllConditions, fileTypes |  | ||||||
| 	} | 	} | ||||||
| 	// Clear variables and return
 |  | ||||||
| 	return false, map[string]bool{} |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| func getFileSize(filepath string) int64 { | 	return true, fileTypes | ||||||
| 	fileInfo, err := os.Stat(rootPath + "/" + filepath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return 0 |  | ||||||
| 	} |  | ||||||
| 	return fileInfo.Size() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func getLastPathComponent(path string) string { |  | ||||||
| 	// Use filepath.Base to extract the last component of the path
 |  | ||||||
| 	return filepath.Base(path) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func generateRandomHash(length int) string { | func generateRandomHash(length int) string { | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ func BenchmarkSearchAllIndexes(b *testing.B) { | ||||||
| 	InitializeIndex(5, false) | 	InitializeIndex(5, false) | ||||||
| 	si := GetIndex(rootPath) | 	si := GetIndex(rootPath) | ||||||
| 
 | 
 | ||||||
| 	si.createMockData(50, 3) // 1000 dirs, 3 files per dir
 | 	si.createMockData(50, 3) // 50 dirs, 3 files per dir
 | ||||||
| 
 | 
 | ||||||
| 	// Generate 100 random search terms
 | 	// Generate 100 random search terms
 | ||||||
| 	searchTerms := generateRandomSearchTerms(100) | 	searchTerms := generateRandomSearchTerms(100) | ||||||
|  | @ -26,87 +26,90 @@ func BenchmarkSearchAllIndexes(b *testing.B) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // loop over test files and compare output
 |  | ||||||
| func TestParseSearch(t *testing.T) { | func TestParseSearch(t *testing.T) { | ||||||
| 	value := ParseSearch("my test search") | 	tests := []struct { | ||||||
| 	want := &SearchOptions{ | 		input string | ||||||
| 		Conditions: map[string]bool{ | 		want  *SearchOptions | ||||||
| 			"exact": false, | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			input: "my test search", | ||||||
|  | 			want: &SearchOptions{ | ||||||
|  | 				Conditions: map[string]bool{"exact": false}, | ||||||
|  | 				Terms:      []string{"my test search"}, | ||||||
|  | 			}, | ||||||
| 		}, | 		}, | ||||||
| 		Terms: []string{"my test search"}, | 		{ | ||||||
| 	} | 			input: "case:exact my|test|search", | ||||||
| 	if !reflect.DeepEqual(value, want) { | 			want: &SearchOptions{ | ||||||
| 		t.Fatalf("\n got:  %+v\n want: %+v", value, want) | 				Conditions: map[string]bool{"exact": true}, | ||||||
| 	} | 				Terms:      []string{"my", "test", "search"}, | ||||||
| 	value = ParseSearch("case:exact my|test|search") | 			}, | ||||||
| 	want = &SearchOptions{ |  | ||||||
| 		Conditions: map[string]bool{ |  | ||||||
| 			"exact": true, |  | ||||||
| 		}, | 		}, | ||||||
| 		Terms: []string{"my", "test", "search"}, | 		{ | ||||||
| 	} | 			input: "type:largerThan=100 type:smallerThan=1000 test", | ||||||
| 	if !reflect.DeepEqual(value, want) { | 			want: &SearchOptions{ | ||||||
| 		t.Fatalf("\n got:  %+v\n want: %+v", value, want) | 				Conditions:  map[string]bool{"exact": false, "larger": true, "smaller": true}, | ||||||
| 	} | 				Terms:       []string{"test"}, | ||||||
| 	value = ParseSearch("type:largerThan=100 type:smallerThan=1000 test") | 				LargerThan:  100, | ||||||
| 	want = &SearchOptions{ | 				SmallerThan: 1000, | ||||||
| 		Conditions: map[string]bool{ | 			}, | ||||||
| 			"exact":  false, |  | ||||||
| 			"larger": true, |  | ||||||
| 		}, | 		}, | ||||||
| 		Terms:       []string{"test"}, | 		{ | ||||||
| 		LargerThan:  100, | 			input: "type:audio thisfile", | ||||||
| 		SmallerThan: 1000, | 			want: &SearchOptions{ | ||||||
| 	} | 				Conditions: map[string]bool{"exact": false, "audio": true}, | ||||||
| 	if !reflect.DeepEqual(value, want) { | 				Terms:      []string{"thisfile"}, | ||||||
| 		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) | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.input, func(t *testing.T) { | ||||||
|  | 			value := ParseSearch(tt.input) | ||||||
|  | 			if !reflect.DeepEqual(value, tt.want) { | ||||||
|  | 				t.Fatalf("\n got:  %+v\n want: %+v", value, tt.want) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestSearchWhileIndexing(t *testing.T) { | func TestSearchWhileIndexing(t *testing.T) { | ||||||
| 	InitializeIndex(5, false) | 	InitializeIndex(5, false) | ||||||
| 	si := GetIndex(rootPath) | 	si := GetIndex(rootPath) | ||||||
| 	// Generate 100 random search terms
 | 
 | ||||||
| 	// Generate 100 random search terms
 |  | ||||||
| 	searchTerms := generateRandomSearchTerms(10) | 	searchTerms := generateRandomSearchTerms(10) | ||||||
| 	for i := 0; i < 5; i++ { | 	for i := 0; i < 5; i++ { | ||||||
| 		// Execute the SearchAllIndexes function
 | 		go si.createMockData(100, 100) // Creating mock data concurrently
 | ||||||
| 		go si.createMockData(100, 100) // 1000 dirs, 3 files per dir
 |  | ||||||
| 		for _, term := range searchTerms { | 		for _, term := range searchTerms { | ||||||
| 			go si.Search(term, "/", "test") | 			go si.Search(term, "/", "test") // Search concurrently
 | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestSearchIndexes(t *testing.T) { | func TestSearchIndexes(t *testing.T) { | ||||||
| 	index := Index{ | 	index := Index{ | ||||||
| 		Directories: map[string]Directory{ | 		Directories: map[string]FileInfo{ | ||||||
| 			"test": { | 			"test":      {Items: []*FileInfo{{Name: "audio1.wav"}}}, | ||||||
| 				Files: "audio1.wav;", | 			"test/path": {Items: []*FileInfo{{Name: "file.txt"}}}, | ||||||
| 			}, | 			"new/test": {Items: []*FileInfo{ | ||||||
| 			"test/path": { | 				{Name: "audio.wav"}, | ||||||
| 				Files: "file.txt;", | 				{Name: "video.mp4"}, | ||||||
| 			}, | 				{Name: "video.MP4"}, | ||||||
| 			"new": {}, | 			}}, | ||||||
| 			"new/test": { | 			"new/test/path": {Items: []*FileInfo{{Name: "archive.zip"}}}, | ||||||
| 				Files: "audio.wav;video.mp4;video.MP4;", | 			"/firstDir": {Items: []*FileInfo{ | ||||||
| 			}, | 				{Name: "archive.zip", Size: 100}, | ||||||
| 			"new/test/path": { | 				{Name: "thisIsDir", IsDir: true, Size: 2 * 1024 * 1024}, | ||||||
| 				Files: "archive.zip;", | 			}}, | ||||||
|  | 			"/firstDir/thisIsDir": { | ||||||
|  | 				Items: []*FileInfo{ | ||||||
|  | 					{Name: "hi.txt"}, | ||||||
|  | 				}, | ||||||
|  | 				Size: 2 * 1024 * 1024, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	tests := []struct { | 	tests := []struct { | ||||||
| 		search         string | 		search         string | ||||||
| 		scope          string | 		scope          string | ||||||
|  | @ -118,7 +121,7 @@ func TestSearchIndexes(t *testing.T) { | ||||||
| 			scope:          "/new/", | 			scope:          "/new/", | ||||||
| 			expectedResult: []string{"test/audio.wav"}, | 			expectedResult: []string{"test/audio.wav"}, | ||||||
| 			expectedTypes: map[string]map[string]bool{ | 			expectedTypes: map[string]map[string]bool{ | ||||||
| 				"test/audio.wav": map[string]bool{"audio": true, "dir": false}, | 				"test/audio.wav": {"audio": true, "dir": false}, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
|  | @ -126,16 +129,41 @@ func TestSearchIndexes(t *testing.T) { | ||||||
| 			scope:          "/", | 			scope:          "/", | ||||||
| 			expectedResult: []string{"test/", "new/test/"}, | 			expectedResult: []string{"test/", "new/test/"}, | ||||||
| 			expectedTypes: map[string]map[string]bool{ | 			expectedTypes: map[string]map[string]bool{ | ||||||
| 				"test/":     map[string]bool{"dir": true}, | 				"test/":     {"dir": true}, | ||||||
| 				"new/test/": map[string]bool{"dir": true}, | 				"new/test/": {"dir": true}, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			search:         "archive", | 			search:         "archive", | ||||||
| 			scope:          "/", | 			scope:          "/", | ||||||
| 			expectedResult: []string{"new/test/path/archive.zip"}, | 			expectedResult: []string{"firstDir/archive.zip", "new/test/path/archive.zip"}, | ||||||
| 			expectedTypes: map[string]map[string]bool{ | 			expectedTypes: map[string]map[string]bool{ | ||||||
| 				"new/test/path/archive.zip": map[string]bool{"archive": true, "dir": false}, | 				"new/test/path/archive.zip": {"archive": true, "dir": false}, | ||||||
|  | 				"firstDir/archive.zip":      {"archive": true, "dir": false}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			search:         "arch", | ||||||
|  | 			scope:          "/firstDir", | ||||||
|  | 			expectedResult: []string{"archive.zip"}, | ||||||
|  | 			expectedTypes: map[string]map[string]bool{ | ||||||
|  | 				"archive.zip": {"archive": true, "dir": false}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			search:         "isdir", | ||||||
|  | 			scope:          "/", | ||||||
|  | 			expectedResult: []string{"firstDir/thisIsDir/"}, | ||||||
|  | 			expectedTypes: map[string]map[string]bool{ | ||||||
|  | 				"firstDir/thisIsDir/": {"dir": true}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			search:         "dir type:largerThan=1", | ||||||
|  | 			scope:          "/", | ||||||
|  | 			expectedResult: []string{"firstDir/thisIsDir/"}, | ||||||
|  | 			expectedTypes: map[string]map[string]bool{ | ||||||
|  | 				"firstDir/thisIsDir/": {"dir": true}, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
|  | @ -146,18 +174,17 @@ func TestSearchIndexes(t *testing.T) { | ||||||
| 				"new/test/video.MP4", | 				"new/test/video.MP4", | ||||||
| 			}, | 			}, | ||||||
| 			expectedTypes: map[string]map[string]bool{ | 			expectedTypes: map[string]map[string]bool{ | ||||||
| 				"new/test/video.MP4": map[string]bool{"video": true, "dir": false}, | 				"new/test/video.MP4": {"video": true, "dir": false}, | ||||||
| 				"new/test/video.mp4": map[string]bool{"video": true, "dir": false}, | 				"new/test/video.mp4": {"video": true, "dir": false}, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	for _, tt := range tests { | 	for _, tt := range tests { | ||||||
| 		t.Run(tt.search, func(t *testing.T) { | 		t.Run(tt.search, func(t *testing.T) { | ||||||
| 			actualResult, actualTypes := index.Search(tt.search, tt.scope, "") | 			actualResult, actualTypes := index.Search(tt.search, tt.scope, "") | ||||||
| 			assert.Equal(t, tt.expectedResult, actualResult) | 			assert.Equal(t, tt.expectedResult, actualResult) | ||||||
| 			if !reflect.DeepEqual(tt.expectedTypes, actualTypes) { | 			assert.Equal(t, tt.expectedTypes, actualTypes) | ||||||
| 				t.Fatalf("\n got:  %+v\n want: %+v", actualTypes, tt.expectedTypes) |  | ||||||
| 			} |  | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -186,6 +213,7 @@ func Test_scopedPathNameFilter(t *testing.T) { | ||||||
| 			want: "", // Update this with the expected result
 | 			want: "", // Update this with the expected result
 | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	for _, tt := range tests { | 	for _, tt := range tests { | ||||||
| 		t.Run(tt.name, func(t *testing.T) { | 		t.Run(tt.name, func(t *testing.T) { | ||||||
| 			if got := scopedPathNameFilter(tt.args.pathName, tt.args.scope, tt.args.isDir); got != tt.want { | 			if got := scopedPathNameFilter(tt.args.pathName, tt.args.scope, tt.args.isDir); got != tt.want { | ||||||
|  | @ -194,103 +222,3 @@ func Test_scopedPathNameFilter(t *testing.T) { | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 |  | ||||||
| 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) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| package files | package files | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"io/fs" |  | ||||||
| 	"log" | 	"log" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
|  | @ -13,15 +12,10 @@ func (si *Index) UpdateFileMetadata(adjustedPath string, info FileInfo) bool { | ||||||
| 	si.mu.Lock() | 	si.mu.Lock() | ||||||
| 	defer si.mu.Unlock() | 	defer si.mu.Unlock() | ||||||
| 	dir, exists := si.Directories[adjustedPath] | 	dir, exists := si.Directories[adjustedPath] | ||||||
| 	if !exists || exists && dir.Metadata == nil { | 	if !exists { | ||||||
| 		// Initialize the Metadata map if it is nil
 | 		si.Directories[adjustedPath] = FileInfo{} | ||||||
| 		if dir.Metadata == nil { |  | ||||||
| 			dir.Metadata = make(map[string]FileInfo) |  | ||||||
| 		} |  | ||||||
| 		si.Directories[adjustedPath] = dir |  | ||||||
| 		// Release the read lock before calling SetFileMetadata
 |  | ||||||
| 	} | 	} | ||||||
| 	return si.SetFileMetadata(adjustedPath, info) | 	return si.SetFileMetadata(adjustedPath, dir) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // SetFileMetadata sets the FileInfo for the specified directory in the index.
 | // SetFileMetadata sets the FileInfo for the specified directory in the index.
 | ||||||
|  | @ -32,37 +26,45 @@ func (si *Index) SetFileMetadata(adjustedPath string, info FileInfo) bool { | ||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
| 	info.CacheTime = time.Now() | 	info.CacheTime = time.Now() | ||||||
| 	si.Directories[adjustedPath].Metadata[adjustedPath] = info | 	si.Directories[adjustedPath] = info | ||||||
| 	return true | 	return true | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetMetadataInfo retrieves the FileInfo from the specified directory in the index.
 | // GetMetadataInfo retrieves the FileInfo from the specified directory in the index.
 | ||||||
| func (si *Index) GetMetadataInfo(adjustedPath string) (FileInfo, bool) { | func (si *Index) GetMetadataInfo(adjustedPath string) (FileInfo, bool) { | ||||||
| 	fi := FileInfo{} |  | ||||||
| 	si.mu.RLock() | 	si.mu.RLock() | ||||||
| 	dir, exists := si.Directories[adjustedPath] | 	dir, exists := si.Directories[adjustedPath] | ||||||
| 	si.mu.RUnlock() | 	si.mu.RUnlock() | ||||||
| 	if exists { | 	if !exists { | ||||||
| 		// Initialize the Metadata map if it is nil
 | 		return dir, exists | ||||||
| 		if dir.Metadata == nil { |  | ||||||
| 			dir.Metadata = make(map[string]FileInfo) |  | ||||||
| 			si.SetDirectoryInfo(adjustedPath, dir) |  | ||||||
| 		} else { |  | ||||||
| 			fi = dir.Metadata[adjustedPath] |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 	return fi, exists | 	// remove recursive items, we only want this directories direct files
 | ||||||
|  | 	cleanedItems := []ReducedItem{} | ||||||
|  | 	for _, item := range dir.Items { | ||||||
|  | 		cleanedItems = append(cleanedItems, ReducedItem{ | ||||||
|  | 			Name:    item.Name, | ||||||
|  | 			Size:    item.Size, | ||||||
|  | 			IsDir:   item.IsDir, | ||||||
|  | 			ModTime: item.ModTime, | ||||||
|  | 			Type:    item.Type, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 	dir.Items = nil | ||||||
|  | 	dir.ReducedItems = cleanedItems | ||||||
|  | 	realPath, _, _ := GetRealPath(adjustedPath) | ||||||
|  | 	dir.Path = realPath | ||||||
|  | 	return dir, exists | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // SetDirectoryInfo sets the directory information in the index.
 | // SetDirectoryInfo sets the directory information in the index.
 | ||||||
| func (si *Index) SetDirectoryInfo(adjustedPath string, dir Directory) { | func (si *Index) SetDirectoryInfo(adjustedPath string, dir FileInfo) { | ||||||
| 	si.mu.Lock() | 	si.mu.Lock() | ||||||
| 	si.Directories[adjustedPath] = dir | 	si.Directories[adjustedPath] = dir | ||||||
| 	si.mu.Unlock() | 	si.mu.Unlock() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // SetDirectoryInfo sets the directory information in the index.
 | // SetDirectoryInfo sets the directory information in the index.
 | ||||||
| func (si *Index) GetDirectoryInfo(adjustedPath string) (Directory, bool) { | func (si *Index) GetDirectoryInfo(adjustedPath string) (FileInfo, bool) { | ||||||
| 	si.mu.RLock() | 	si.mu.RLock() | ||||||
| 	dir, exists := si.Directories[adjustedPath] | 	dir, exists := si.Directories[adjustedPath] | ||||||
| 	si.mu.RUnlock() | 	si.mu.RUnlock() | ||||||
|  | @ -106,7 +108,7 @@ func GetIndex(root string) *Index { | ||||||
| 	} | 	} | ||||||
| 	newIndex := &Index{ | 	newIndex := &Index{ | ||||||
| 		Root:        rootPath, | 		Root:        rootPath, | ||||||
| 		Directories: make(map[string]Directory), // Initialize the map
 | 		Directories: map[string]FileInfo{}, | ||||||
| 		NumDirs:     0, | 		NumDirs:     0, | ||||||
| 		NumFiles:    0, | 		NumFiles:    0, | ||||||
| 		inProgress:  false, | 		inProgress:  false, | ||||||
|  | @ -116,36 +118,3 @@ func GetIndex(root string) *Index { | ||||||
| 	indexesMutex.Unlock() | 	indexesMutex.Unlock() | ||||||
| 	return newIndex | 	return newIndex | ||||||
| } | } | ||||||
| 
 |  | ||||||
| func (si *Index) UpdateQuickList(files []fs.FileInfo) { |  | ||||||
| 	si.mu.Lock() |  | ||||||
| 	defer si.mu.Unlock() |  | ||||||
| 	si.quickList = []File{} |  | ||||||
| 	for _, file := range files { |  | ||||||
| 		newFile := File{ |  | ||||||
| 			Name:  file.Name(), |  | ||||||
| 			IsDir: file.IsDir(), |  | ||||||
| 		} |  | ||||||
| 		si.quickList = append(si.quickList, newFile) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (si *Index) UpdateQuickListForTests(files []File) { |  | ||||||
| 	si.mu.Lock() |  | ||||||
| 	defer si.mu.Unlock() |  | ||||||
| 	si.quickList = []File{} |  | ||||||
| 	for _, file := range files { |  | ||||||
| 		newFile := File{ |  | ||||||
| 			Name:  file.Name, |  | ||||||
| 			IsDir: file.IsDir, |  | ||||||
| 		} |  | ||||||
| 		si.quickList = append(si.quickList, newFile) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (si *Index) GetQuickList() []File { |  | ||||||
| 	si.mu.Lock() |  | ||||||
| 	defer si.mu.Unlock() |  | ||||||
| 	newQuickList := si.quickList |  | ||||||
| 	return newQuickList |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,92 +1,118 @@ | ||||||
| package files | package files | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"io/fs" |  | ||||||
| 	"os" |  | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Mock for fs.FileInfo
 |  | ||||||
| type mockFileInfo struct { |  | ||||||
| 	name  string |  | ||||||
| 	isDir bool |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m mockFileInfo) Name() string       { return m.name } |  | ||||||
| func (m mockFileInfo) Size() int64        { return 0 } |  | ||||||
| func (m mockFileInfo) Mode() os.FileMode  { return 0 } |  | ||||||
| func (m mockFileInfo) ModTime() time.Time { return time.Now() } |  | ||||||
| func (m mockFileInfo) IsDir() bool        { return m.isDir } |  | ||||||
| func (m mockFileInfo) Sys() interface{}   { return nil } |  | ||||||
| 
 |  | ||||||
| var testIndex Index | var testIndex Index | ||||||
| 
 | 
 | ||||||
| // Test for GetFileMetadata
 | // Test for GetFileMetadata// Test for GetFileMetadata
 | ||||||
| //func TestGetFileMetadata(t *testing.T) {
 | func TestGetFileMetadataSize(t *testing.T) { | ||||||
| //	t.Parallel()
 | 	t.Parallel() | ||||||
| //	tests := []struct {
 | 	tests := []struct { | ||||||
| //		name           string
 | 		name         string | ||||||
| //		adjustedPath   string
 | 		adjustedPath string | ||||||
| //		fileName       string
 | 		expectedName string | ||||||
| //		expectedName   string
 | 		expectedSize int64 | ||||||
| //		expectedExists bool
 | 	}{ | ||||||
| //	}{
 | 		{ | ||||||
| //		{
 | 			name:         "testpath exists", | ||||||
| //			name:           "testpath exists",
 | 			adjustedPath: "/testpath", | ||||||
| //			adjustedPath:   "/testpath",
 | 			expectedName: "testfile.txt", | ||||||
| //			fileName:       "testfile.txt",
 | 			expectedSize: 100, | ||||||
| //			expectedName:   "testfile.txt",
 | 		}, | ||||||
| //			expectedExists: true,
 | 		{ | ||||||
| //		},
 | 			name:         "testpath exists", | ||||||
| //		{
 | 			adjustedPath: "/testpath", | ||||||
| //			name:           "testpath not exists",
 | 			expectedName: "directory", | ||||||
| //			adjustedPath:   "/testpath",
 | 			expectedSize: 100, | ||||||
| //			fileName:       "nonexistent.txt",
 | 		}, | ||||||
| //			expectedName:   "",
 | 	} | ||||||
| //			expectedExists: false,
 | 	for _, tt := range tests { | ||||||
| //		},
 | 		t.Run(tt.name, func(t *testing.T) { | ||||||
| //		{
 | 			fileInfo, _ := testIndex.GetMetadataInfo(tt.adjustedPath) | ||||||
| //			name:           "File exists in /anotherpath",
 | 			// Iterate over fileInfo.Items to look for expectedName
 | ||||||
| //			adjustedPath:   "/anotherpath",
 | 			for _, item := range fileInfo.ReducedItems { | ||||||
| //			fileName:       "afile.txt",
 | 				// Assert the existence and the name
 | ||||||
| //			expectedName:   "afile.txt",
 | 				if item.Name == tt.expectedName { | ||||||
| //			expectedExists: true,
 | 					assert.Equal(t, tt.expectedSize, item.Size) | ||||||
| //		},
 | 					break | ||||||
| //		{
 | 				} | ||||||
| //			name:           "File does not exist in /anotherpath",
 | 			} | ||||||
| //			adjustedPath:   "/anotherpath",
 | 		}) | ||||||
| //			fileName:       "nonexistentfile.txt",
 | 	} | ||||||
| //			expectedName:   "",
 | } | ||||||
| //			expectedExists: false,
 | 
 | ||||||
| //		},
 | // Test for GetFileMetadata// Test for GetFileMetadata
 | ||||||
| //		{
 | func TestGetFileMetadata(t *testing.T) { | ||||||
| //			name:           "Directory does not exist",
 | 	t.Parallel() | ||||||
| //			adjustedPath:   "/nonexistentpath",
 | 	tests := []struct { | ||||||
| //			fileName:       "testfile.txt",
 | 		name           string | ||||||
| //			expectedName:   "",
 | 		adjustedPath   string | ||||||
| //			expectedExists: false,
 | 		expectedName   string | ||||||
| //		},
 | 		expectedExists bool | ||||||
| //	}
 | 	}{ | ||||||
| //
 | 		{ | ||||||
| //	for _, tt := range tests {
 | 			name:           "testpath exists", | ||||||
| //		t.Run(tt.name, func(t *testing.T) {
 | 			adjustedPath:   "/testpath", | ||||||
| //			fileInfo, exists := testIndex.GetFileMetadata(tt.adjustedPath)
 | 			expectedName:   "testfile.txt", | ||||||
| //			if exists != tt.expectedExists || fileInfo.Name != tt.expectedName {
 | 			expectedExists: true, | ||||||
| //				t.Errorf("expected %v:%v but got: %v:%v", tt.expectedName, tt.expectedExists, //fileInfo.Name, exists)
 | 		}, | ||||||
| //			}
 | 		{ | ||||||
| //		})
 | 			name:           "testpath not exists", | ||||||
| //	}
 | 			adjustedPath:   "/testpath", | ||||||
| //}
 | 			expectedName:   "nonexistent.txt", | ||||||
|  | 			expectedExists: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:           "File exists in /anotherpath", | ||||||
|  | 			adjustedPath:   "/anotherpath", | ||||||
|  | 			expectedName:   "afile.txt", | ||||||
|  | 			expectedExists: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:           "File does not exist in /anotherpath", | ||||||
|  | 			adjustedPath:   "/anotherpath", | ||||||
|  | 			expectedName:   "nonexistentfile.txt", | ||||||
|  | 			expectedExists: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:           "Directory does not exist", | ||||||
|  | 			adjustedPath:   "/nonexistentpath", | ||||||
|  | 			expectedName:   "", | ||||||
|  | 			expectedExists: false, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			fileInfo, _ := testIndex.GetMetadataInfo(tt.adjustedPath) | ||||||
|  | 			found := false | ||||||
|  | 			// Iterate over fileInfo.Items to look for expectedName
 | ||||||
|  | 			for _, item := range fileInfo.ReducedItems { | ||||||
|  | 				// Assert the existence and the name
 | ||||||
|  | 				if item.Name == tt.expectedName { | ||||||
|  | 					found = true | ||||||
|  | 					break | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			assert.Equal(t, tt.expectedExists, found) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| // Test for UpdateFileMetadata
 | // Test for UpdateFileMetadata
 | ||||||
| func TestUpdateFileMetadata(t *testing.T) { | func TestUpdateFileMetadata(t *testing.T) { | ||||||
| 	index := &Index{ | 	index := &Index{ | ||||||
| 		Directories: map[string]Directory{ | 		Directories: map[string]FileInfo{ | ||||||
| 			"/testpath": { | 			"/testpath": { | ||||||
| 				Metadata: map[string]FileInfo{ | 				Path:  "/testpath", | ||||||
| 					"testfile.txt":    {Name: "testfile.txt"}, | 				Name:  "testpath", | ||||||
| 					"anotherfile.txt": {Name: "anotherfile.txt"}, | 				IsDir: true, | ||||||
|  | 				ReducedItems: []ReducedItem{ | ||||||
|  | 					{Name: "testfile.txt"}, | ||||||
|  | 					{Name: "anotherfile.txt"}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
|  | @ -100,7 +126,7 @@ func TestUpdateFileMetadata(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	dir, exists := index.Directories["/testpath"] | 	dir, exists := index.Directories["/testpath"] | ||||||
| 	if !exists || dir.Metadata["testfile.txt"].Name != "testfile.txt" { | 	if !exists || dir.ReducedItems[0].Name != "testfile.txt" { | ||||||
| 		t.Fatalf("expected testfile.txt to be updated in the directory metadata") | 		t.Fatalf("expected testfile.txt to be updated in the directory metadata") | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -122,19 +148,29 @@ func TestGetDirMetadata(t *testing.T) { | ||||||
| // Test for SetDirectoryInfo
 | // Test for SetDirectoryInfo
 | ||||||
| func TestSetDirectoryInfo(t *testing.T) { | func TestSetDirectoryInfo(t *testing.T) { | ||||||
| 	index := &Index{ | 	index := &Index{ | ||||||
| 		Directories: map[string]Directory{ | 		Directories: map[string]FileInfo{ | ||||||
| 			"/testpath": { | 			"/testpath": { | ||||||
| 				Metadata: map[string]FileInfo{ | 				Path:  "/testpath", | ||||||
| 					"testfile.txt":    {Name: "testfile.txt"}, | 				Name:  "testpath", | ||||||
| 					"anotherfile.txt": {Name: "anotherfile.txt"}, | 				IsDir: true, | ||||||
|  | 				Items: []*FileInfo{ | ||||||
|  | 					{Name: "testfile.txt"}, | ||||||
|  | 					{Name: "anotherfile.txt"}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 	dir := Directory{Metadata: map[string]FileInfo{"testfile.txt": {Name: "testfile.txt"}}} | 	dir := FileInfo{ | ||||||
|  | 		Path:  "/newPath", | ||||||
|  | 		Name:  "newPath", | ||||||
|  | 		IsDir: true, | ||||||
|  | 		Items: []*FileInfo{ | ||||||
|  | 			{Name: "testfile.txt"}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
| 	index.SetDirectoryInfo("/newPath", dir) | 	index.SetDirectoryInfo("/newPath", dir) | ||||||
| 	storedDir, exists := index.Directories["/newPath"] | 	storedDir, exists := index.Directories["/newPath"] | ||||||
| 	if !exists || storedDir.Metadata["testfile.txt"].Name != "testfile.txt" { | 	if !exists || storedDir.Items[0].Name != "testfile.txt" { | ||||||
| 		t.Fatalf("expected SetDirectoryInfo to store directory info correctly") | 		t.Fatalf("expected SetDirectoryInfo to store directory info correctly") | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -143,7 +179,7 @@ func TestSetDirectoryInfo(t *testing.T) { | ||||||
| func TestGetDirectoryInfo(t *testing.T) { | func TestGetDirectoryInfo(t *testing.T) { | ||||||
| 	t.Parallel() | 	t.Parallel() | ||||||
| 	dir, exists := testIndex.GetDirectoryInfo("/testpath") | 	dir, exists := testIndex.GetDirectoryInfo("/testpath") | ||||||
| 	if !exists || dir.Metadata["testfile.txt"].Name != "testfile.txt" { | 	if !exists || dir.Items[0].Name != "testfile.txt" { | ||||||
| 		t.Fatalf("expected GetDirectoryInfo to return correct directory info") | 		t.Fatalf("expected GetDirectoryInfo to return correct directory info") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -156,7 +192,7 @@ func TestGetDirectoryInfo(t *testing.T) { | ||||||
| // Test for RemoveDirectory
 | // Test for RemoveDirectory
 | ||||||
| func TestRemoveDirectory(t *testing.T) { | func TestRemoveDirectory(t *testing.T) { | ||||||
| 	index := &Index{ | 	index := &Index{ | ||||||
| 		Directories: map[string]Directory{ | 		Directories: map[string]FileInfo{ | ||||||
| 			"/testpath": {}, | 			"/testpath": {}, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  | @ -194,27 +230,33 @@ func TestUpdateCount(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
| 	testIndex = Index{ | 	testIndex = Index{ | ||||||
|  | 		Root:       "/", | ||||||
| 		NumFiles:   10, | 		NumFiles:   10, | ||||||
| 		NumDirs:    5, | 		NumDirs:    5, | ||||||
| 		inProgress: false, | 		inProgress: false, | ||||||
| 		Directories: map[string]Directory{ | 		Directories: map[string]FileInfo{ | ||||||
| 			"/testpath": { | 			"/testpath": { | ||||||
| 				Metadata: map[string]FileInfo{ | 				Path:     "/testpath", | ||||||
| 					"testfile.txt":    {Name: "testfile.txt"}, | 				Name:     "testpath", | ||||||
| 					"anotherfile.txt": {Name: "anotherfile.txt"}, | 				IsDir:    true, | ||||||
|  | 				NumDirs:  1, | ||||||
|  | 				NumFiles: 2, | ||||||
|  | 				Items: []*FileInfo{ | ||||||
|  | 					{Name: "testfile.txt", Size: 100}, | ||||||
|  | 					{Name: "anotherfile.txt", Size: 100}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			"/anotherpath": { | 			"/anotherpath": { | ||||||
| 				Metadata: map[string]FileInfo{ | 				Path:     "/anotherpath", | ||||||
| 					"afile.txt": {Name: "afile.txt"}, | 				Name:     "anotherpath", | ||||||
|  | 				IsDir:    true, | ||||||
|  | 				NumDirs:  1, | ||||||
|  | 				NumFiles: 1, | ||||||
|  | 				Items: []*FileInfo{ | ||||||
|  | 					{Name: "directory", IsDir: true, Size: 100}, | ||||||
|  | 					{Name: "afile.txt", Size: 100}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	files := []fs.FileInfo{ |  | ||||||
| 		mockFileInfo{name: "file1.txt", isDir: false}, |  | ||||||
| 		mockFileInfo{name: "dir1", isDir: true}, |  | ||||||
| 	} |  | ||||||
| 	testIndex.UpdateQuickList(files) |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -15,11 +15,20 @@ type modifyRequest struct { | ||||||
| 	Which []string `json:"which"` // Answer to: which fields?
 | 	Which []string `json:"which"` // Answer to: which fields?
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | var ( | ||||||
|  | 	store     *storage.Storage | ||||||
|  | 	server    *settings.Server | ||||||
|  | 	fileCache FileCache | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func SetupEnv(storage *storage.Storage, s *settings.Server, cache FileCache) { | ||||||
|  | 	store = storage | ||||||
|  | 	server = s | ||||||
|  | 	fileCache = cache | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func NewHandler( | func NewHandler( | ||||||
| 	imgSvc ImgService, | 	imgSvc ImgService, | ||||||
| 	fileCache FileCache, |  | ||||||
| 	store *storage.Storage, |  | ||||||
| 	server *settings.Server, |  | ||||||
| 	assetsFs fs.FS, | 	assetsFs fs.FS, | ||||||
| ) (http.Handler, error) { | ) (http.Handler, error) { | ||||||
| 	server.Clean() | 	server.Clean() | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/gtsteffaniak/filebrowser/settings" | 	"github.com/gtsteffaniak/filebrowser/settings" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/share" | 	"github.com/gtsteffaniak/filebrowser/share" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/storage" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/storage/bolt" | 	"github.com/gtsteffaniak/filebrowser/storage/bolt" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/users" | 	"github.com/gtsteffaniak/filebrowser/users" | ||||||
| ) | ) | ||||||
|  | @ -73,8 +74,13 @@ func TestPublicShareHandlerAuthentication(t *testing.T) { | ||||||
| 						t.Errorf("failed to close db: %v", err) | 						t.Errorf("failed to close db: %v", err) | ||||||
| 					} | 					} | ||||||
| 				}) | 				}) | ||||||
| 
 | 				authStore, userStore, shareStore, settingsStore, err := bolt.NewStorage(db) | ||||||
| 				storage, err := bolt.NewStorage(db) | 				storage := &storage.Storage{ | ||||||
|  | 					Auth:     authStore, | ||||||
|  | 					Users:    userStore, | ||||||
|  | 					Share:    shareStore, | ||||||
|  | 					Settings: settingsStore, | ||||||
|  | 				} | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					t.Fatalf("failed to get storage: %v", err) | 					t.Fatalf("failed to get storage: %v", err) | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ package http | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"log" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"sort" | 	"sort" | ||||||
|  | @ -14,6 +13,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/gtsteffaniak/filebrowser/errors" | 	"github.com/gtsteffaniak/filebrowser/errors" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/files" | 	"github.com/gtsteffaniak/filebrowser/files" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/storage" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/users" | 	"github.com/gtsteffaniak/filebrowser/users" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -130,21 +130,7 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d * | ||||||
| 		return http.StatusBadRequest, errors.ErrEmptyPassword | 		return http.StatusBadRequest, errors.ErrEmptyPassword | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	newUser := users.ApplyDefaults(*req.Data) | 	err = storage.CreateUser(*req.Data, req.Data.Perm.Admin) | ||||||
| 
 |  | ||||||
| 	userHome, err := d.settings.MakeUserDir(req.Data.Username, req.Data.Scope, d.server.Root) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Printf("create user: failed to mkdir user home dir: [%s]", userHome) |  | ||||||
| 		return http.StatusInternalServerError, err |  | ||||||
| 	} |  | ||||||
| 	newUser.Scope = userHome |  | ||||||
| 	log.Printf("user: %s, home dir: [%s].", req.Data.Username, userHome) |  | ||||||
| 	_, _, err = files.GetRealPath(d.server.Root, req.Data.Scope) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Println("user path is not valid", req.Data.Scope) |  | ||||||
| 		return http.StatusBadRequest, nil |  | ||||||
| 	} |  | ||||||
| 	err = d.store.Users.Save(&newUser) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return http.StatusInternalServerError, err | 		return http.StatusInternalServerError, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -34,15 +34,14 @@ func loadConfigFile(configFile string) []byte { | ||||||
| 	// Open and read the YAML file
 | 	// Open and read the YAML file
 | ||||||
| 	yamlFile, err := os.Open(configFile) | 	yamlFile, err := os.Open(configFile) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Printf("ERROR: opening config file\n %v\n WARNING: Using default config only\n If this was a mistake, please make sure the file exists and is accessible by the filebrowser binary.\n\n", err) | 		log.Println(err) | ||||||
| 		Config = setDefaults() | 		os.Exit(1) | ||||||
| 		return []byte{} |  | ||||||
| 	} | 	} | ||||||
| 	defer yamlFile.Close() | 	defer yamlFile.Close() | ||||||
| 
 | 
 | ||||||
| 	stat, err := yamlFile.Stat() | 	stat, err := yamlFile.Stat() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatalf("Error getting file information: %s", err.Error()) | 		log.Fatalf("error getting file information: %s", err.Error()) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	yamlData := make([]byte, stat.Size()) | 	yamlData := make([]byte, stat.Size()) | ||||||
|  |  | ||||||
|  | @ -39,3 +39,15 @@ func GenerateKey() ([]byte, error) { | ||||||
| func GetSettingsConfig(nameType string, Value string) string { | func GetSettingsConfig(nameType string, Value string) string { | ||||||
| 	return nameType + Value | 	return nameType + Value | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func AdminPerms() Permissions { | ||||||
|  | 	return Permissions{ | ||||||
|  | 		Create:   true, | ||||||
|  | 		Rename:   true, | ||||||
|  | 		Modify:   true, | ||||||
|  | 		Delete:   true, | ||||||
|  | 		Share:    true, | ||||||
|  | 		Download: true, | ||||||
|  | 		Admin:    true, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -28,5 +28,5 @@ func (s authBackend) Get(t string) (auth.Auther, error) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s authBackend) Save(a auth.Auther) error { | func (s authBackend) Save(a auth.Auther) error { | ||||||
| 	return save(s.db, "auther", a) | 	return Save(s.db, "auther", a) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -6,26 +6,14 @@ import ( | ||||||
| 	"github.com/gtsteffaniak/filebrowser/auth" | 	"github.com/gtsteffaniak/filebrowser/auth" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/settings" | 	"github.com/gtsteffaniak/filebrowser/settings" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/share" | 	"github.com/gtsteffaniak/filebrowser/share" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/storage" |  | ||||||
| 	"github.com/gtsteffaniak/filebrowser/users" | 	"github.com/gtsteffaniak/filebrowser/users" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // NewStorage creates a storage.Storage based on Bolt DB.
 | // NewStorage creates a storage.Storage based on Bolt DB.
 | ||||||
| func NewStorage(db *storm.DB) (*storage.Storage, error) { | func NewStorage(db *storm.DB) (*auth.Storage, *users.Storage, *share.Storage, *settings.Storage, error) { | ||||||
| 	userStore := users.NewStorage(usersBackend{db: db}) | 	userStore := users.NewStorage(usersBackend{db: db}) | ||||||
| 	shareStore := share.NewStorage(shareBackend{db: db}) | 	shareStore := share.NewStorage(shareBackend{db: db}) | ||||||
| 	settingsStore := settings.NewStorage(settingsBackend{db: db}) | 	settingsStore := settings.NewStorage(settingsBackend{db: db}) | ||||||
| 	authStore := auth.NewStorage(authBackend{db: db}, userStore) | 	authStore := auth.NewStorage(authBackend{db: db}, userStore) | ||||||
| 
 | 	return authStore, userStore, shareStore, settingsStore, nil | ||||||
| 	err := save(db, "version", 2) //nolint:gomnd
 |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return &storage.Storage{ |  | ||||||
| 		Auth:     authStore, |  | ||||||
| 		Users:    userStore, |  | ||||||
| 		Share:    shareStore, |  | ||||||
| 		Settings: settingsStore, |  | ||||||
| 	}, nil |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ func (s settingsBackend) Get() (*settings.Settings, error) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s settingsBackend) Save(set *settings.Settings) error { | func (s settingsBackend) Save(set *settings.Settings) error { | ||||||
| 	return save(s.db, "settings", set) | 	return Save(s.db, "settings", set) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s settingsBackend) GetServer() (*settings.Server, error) { | func (s settingsBackend) GetServer() (*settings.Server, error) { | ||||||
|  | @ -27,5 +27,5 @@ func (s settingsBackend) GetServer() (*settings.Server, error) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s settingsBackend) SaveServer(server *settings.Server) error { | func (s settingsBackend) SaveServer(server *settings.Server) error { | ||||||
| 	return save(s.db, "server", server) | 	return Save(s.db, "server", server) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -15,6 +15,6 @@ func get(db *storm.DB, name string, to interface{}) error { | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func save(db *storm.DB, name string, from interface{}) error { | func Save(db *storm.DB, name string, from interface{}) error { | ||||||
| 	return db.Set("config", name, from) | 	return db.Set("config", name, from) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,10 +1,20 @@ | ||||||
| package storage | package storage | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"log" | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 
 | ||||||
|  | 	"github.com/asdine/storm/v3" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/auth" | 	"github.com/gtsteffaniak/filebrowser/auth" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/errors" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/files" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/settings" | 	"github.com/gtsteffaniak/filebrowser/settings" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/share" | 	"github.com/gtsteffaniak/filebrowser/share" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/storage/bolt" | ||||||
| 	"github.com/gtsteffaniak/filebrowser/users" | 	"github.com/gtsteffaniak/filebrowser/users" | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/utils" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Storage is a storage powered by a Backend which makes the necessary
 | // Storage is a storage powered by a Backend which makes the necessary
 | ||||||
|  | @ -15,3 +25,112 @@ type Storage struct { | ||||||
| 	Auth     *auth.Storage | 	Auth     *auth.Storage | ||||||
| 	Settings *settings.Storage | 	Settings *settings.Storage | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | var store *Storage | ||||||
|  | 
 | ||||||
|  | func InitializeDb(path string) (*Storage, bool, error) { | ||||||
|  | 	exists, err := dbExists(path) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	db, err := storm.Open(path) | ||||||
|  | 
 | ||||||
|  | 	utils.CheckErr(fmt.Sprintf("storm.Open path %v", path), err) | ||||||
|  | 	authStore, userStore, shareStore, settingsStore, err := bolt.NewStorage(db) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, exists, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err = bolt.Save(db, "version", 2) //nolint:gomnd
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, exists, err | ||||||
|  | 	} | ||||||
|  | 	store = &Storage{ | ||||||
|  | 		Auth:     authStore, | ||||||
|  | 		Users:    userStore, | ||||||
|  | 		Share:    shareStore, | ||||||
|  | 		Settings: settingsStore, | ||||||
|  | 	} | ||||||
|  | 	if !exists { | ||||||
|  | 		quickSetup(store) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return store, exists, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func dbExists(path string) (bool, error) { | ||||||
|  | 	stat, err := os.Stat(path) | ||||||
|  | 	if err == nil { | ||||||
|  | 		return stat.Size() != 0, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if os.IsNotExist(err) { | ||||||
|  | 		d := filepath.Dir(path) | ||||||
|  | 		_, err = os.Stat(d) | ||||||
|  | 		if os.IsNotExist(err) { | ||||||
|  | 			if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet,gomnd
 | ||||||
|  | 				return false, err | ||||||
|  | 			} | ||||||
|  | 			return false, nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return false, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func quickSetup(store *Storage) { | ||||||
|  | 	settings.Config.Auth.Key = utils.GenerateKey() | ||||||
|  | 	if settings.Config.Auth.Method == "noauth" { | ||||||
|  | 		err := store.Auth.Save(&auth.NoAuth{}) | ||||||
|  | 		utils.CheckErr("store.Auth.Save", err) | ||||||
|  | 	} else { | ||||||
|  | 		settings.Config.Auth.Method = "password" | ||||||
|  | 		err := store.Auth.Save(&auth.JSONAuth{}) | ||||||
|  | 		utils.CheckErr("store.Auth.Save", err) | ||||||
|  | 	} | ||||||
|  | 	err := store.Settings.Save(&settings.Config) | ||||||
|  | 	utils.CheckErr("store.Settings.Save", err) | ||||||
|  | 	err = store.Settings.SaveServer(&settings.Config.Server) | ||||||
|  | 	utils.CheckErr("store.Settings.SaveServer", err) | ||||||
|  | 	user := users.ApplyDefaults(users.User{}) | ||||||
|  | 	user.Username = settings.Config.Auth.AdminUsername | ||||||
|  | 	user.Password = settings.Config.Auth.AdminPassword | ||||||
|  | 	user.Perm.Admin = true | ||||||
|  | 	user.Scope = "./" | ||||||
|  | 	user.DarkMode = true | ||||||
|  | 	user.ViewMode = "normal" | ||||||
|  | 	user.LockPassword = false | ||||||
|  | 	user.Perm = settings.AdminPerms() | ||||||
|  | 	err = store.Users.Save(&user) | ||||||
|  | 	utils.CheckErr("store.Users.Save", err) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // create new user
 | ||||||
|  | func CreateUser(userInfo users.User, asAdmin bool) error { | ||||||
|  | 	// must have username or password to create
 | ||||||
|  | 	if userInfo.Username == "" || userInfo.Password == "" { | ||||||
|  | 		return errors.ErrInvalidRequestParams | ||||||
|  | 	} | ||||||
|  | 	newUser := users.ApplyDefaults(userInfo) | ||||||
|  | 	if asAdmin { | ||||||
|  | 		newUser.Perm = settings.AdminPerms() | ||||||
|  | 	} | ||||||
|  | 	// create new home directory
 | ||||||
|  | 	userHome, err := settings.Config.MakeUserDir(newUser.Username, newUser.Scope, settings.Config.Server.Root) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Printf("create user: failed to mkdir user home dir: [%s]", userHome) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	newUser.Scope = userHome | ||||||
|  | 	log.Printf("user: %s, home dir: [%s].", newUser.Username, userHome) | ||||||
|  | 	_, _, err = files.GetRealPath(settings.Config.Server.Root, newUser.Scope) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Println("user path is not valid", newUser.Scope) | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	err = store.Users.Save(&newUser) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,19 @@ | ||||||
|  | package utils | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"log" | ||||||
|  | 
 | ||||||
|  | 	"github.com/gtsteffaniak/filebrowser/settings" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func CheckErr(source string, err error) { | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatalf("%s: %v", source, err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func GenerateKey() []byte { | ||||||
|  | 	k, err := settings.GenerateKey() | ||||||
|  | 	CheckErr("generateKey", err) | ||||||
|  | 	return k | ||||||
|  | } | ||||||
|  | @ -0,0 +1,2 @@ | ||||||
|  | # Contributing Guide | ||||||
|  | 
 | ||||||
|  | @ -0,0 +1,2 @@ | ||||||
|  | # Getting Started using FileBrowser Quantum | ||||||
|  | 
 | ||||||
|  | @ -0,0 +1,22 @@ | ||||||
|  | # Migration help | ||||||
|  | 
 | ||||||
|  | It is possible to use the same database as used by filebrowser/filebrowser,  | ||||||
|  | but you will need to follow the following process: | ||||||
|  | 
 | ||||||
|  | 1. Create a configuration file as mentioned above. | ||||||
|  | 2. Copy your database file from the original filebrowser to the path of | ||||||
|  |    the new one. | ||||||
|  | 3. Update the configuration file to use the database (under server in | ||||||
|  |    filebrowser.yml) | ||||||
|  | 4. If you are using docker, update the docker-compose file or docker run | ||||||
|  |    command to use the config file as described in the install section | ||||||
|  |    above. | ||||||
|  | 5. If you are not using docker, just make sure you run filebrowser -c | ||||||
|  |    filebrowser.yml and have a valid filebrowser config. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Note: share links will not work and will need to be re-created after migration. | ||||||
|  | 
 | ||||||
|  | The filebrowser Quantum application should run with the same user and rules that | ||||||
|  | you have from the original. But keep in mind the differences that may not work  | ||||||
|  | the same way, but all user configuration should be available. | ||||||
|  | @ -0,0 +1,24 @@ | ||||||
|  | # Planned Roadmap | ||||||
|  | 
 | ||||||
|  | upcoming 0.2.x releases: | ||||||
|  | 
 | ||||||
|  | - Replace http routes for gorilla/mux with stdlib | ||||||
|  | - Theme configuration from settings | ||||||
|  | - File syncronization improvements | ||||||
|  | - more filetype previews | ||||||
|  | 
 | ||||||
|  | next major 0.3.0 release : | ||||||
|  | 
 | ||||||
|  | - multiple sources https://github.com/filebrowser/filebrowser/issues/2514 | ||||||
|  | - introduce jobs as replacement to runners. | ||||||
|  | - Add Job status to the sidebar | ||||||
|  |   - index status. | ||||||
|  |   - Job status from users | ||||||
|  |   - upload status | ||||||
|  | 
 | ||||||
|  | Unplanned Future releases: | ||||||
|  |   - Add tools to sidebar | ||||||
|  |     - duplicate file detector. | ||||||
|  |     - bulk rename https://github.com/filebrowser/filebrowser/issues/2473 | ||||||
|  |     - metrics tracker - user access, file access, download count, last login, etc | ||||||
|  |   - support minio, s3, and backblaze sources https://github.com/filebrowser/filebrowser/issues/2544 | ||||||
|  | @ -19,18 +19,20 @@ | ||||||
|       <input |       <input | ||||||
|         v-model="gallerySize" |         v-model="gallerySize" | ||||||
|         type="range" |         type="range" | ||||||
|         id="gallary-size" |         id="gallery-size" | ||||||
|         name="gallary-size" |         name="gallery-size" | ||||||
|         :value="gallerySize" |         :value="gallerySize" | ||||||
|         min="0" |         min="0" | ||||||
|         max="10" |         max="10" | ||||||
|  |         @input="updateGallerySize" | ||||||
|  |         @change="commitGallerySize" | ||||||
|       /> |       /> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script> | <script> | ||||||
| import { state, mutations, getters } from "@/store"; // Import mutations as well | import { state, mutations, getters } from "@/store"; | ||||||
| import Action from "@/components/Action.vue"; | import Action from "@/components/Action.vue"; | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|  | @ -43,12 +45,6 @@ export default { | ||||||
|       gallerySize: state.user.gallerySize, |       gallerySize: state.user.gallerySize, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   watch: { |  | ||||||
|     gallerySize(newValue) { |  | ||||||
|       this.gallerySize = parseInt(newValue, 0); // Update the user object |  | ||||||
|       mutations.setGallerySize(this.gallerySize); |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   props: ["base", "noLink"], |   props: ["base", "noLink"], | ||||||
|   computed: { |   computed: { | ||||||
|     isCardView() { |     isCardView() { | ||||||
|  | @ -100,13 +96,16 @@ export default { | ||||||
|       return "router-link"; |       return "router-link"; | ||||||
|     }, |     }, | ||||||
|     showShare() { |     showShare() { | ||||||
|       // Ensure user properties are accessed safely |       return state.user?.perm && state.user?.perm.share; | ||||||
|       if (state.route.path.startsWith("/share")) { |     }, | ||||||
|         return false; |   }, | ||||||
|       } |   methods: { | ||||||
|       return state.user?.perm && state.user?.perm.share; // Access from state directly |     updateGallerySize(event) { | ||||||
|  |       this.gallerySize = parseInt(event.target.value, 10); | ||||||
|  |     }, | ||||||
|  |     commitGallerySize() { | ||||||
|  |       mutations.setGallerySize(this.gallerySize); | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   methods: { }, |  | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -166,10 +166,6 @@ | ||||||
|             <b>Multiple Search terms:</b> Additional terms separated by <code>|</code>, |             <b>Multiple Search terms:</b> Additional terms separated by <code>|</code>, | ||||||
|             for example <code>"test|not"</code> searches for both terms independently. |             for example <code>"test|not"</code> searches for both terms independently. | ||||||
|           </p> |           </p> | ||||||
|           <p> |  | ||||||
|             <b>File size:</b> Searching files by size may have significantly longer search |  | ||||||
|             times. |  | ||||||
|           </p> |  | ||||||
|         </div> |         </div> | ||||||
|         <!-- List of search results --> |         <!-- List of search results --> | ||||||
|         <ul v-show="results.length > 0"> |         <ul v-show="results.length > 0"> | ||||||
|  | @ -311,6 +307,9 @@ export default { | ||||||
|       path = path.slice(1); |       path = path.slice(1); | ||||||
|       path = "./" + path.substring(path.indexOf("/") + 1); |       path = "./" + path.substring(path.indexOf("/") + 1); | ||||||
|       path = path.replace(/\/+$/, "") + "/"; |       path = path.replace(/\/+$/, "") + "/"; | ||||||
|  |       if (path == "./files/") { | ||||||
|  |         path = "./"; | ||||||
|  |       } | ||||||
|       return path; |       return path; | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  | @ -391,10 +390,10 @@ export default { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|       let searchTypesFull = this.searchTypes; |       let searchTypesFull = this.searchTypes; | ||||||
|       if (this.largerThan != "") { |       if (this.largerThan != "" && !this.isTypeSelectDisabled) { | ||||||
|         searchTypesFull = searchTypesFull + "type:largerThan=" + this.largerThan + " "; |         searchTypesFull = searchTypesFull + "type:largerThan=" + this.largerThan + " "; | ||||||
|       } |       } | ||||||
|       if (this.smallerThan != "") { |       if (this.smallerThan != "" && !this.isTypeSelectDisabled) { | ||||||
|         searchTypesFull = searchTypesFull + "type:smallerThan=" + this.smallerThan + " "; |         searchTypesFull = searchTypesFull + "type:smallerThan=" + this.smallerThan + " "; | ||||||
|       } |       } | ||||||
|       let path = state.route.path; |       let path = state.route.path; | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| <template> | <template> | ||||||
|   <component |   <component | ||||||
|     :is="isSelected || user.singleClick ? 'a' : 'div'" |     :is="quickNav ? 'a' : 'div'" | ||||||
|     :href="isSelected || user.singleClick ? url : undefined" |     :href="quickNav ? url : undefined" | ||||||
|     :class="{ |     :class="{ | ||||||
|       item: true, |       item: true, | ||||||
|       activebutton: isMaximized && isSelected, |       activebutton: isMaximized && isSelected, | ||||||
|  | @ -16,7 +16,7 @@ | ||||||
|     :data-type="type" |     :data-type="type" | ||||||
|     :aria-label="name" |     :aria-label="name" | ||||||
|     :aria-selected="isSelected" |     :aria-selected="isSelected" | ||||||
|     @click="isSelected || user.singleClick ? toggleClick() : itemClick($event)" |     @click="quickNav ? toggleClick() : itemClick($event)" | ||||||
|   > |   > | ||||||
|     <div @click="toggleClick" :class="{ activetitle: isMaximized && isSelected }"> |     <div @click="toggleClick" :class="{ activetitle: isMaximized && isSelected }"> | ||||||
|       <img |       <img | ||||||
|  | @ -34,8 +34,7 @@ | ||||||
| 
 | 
 | ||||||
|     <div class="text" :class="{ activecontent: isMaximized && isSelected }"> |     <div class="text" :class="{ activecontent: isMaximized && isSelected }"> | ||||||
|       <p class="name">{{ name }}</p> |       <p class="name">{{ name }}</p> | ||||||
|       <p v-if="isDir" class="size" data-order="-1">—</p> |       <p class="size" :data-order="humanSize()">{{ humanSize() }}</p> | ||||||
|       <p v-else class="size" :data-order="humanSize()">{{ humanSize() }}</p> |  | ||||||
|       <p class="modified"> |       <p class="modified"> | ||||||
|         <time :datetime="modified">{{ humanTime() }}</time> |         <time :datetime="modified">{{ humanTime() }}</time> | ||||||
|       </p> |       </p> | ||||||
|  | @ -93,6 +92,9 @@ export default { | ||||||
|     "path", |     "path", | ||||||
|   ], |   ], | ||||||
|   computed: { |   computed: { | ||||||
|  |     quickNav() { | ||||||
|  |       return state.user.singleClick && !state.multiple; | ||||||
|  |     }, | ||||||
|     user() { |     user() { | ||||||
|       return state.user; |       return state.user; | ||||||
|     }, |     }, | ||||||
|  | @ -263,6 +265,7 @@ export default { | ||||||
|       action(overwrite, rename); |       action(overwrite, rename); | ||||||
|     }, |     }, | ||||||
|     itemClick(event) { |     itemClick(event) { | ||||||
|  |       console.log("should say something"); | ||||||
|       if (this.singleClick && !state.multiple) this.open(); |       if (this.singleClick && !state.multiple) this.open(); | ||||||
|       else this.click(event); |       else this.click(event); | ||||||
|     }, |     }, | ||||||
|  | @ -271,7 +274,7 @@ export default { | ||||||
| 
 | 
 | ||||||
|       setTimeout(() => { |       setTimeout(() => { | ||||||
|         this.touches = 0; |         this.touches = 0; | ||||||
|       }, 300); |       }, 500); | ||||||
| 
 | 
 | ||||||
|       this.touches++; |       this.touches++; | ||||||
|       if (this.touches > 1) { |       if (this.touches > 1) { | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ export const mutations = { | ||||||
|   setGallerySize: (value) => { |   setGallerySize: (value) => { | ||||||
|     state.user.gallerySize = value |     state.user.gallerySize = value | ||||||
|     emitStateChanged(); |     emitStateChanged(); | ||||||
|  |     users.update(state.user,['gallerySize']); | ||||||
|   }, |   }, | ||||||
|   setActiveSettingsView: (value) => { |   setActiveSettingsView: (value) => { | ||||||
|     state.activeSettingsView = value; |     state.activeSettingsView = value; | ||||||
|  | @ -195,19 +196,20 @@ export const mutations = { | ||||||
|     emitStateChanged(); |     emitStateChanged(); | ||||||
|   }, |   }, | ||||||
|   setRoute: (value) => { |   setRoute: (value) => { | ||||||
|  |     console.log("going...",value) | ||||||
|     state.route = value; |     state.route = value; | ||||||
|     emitStateChanged(); |     emitStateChanged(); | ||||||
|   }, |   }, | ||||||
|   updateListingSortConfig: ({ field, asc }) => { |   updateListingSortConfig: ({ field, asc }) => { | ||||||
|     state.req.sorting.by = field; |     state.user.sorting.by = field; | ||||||
|     state.req.sorting.asc = asc; |     state.user.sorting.asc = asc; | ||||||
|     emitStateChanged(); |     emitStateChanged(); | ||||||
|   }, |   }, | ||||||
|   updateListingItems: () => { |   updateListingItems: () => { | ||||||
|     state.req.items.sort((a, b) => { |     state.req.items.sort((a, b) => { | ||||||
|       const valueA = a[state.req.sorting.by]; |       const valueA = a[state.user.sorting.by]; | ||||||
|       const valueB = b[state.req.sorting.by]; |       const valueB = b[state.user.sorting.by]; | ||||||
|       if (state.req.sorting.asc) { |       if (state.user.sorting.asc) { | ||||||
|         return valueA > valueB ? 1 : -1; |         return valueA > valueB ? 1 : -1; | ||||||
|       } else { |       } else { | ||||||
|         return valueA < valueB ? 1 : -1; |         return valueA < valueB ? 1 : -1; | ||||||
|  |  | ||||||
|  | @ -7,24 +7,24 @@ export function getHumanReadableFilesize(fileSizeBytes) { | ||||||
|     switch (true) { |     switch (true) { | ||||||
|         case fileSizeBytes < 1024: |         case fileSizeBytes < 1024: | ||||||
|             break; |             break; | ||||||
|         case fileSizeBytes < 1000 ** 2:  // 1 KB - 1 MB
 |         case fileSizeBytes < 1024 ** 2:  // 1 KB - 1 MB
 | ||||||
|             size = fileSizeBytes / 1000; |             size = fileSizeBytes / 1024; | ||||||
|             unit = 'KB'; |             unit = 'KB'; | ||||||
|             break; |             break; | ||||||
|         case fileSizeBytes < 1000 ** 3:  // 1 MB - 1 GB
 |         case fileSizeBytes < 1024 ** 3:  // 1 MB - 1 GB
 | ||||||
|             size = fileSizeBytes / (1000 ** 2); |             size = fileSizeBytes / (1024 ** 2); | ||||||
|             unit = 'MB'; |             unit = 'MB'; | ||||||
|             break; |             break; | ||||||
|         case fileSizeBytes < 1000 ** 4:  // 1 GB - 1 TB
 |         case fileSizeBytes < 1024 ** 4:  // 1 GB - 1 TB
 | ||||||
|             size = fileSizeBytes / (1000 ** 3); |             size = fileSizeBytes / (1024 ** 3); | ||||||
|             unit = 'GB'; |             unit = 'GB'; | ||||||
|             break; |             break; | ||||||
|         case fileSizeBytes < 1000 ** 5:  // 1 TB - 1 PB
 |         case fileSizeBytes < 1024 ** 5:  // 1 TB - 1 PB
 | ||||||
|             size = fileSizeBytes / (1000 ** 4); |             size = fileSizeBytes / (1024 ** 4); | ||||||
|             unit = 'TB'; |             unit = 'TB'; | ||||||
|             break; |             break; | ||||||
|         default:  // >= 1 PB
 |         default:  // >= 1 PB
 | ||||||
|             size = fileSizeBytes / (1000 ** 5); |             size = fileSizeBytes / (1024 ** 5); | ||||||
|             unit = 'PB'; |             unit = 'PB'; | ||||||
|             break; |             break; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -51,13 +51,13 @@ export default { | ||||||
|       return state.selected; |       return state.selected; | ||||||
|     }, |     }, | ||||||
|     nameSorted() { |     nameSorted() { | ||||||
|       return state.req.sorting.by === "name"; |       return state.user.sorting.by === "name"; | ||||||
|     }, |     }, | ||||||
|     sizeSorted() { |     sizeSorted() { | ||||||
|       return state.req.sorting.by === "size"; |       return state.user.sorting.by === "size"; | ||||||
|     }, |     }, | ||||||
|     modifiedSorted() { |     modifiedSorted() { | ||||||
|       return state.req.sorting.by === "modified"; |       return state.user.sorting.by === "modified"; | ||||||
|     }, |     }, | ||||||
|     ascOrdered() { |     ascOrdered() { | ||||||
|       return state.req.sorting.asc; |       return state.req.sorting.asc; | ||||||
|  | @ -297,7 +297,7 @@ export default { | ||||||
|       const currentIndex = this.viewModes.indexOf(state.user.viewMode); |       const currentIndex = this.viewModes.indexOf(state.user.viewMode); | ||||||
|       const nextIndex = (currentIndex + 1) % this.viewModes.length; |       const nextIndex = (currentIndex + 1) % this.viewModes.length; | ||||||
|       const newView = this.viewModes[nextIndex]; |       const newView = this.viewModes[nextIndex]; | ||||||
|       mutations.updateCurrentUser({ "viewMode": newView }); |       mutations.updateCurrentUser({ viewMode: newView }); | ||||||
|     }, |     }, | ||||||
|     preventDefault(event) { |     preventDefault(event) { | ||||||
|       // Wrapper around prevent default. |       // Wrapper around prevent default. | ||||||
|  |  | ||||||
|  | @ -207,16 +207,16 @@ export default { | ||||||
|       return state.multiple; |       return state.multiple; | ||||||
|     }, |     }, | ||||||
|     nameSorted() { |     nameSorted() { | ||||||
|       return state.req.sorting.by === "name"; |       return state.user.sorting.by === "name"; | ||||||
|     }, |     }, | ||||||
|     sizeSorted() { |     sizeSorted() { | ||||||
|       return state.req.sorting.by === "size"; |       return state.user.sorting.by === "size"; | ||||||
|     }, |     }, | ||||||
|     modifiedSorted() { |     modifiedSorted() { | ||||||
|       return state.req.sorting.by === "modified"; |       return state.user.sorting.by === "modified"; | ||||||
|     }, |     }, | ||||||
|     ascOrdered() { |     ascOrdered() { | ||||||
|       return state.req.sorting.asc; |       return state.user.sorting.asc; | ||||||
|     }, |     }, | ||||||
|     items() { |     items() { | ||||||
|       return getters.reqItems(); |       return getters.reqItems(); | ||||||
|  | @ -443,7 +443,7 @@ export default { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|       if (noModifierKeys && getters.currentPromptName() != null) { |       if (noModifierKeys && getters.currentPromptName() != null) { | ||||||
|         return |         return; | ||||||
|       } |       } | ||||||
|       // Handle the space bar key |       // Handle the space bar key | ||||||
|       if (key === " ") { |       if (key === " ") { | ||||||
|  |  | ||||||
							
								
								
									
										27
									
								
								roadmap.md
								
								
								
								
							
							
						
						
									
										27
									
								
								roadmap.md
								
								
								
								
							|  | @ -1,27 +0,0 @@ | ||||||
| # Planned Roadmap |  | ||||||
| 
 |  | ||||||
| next 0.2.x release: |  | ||||||
| 
 |  | ||||||
| - Theme configuration from settings |  | ||||||
| - File syncronization improvements |  | ||||||
| - right-click context menu |  | ||||||
| 
 |  | ||||||
| initial 0.3.0 release : |  | ||||||
| 
 |  | ||||||
| - database changes |  | ||||||
| - introduce jobs as replacement to runners. |  | ||||||
| - Add Job status to the sidebar |  | ||||||
|   - index status. |  | ||||||
|   - Job status from users |  | ||||||
|   - upload status |  | ||||||
| 
 |  | ||||||
| Future releases: |  | ||||||
|   - Replace http routes for gorilla/mux with pocketbase |  | ||||||
|   - Allow multiple volumes to show up in the same filebrowser container. https://github.com/filebrowser/filebrowser/issues/2514 |  | ||||||
|   - enable/disable indexing for certain mounts |  | ||||||
|   - Add tools to sidebar |  | ||||||
|     - duplicate file detector. |  | ||||||
|     - bulk rename https://github.com/filebrowser/filebrowser/issues/2473 |  | ||||||
|     - job manager - folder sync, copy, lifecycle operations |  | ||||||
|     - metrics tracker - user access, file access, download count, last login, etc |  | ||||||
|   - support minio s3 and backblaze sources https://github.com/filebrowser/filebrowser/issues/2544 |  | ||||||
		Loading…
	
		Reference in New Issue