diff --git a/.gitignore b/.gitignore index 529ea53e..66f2408f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ _old rice-box.go .idea/ /backend/backend +/backend/filebrowser +/backend/filebrowser.exe /backend/backend.exe /frontend/dist /frontend/pkg diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d82b150..70cafc7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ 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.4.1-beta + + **New Features** + - right-click actions are available on search. https://github.com/gtsteffaniak/filebrowser/issues/273 + + **Notes** + - delete prompt now lists all items that will be affected by delete + - Debug and logger output tweaks. + + **Bugfixes**: + - calculating checksums errors. + - copy/move issues for some circumstances. + - The previous position wasn't returned when closing a preview window https://github.com/gtsteffaniak/filebrowser/issues/298 + - fixed sources configuration mapping error (advanced `server.sources` config) + ## v0.4.0-beta **New Features** diff --git a/backend/cmd/root.go b/backend/cmd/root.go index a7add1ef..94ba3c6f 100644 --- a/backend/cmd/root.go +++ b/backend/cmd/root.go @@ -130,7 +130,6 @@ Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v logger.Debug(fmt.Sprintf("Embeded frontend : %v", os.Getenv("FILEBROWSER_NO_EMBEDED") != "true")) logger.Info(database) logger.Info(fmt.Sprintf("Sources : %v", sources)) - serverConfig := settings.Config.Server swagInfo := docs.SwaggerInfo swagInfo.BasePath = serverConfig.BaseURL diff --git a/backend/filebrowser b/backend/filebrowser deleted file mode 100755 index 8f78d299..00000000 Binary files a/backend/filebrowser and /dev/null differ diff --git a/backend/filebrowser-playwright.yaml b/backend/filebrowser-playwright.yaml index 3a2d7d16..a4784372 100644 --- a/backend/filebrowser-playwright.yaml +++ b/backend/filebrowser-playwright.yaml @@ -5,6 +5,13 @@ server: auth: method: password signup: false +frontend: + name: "Graham's Filebrowser" + disableDefaultLinks: true + externalLinks: + - text: "A playwright test" + url: "https://playwright.dev/" + title: "Playwright" userDefaults: darkMode: true disableSettings: false diff --git a/backend/files/file.go b/backend/files/file.go index a6996119..1d9dfb27 100644 --- a/backend/files/file.go +++ b/backend/files/file.go @@ -208,11 +208,11 @@ func MoveResource(source, realsrc, realdst string, isSrcDir bool) error { index := GetIndex(source) // refresh info for source and dest err = index.RefreshFileInfo(FileOptions{ - Path: filepath.Dir(realsrc), + Path: realsrc, IsDir: isSrcDir, }) if err != nil { - return errors.ErrEmptyKey + return fmt.Errorf("could not refresh index for source: %v", err) } refreshConfig := FileOptions{Path: realdst, IsDir: true} if !isSrcDir { @@ -220,7 +220,7 @@ func MoveResource(source, realsrc, realdst string, isSrcDir bool) error { } err = index.RefreshFileInfo(refreshConfig) if err != nil { - return errors.ErrEmptyKey + return fmt.Errorf("could not refresh index for dest: %v", err) } return nil } diff --git a/backend/files/indexingSchedule.go b/backend/files/indexingSchedule.go index fea5839e..16a6aceb 100644 --- a/backend/files/indexingSchedule.go +++ b/backend/files/indexingSchedule.go @@ -34,7 +34,7 @@ func (idx *Index) newScanner(origin string) { } // Log and sleep before indexing - logger.Debug(fmt.Sprintf("Next scan in %v\n", sleepTime)) + logger.Debug(fmt.Sprintf("Next scan in %v", sleepTime)) time.Sleep(sleepTime) idx.scannerMu.Lock() diff --git a/backend/fileutils/file.go b/backend/fileutils/file.go index e077093b..bd89a644 100644 --- a/backend/fileutils/file.go +++ b/backend/fileutils/file.go @@ -1,6 +1,7 @@ package fileutils import ( + "fmt" "io" "os" "path" @@ -11,15 +12,21 @@ import ( // By default, the rename system call is used. If src and dst point to different volumes, // the file copy is used as a fallback. func MoveFile(src, dst string) error { + fmt.Println("moving", src, dst) if os.Rename(src, dst) == nil { return nil } + fmt.Println("copyfile instead", src, dst) + // fallback err := CopyFile(src, dst) if err != nil { + fmt.Println("ok it errored too", err) + _ = os.Remove(dst) return err } + fmt.Println("removing", src) if err := os.Remove(src); err != nil { return err } diff --git a/backend/http/middleware.go b/backend/http/middleware.go index 62ad2369..6719977b 100644 --- a/backend/http/middleware.go +++ b/backend/http/middleware.go @@ -108,7 +108,6 @@ func withUserHelper(fn handleFunc) handleFunc { } tokenString, err := extractToken(r) if err != nil { - logger.Debug(fmt.Sprintf("error extracting from request %v", err)) return http.StatusUnauthorized, err } data.token = tokenString diff --git a/backend/http/resource.go b/backend/http/resource.go index 7c65d6c6..ff2a999b 100644 --- a/backend/http/resource.go +++ b/backend/http/resource.go @@ -16,6 +16,7 @@ import ( "github.com/gtsteffaniak/filebrowser/backend/cache" "github.com/gtsteffaniak/filebrowser/backend/errors" "github.com/gtsteffaniak/filebrowser/backend/files" + "github.com/gtsteffaniak/filebrowser/backend/logger" ) // resourceGetHandler retrieves information about a resource. @@ -60,7 +61,9 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex return renderJSON(w, r, fileInfo) } if algo := r.URL.Query().Get("checksum"); algo != "" { - checksums, err := files.GetChecksum(fileInfo.Path, algo) + idx := files.GetIndex(source) + realPath, _, _ := idx.GetRealPath(d.user.Scope, path) + checksums, err := files.GetChecksum(realPath, algo) if err == errors.ErrInvalidOption { return http.StatusBadRequest, nil } else if err != nil { @@ -115,10 +118,7 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon } // delete thumbnails - err = delThumbs(r.Context(), fileCache, fileInfo) - if err != nil { - return errToStatus(err), err - } + delThumbs(r.Context(), fileCache, fileInfo) err = files.DeleteFiles(source, fileInfo.RealPath, filepath.Dir(fileInfo.RealPath)) if err != nil { @@ -184,10 +184,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte return http.StatusForbidden, nil } - err = delThumbs(r.Context(), fileCache, fileInfo) - if err != nil { - return errToStatus(err), err - } + delThumbs(r.Context(), fileCache, fileInfo) } err = files.WriteFile(fileOpts, r.Body) if err != nil { @@ -312,7 +309,9 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont err = d.RunHook(func() error { return patchAction(r.Context(), action, realSrc, realDest, d, fileCache, isSrcDir, source) }, action, realSrc, realDest, d.user) - + if err != nil { + logger.Debug(fmt.Sprintf("Could not run patch action. src=%v dst=%v err=%v", realSrc, realDest, err)) + } return errToStatus(err), err } @@ -332,11 +331,11 @@ func addVersionSuffix(source string) string { return source } -func delThumbs(ctx context.Context, fileCache FileCache, file files.ExtendedFileInfo) error { - if err := fileCache.Delete(ctx, previewCacheKey(file.RealPath, "small", file.FileInfo.ModTime)); err != nil { - return err +func delThumbs(ctx context.Context, fileCache FileCache, file files.ExtendedFileInfo) { + err := fileCache.Delete(ctx, previewCacheKey(file.RealPath, "small", file.FileInfo.ModTime)) + if err != nil { + logger.Debug(fmt.Sprintf("Could not delete small thumbnail: %v", err)) } - return nil } func patchAction(ctx context.Context, action, src, dst string, d *requestContext, fileCache FileCache, isSrcDir bool, index string) error { @@ -348,7 +347,6 @@ func patchAction(ctx context.Context, action, src, dst string, d *requestContext err := files.CopyResource(index, src, dst, isSrcDir) return err case "rename", "move": - if !d.user.Perm.Rename { return errors.ErrPermissionDenied } @@ -366,10 +364,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *requestContext } // delete thumbnails - err = delThumbs(ctx, fileCache, fileInfo) - if err != nil { - return err - } + delThumbs(ctx, fileCache, fileInfo) return files.MoveResource(index, src, dst, isSrcDir) default: return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams) diff --git a/backend/img/testdata b/backend/img/testdata new file mode 120000 index 00000000..55d8503d --- /dev/null +++ b/backend/img/testdata @@ -0,0 +1 @@ +../../frontend/tests/playwright-files/myfolder/testdata/ \ No newline at end of file diff --git a/backend/logger/setup.go b/backend/logger/setup.go index a59ddbb0..933abb5a 100644 --- a/backend/logger/setup.go +++ b/backend/logger/setup.go @@ -11,13 +11,14 @@ import ( // Logger wraps the standard log.Logger with log level functionality type Logger struct { - logger *log.Logger - levels []LogLevel - apiLevels []LogLevel - stdout bool - disabled bool - disabledAPI bool - colors bool + logger *log.Logger + levels []LogLevel + apiLevels []LogLevel + stdout bool + disabled bool + debugEnabled bool + disabledAPI bool + colors bool } var stdOutLoggerExists bool @@ -34,7 +35,7 @@ func NewLogger(filepath string, levels, apiLevels []LogLevel, noColors bool) (*L } fileWriter = file } - flags := log.Ldate | log.Ltime + var flags int if slices.Contains(levels, DEBUG) { flags |= log.Lshortfile } @@ -46,13 +47,14 @@ func NewLogger(filepath string, levels, apiLevels []LogLevel, noColors bool) (*L stdOutLoggerExists = true } return &Logger{ - logger: logger, - levels: levels, - apiLevels: apiLevels, - disabled: slices.Contains(levels, DISABLED), - disabledAPI: slices.Contains(apiLevels, DISABLED), - colors: !noColors, - stdout: stdout, + logger: logger, + levels: levels, + apiLevels: apiLevels, + disabled: slices.Contains(levels, DISABLED), + debugEnabled: slices.Contains(levels, DEBUG), + disabledAPI: slices.Contains(apiLevels, DISABLED), + colors: !noColors, + stdout: stdout, }, nil } @@ -67,6 +69,9 @@ func SetupLogger(output, levels, apiLevels string, noColors bool) error { if upperLevel == "WARNING" || upperLevel == "WARN" { upperLevel = "WARN " } + if upperLevel == "INFO" { + upperLevel = "INFO " + } // Convert level strings to LogLevel level, ok := stringToLevel[upperLevel] if !ok { @@ -87,6 +92,9 @@ func SetupLogger(output, levels, apiLevels string, noColors bool) error { if upperLevel == "WARNING" || upperLevel == "WARN" { upperLevel = "WARN " } + if upperLevel == "INFO" { + upperLevel = "INFO " + } // Convert level strings to LogLevel level, ok := stringToLevel[strings.ToUpper(upperLevel)] if !ok { @@ -119,6 +127,7 @@ func SetupLogger(output, levels, apiLevels string, noColors bool) error { loggers = append(loggers, logger) return nil } + func SplitByMultiple(str string) []string { delimiters := []rune{'|', ',', ' '} return strings.FieldsFunc(str, func(r rune) bool { diff --git a/backend/logger/write.go b/backend/logger/write.go index bcbd32c3..25f74f85 100644 --- a/backend/logger/write.go +++ b/backend/logger/write.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "slices" + "time" ) type LogLevel int @@ -38,7 +39,7 @@ type levelConsts struct { } var levels = levelConsts{ - INFO: "INFO", + INFO: "INFO ", // with consistent space padding FATAL: "FATAL", ERROR: "ERROR", WARNING: "WARN ", // with consistent space padding @@ -50,7 +51,7 @@ var levels = levelConsts{ // stringToLevel maps string representation to LogLevel var stringToLevel = map[string]LogLevel{ "DEBUG": DEBUG, - "INFO": INFO, + "INFO ": INFO, // with consistent space padding "ERROR": ERROR, "DISABLED": DISABLED, "WARN ": WARNING, // with consistent space padding @@ -71,15 +72,21 @@ func Log(level string, msg string, prefix, api bool, color string) { continue } } - if logger.stdout && LEVEL == FATAL { + if logger.stdout && level == "FATAL" { continue } writeOut := msg - if prefix { - writeOut = fmt.Sprintf("[%s] ", level) + writeOut + formattedTime := time.Now().Format("2006/01/02 15:04:05") + if logger.colors && color != "" { + formattedTime = formattedTime + color + } + if prefix || logger.debugEnabled { + logger.logger.SetPrefix(fmt.Sprintf("%s [%s] ", formattedTime, level)) + } else { + logger.logger.SetPrefix(formattedTime + " ") } if logger.colors && color != "" { - writeOut = color + writeOut + "\033[0m" + writeOut = writeOut + "\033[0m" } err := logger.logger.Output(3, writeOut) // 3 skips this function for correct file:line if err != nil { @@ -102,6 +109,8 @@ func Api(msg string, statusCode int) { func Debug(msg string) { if len(loggers) > 0 { Log(levels.DEBUG, msg, true, false, GRAY) + } else { + log.Println("[DEBUG]: " + msg) } } diff --git a/backend/settings/config.go b/backend/settings/config.go index 6445a915..dc927fef 100644 --- a/backend/settings/config.go +++ b/backend/settings/config.go @@ -17,6 +17,9 @@ var Config Settings func Initialize(configFile string) { yamlData, err := loadConfigFile(configFile) + if err != nil && configFile != "config.yaml" { + logger.Fatal("Could not load specified config file: " + err.Error()) + } if err != nil { logger.Warning(fmt.Sprintf("Could not load config file '%v', using default settings: %v", configFile, err)) } @@ -35,21 +38,22 @@ func Initialize(configFile string) { logger.Fatal(fmt.Sprintf("Error getting source path: %v", err2)) } source.Path = realPath - source.Name = "default" // Modify the local copy of the map value - Config.Server.Sources["default"] = source // Assign the modified value back to the map + source.Name = "default" + Config.Server.Sources = []Source{source} // temporary set only one source } } else { realPath, err2 := filepath.Abs(Config.Server.Root) if err2 != nil { logger.Fatal(fmt.Sprintf("Error getting source path: %v", err2)) } - Config.Server.Sources = map[string]Source{ - "default": { + Config.Server.Sources = []Source{ + { Name: "default", Path: realPath, }, } } + baseurl := strings.Trim(Config.Server.BaseURL, "/") if baseurl == "" { Config.Server.BaseURL = "/" @@ -57,10 +61,6 @@ func Initialize(configFile string) { Config.Server.BaseURL = "/" + baseurl + "/" } if !Config.Frontend.DisableDefaultLinks { - Config.Frontend.ExternalLinks = append(Config.Frontend.ExternalLinks, ExternalLink{ - Text: "FileBrowser Quantum", - Url: "https://github.com/gtsteffaniak/filebrowser", - }) Config.Frontend.ExternalLinks = append(Config.Frontend.ExternalLinks, ExternalLink{ Text: fmt.Sprintf("(%v)", version.Version), Title: version.CommitSHA, @@ -89,7 +89,6 @@ func Initialize(configFile string) { log.Println("[ERROR] Failed to set up logger:", err) } } - } func loadConfigFile(configFile string) ([]byte, error) { diff --git a/backend/settings/structs.go b/backend/settings/structs.go index 19477c1c..e755cb93 100644 --- a/backend/settings/structs.go +++ b/backend/settings/structs.go @@ -36,26 +36,26 @@ type Recaptcha struct { } type Server struct { - NumImageProcessors int `json:"numImageProcessors"` - Socket string `json:"socket"` - TLSKey string `json:"tlsKey"` - TLSCert string `json:"tlsCert"` - EnableThumbnails bool `json:"enableThumbnails"` - ResizePreview bool `json:"resizePreview"` - EnableExec bool `json:"enableExec"` - TypeDetectionByHeader bool `json:"typeDetectionByHeader"` - AuthHook string `json:"authHook"` - Port int `json:"port"` - BaseURL string `json:"baseURL"` - Address string `json:"address"` - Logging []LogConfig `json:"logging"` - Database string `json:"database"` - Root string `json:"root"` - UserHomeBasePath string `json:"userHomeBasePath"` - CreateUserDir bool `json:"createUserDir"` - Sources map[string]Source `json:"sources"` - ExternalUrl string `json:"externalUrl"` - InternalUrl string `json:"internalUrl"` // used by integrations + NumImageProcessors int `json:"numImageProcessors"` + Socket string `json:"socket"` + TLSKey string `json:"tlsKey"` + TLSCert string `json:"tlsCert"` + EnableThumbnails bool `json:"enableThumbnails"` + ResizePreview bool `json:"resizePreview"` + EnableExec bool `json:"enableExec"` + TypeDetectionByHeader bool `json:"typeDetectionByHeader"` + AuthHook string `json:"authHook"` + Port int `json:"port"` + BaseURL string `json:"baseURL"` + Address string `json:"address"` + Logging []LogConfig `json:"logging"` + Database string `json:"database"` + Root string `json:"root"` + UserHomeBasePath string `json:"userHomeBasePath"` + CreateUserDir bool `json:"createUserDir"` + Sources []Source `json:"sources"` + ExternalUrl string `json:"externalUrl"` + InternalUrl string `json:"internalUrl"` // used by integrations } type Integrations struct { @@ -78,8 +78,8 @@ type LogConfig struct { } type Source struct { - Path string `json:"path"` - Name string + Path string `json:"path"` + Name string `json:"name"` Config IndexConfig `json:"config"` } diff --git a/frontend/global-setup.ts b/frontend/global-setup.ts index 52ccb901..f2d28bd6 100644 --- a/frontend/global-setup.ts +++ b/frontend/global-setup.ts @@ -12,7 +12,7 @@ async function globalSetup() { await page.waitForURL("**/files/", { timeout: 100 }); let cookies = await context.cookies(); expect(cookies.find((c) => c.name == "auth")?.value).toBeDefined(); - await expect(page).toHaveTitle('playwright-files - FileBrowser Quantum - Files'); + await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files"); await page.context().storageState({ path: "./loginAuth.json" }); await browser.close(); } diff --git a/frontend/src/api/files.js b/frontend/src/api/files.js index 036acd64..92416ecb 100644 --- a/frontend/src/api/files.js +++ b/frontend/src/api/files.js @@ -158,10 +158,16 @@ export async function moveCopy(items, action = "copy", overwrite = false, rename } -export async function checksum(url, algo) { +export async function checksum(path, algo) { try { - const data = await resourceAction(`${url}?checksum=${algo}`, "GET"); - return (await data.json()).checksums[algo]; + const params = { + path: encodeURIComponent(removePrefix(path, "files")), + checksum: algo, + }; + const apiPath = getApiPath("api/resources", params); + const res = await fetchURL(apiPath); + const data = await res.json(); + return data.checksums[algo]; } catch (err) { notify.showError(err.message || "Error fetching checksum"); throw err; diff --git a/frontend/src/components/ContextMenu.vue b/frontend/src/components/ContextMenu.vue index e9a0ffff..14225bdc 100644 --- a/frontend/src/components/ContextMenu.vue +++ b/frontend/src/components/ContextMenu.vue @@ -13,21 +13,20 @@
{{ selectedCount }} selected
- -