Compare commits

...
Author SHA1 Message Date
Graham Steffaniak 103ff1c924 updated docs 2025-01-18 11:47:17 -06:00
Graham Steffaniak 3226990c6f updated 2025-01-18 09:56:27 -06:00
Graham eed3652f75 updated docs 2025-01-18 08:28:09 -06:00
Graham Steffaniak baf40e9f54 made sure only one stdout logger at a time 2025-01-17 18:03:27 -06:00
Graham Steffaniak 075db41ab6 Add logger with config (#292) 2025-01-16 15:33:53 -06:00
Graham Steffaniak bedd445b9f updated removing unneeded 2025-01-14 17:03:14 -06:00
Graham 390b381b4f updated archive file name 2025-01-14 05:55:08 -06:00
Graham Steffaniak adc50d04b0 Update CHANGELOG.md 2025-01-13 20:34:41 -05:00
Graham Steffaniak ec1b12f709 updated with username in logs (#289) 2025-01-13 19:31:56 -06:00
Graham Steffaniak 3683f37077 updated 2025-01-13 18:38:40 -06:00
49 changed files with 931 additions and 809 deletions

No files matched your search

+6
View File
@@ -39,3 +39,9 @@ yarn-error.log*
*.sw*
bin/
build/
# docs
/autodocs/public
/autodocs/themes
/autodocs/resources
View File
File renamed without changes.
+8
View File
@@ -2,6 +2,14 @@
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.0-beta
**New Features**
- Better logging
- highly configurable
- api logs include user
- openoffice support
## v0.3.7-beta
**Notes**:
View File
File renamed without changes.
+5
View File
@@ -0,0 +1,5 @@
+++
date = '{{ .Date }}'
draft = true
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
+++
@@ -0,0 +1,334 @@
---
weight: 200
title: "Installation"
description: "How to install Filebrowser"
icon: "settings"
date: "2025-01-15"
lastmod: "2025-01-15"
draft: false
---
## Install
Using docker:
1. docker run (no persistent db):
```
docker run -it -v /path/to/folder:/srv -v $(pwd)/config.yaml:/home/filebrowser/config.yaml -p 80:80 gtstef/filebrowser
```
or optionally, as non-root filebrowser user:
```
docker run -u filebrowser -it -v $(pwd)/config.yaml:/home/filebrowser/config.yaml -v /path/to/folder:/srv -p 80:80 gtstef/filebrowser
```
1. docker compose:
- with local storage
```
services:
filebrowser:
volumes:
- '/path/to/folder:/srv' # required (for now not configurable)
# optional if you want db to persist - configure a path under "database" dir in config file.
- './database:/home/filebrowser/database'
- './config.yaml:/home/filebrowser/config.yaml'
ports:
- '80:80'
image: gtstef/filebrowser
# optionally run as non-root filebrowser user
#user: filebrowser
restart: always
```
- with network share
```
services:
filebrowser:
volumes:
- 'storage:/srv' # required (for now not configurable)
# optional if you want db to persist - configure a path under "database" dir in config file.
- './database:/home/filebrowser/database'
- './config.yaml:/home/filebrowser/config.yaml'
ports:
- '80:80'
image: gtstef/filebrowser
restart: always
volumes:
storage:
driver_opts:
type: cifs
o: "username=admin,password=password,rw" # enter valid info here
device: "//192.168.1.100/share/" # enter valid info here
```
Not using docker (not recommended), download your binary from releases and run with your custom config file:
```
./filebrowser -c <config.yaml or other /path/to/config.yaml>
```
## Example frontend config:
```
frontend:
name: "Your custom name"
disableDefaultLinks: true
externalLinks:
- text: "Your custom link"
url: "https://example.com"
```
## Example advanced source config:
```
server:
port: 80
sources:
- name: "default"
path: "/mnt/folder"
config:
ignoreHidden: true
ignoreZeroSizeFolders: true
exclude:
files:
- "/myfile.txt" # for example, corresponds to "/mnt/folder/myfile.txt"
- "/subfolder/another.txt"
folders:
- "/subfolder/ignoreMe"
fileEndsWith:
- ".zip" # excludes any files that end with ".zip"
- ".tar.gz"
- "-hidden.jpg"
```
## All possible configurations
Here is an expanded config file which includes all possible configurations:
```
server:
CreateUserDir: false
UserHomeBasePath: ""
indexing: true
numImageProcessors: 4
socket: ""
tlsKey: ""
tlsCert: ""
enableThumbnails: false
resizePreview: true
typeDetectionByHeader: true
port: 80
baseURL: "/"
address: ""
log: "stdout"
database: "/database/database.db"
root: "/srv"
sources: # optional only needed for custom index configuration, can use root still for default index config.
auth:
adminUsername: admin
adminPassword: admin
recaptcha:
host: ""
key: ""
secret: ""
tokenExpirationTime: 2h
header: ""
method: json
signup: false
frontend:
name: ""
disableDefaultLinks: false
disableUsedPercentage: true
externalLinks:
- text: "Your custom link"
url: "https://example.com"
title: "this text appears when hovering over link"
color: ""
userDefaults:
settingsAllowed: true
darkMode: false
scope: ""
locale: "en"
viewMode: ""
singleClick: true
permissions:
admin: true
execute: true
create: true
rename: true
modify: true
delete: true
share: true
download: true
api: true
hideDotfiles: false
dateFormat: false
```
Here are the defaults if nothing is set:
```
server:
enableThumbnails: true
enableExec: false
port: 80
numImageProcessors: 4
baseURL: ""
database: database.db
log: stdout
root: /srv
sources:
auth:
adminUsername: admin
adminPassword: admin
method: password
recaptcha:
host: ""
userDefaults:
settingsAllowed: true
darkMode: false
locale: "en"
scope: "."
lockPassword: false
hideDotfiles: true
permissions:
create: true
rename: true
modify: true
delete: true
share: true
download: true
api: false
```
## About each configuration
# Configuration Settings Documentation
## About each configuration
### Server configuration settings
- `indexingInterval`: This optional paramter disables smart indexing and specifies a time in minutes the system waits before checking for filesystem changes. See [indexing readme](indexing.md) for more information.
- `indexing`: This enables or disables indexing. (Note: search will not work without indexing) Default: `true`
- `numImageProcessors`: This is the number of image processors available. Default: `4`
- `socket`: This is the socket configuration.
- `tlsKey`: This is the TLS key configuration.
- `tlsCert`: This is the TLS certificate configuration.
- `enableThumbnails`: This boolean value determines whether thumbnails are enabled on ui. Default: `true`
- `resizePreview`: This boolean value determines whether preview resizing is enabled. Default: `false`
- `typeDetectionByHeader`: This boolean value determines whether type detection is based on headers.
- `port`: This is the port number on which the server is running. Default: `80`
- `baseURL`: This is the base URL for the server. Default: `""`
- `address`: This is the server address configuration. Default: `0.0.0.0`
- `log`: This specifies the log destination. Default: `stdout`
- `database`: This is the database file path + filename that will be created if it does not already exist. If it exists, it will use the existing file. Default `database.db`
- `root`: This is the root directory path. Default: `/srv`
- `CreateUserDir`: Boolean to create user directory on user creation. Default: `false`
- `UserHomeBasePath`: String to define user home directory base path. Default: `""`
- `externalUrl`: String to define a domain and potentially additional subpath for shared links to use. Example "https://mydomain.com/", Default: `""`
### Auth configuration settings
- `recaptcha`:
- `host`: This is the host for reCAPTCHA.
- `key`: This is the reCAPTCHA key.
- `secret`: This is the reCAPTCHA secret.
- `header`: This is the authentication header.
- `method`: This is the authentication method used (e.g., "json"). Possible values:
- `password` - username and password
- `hook` - hook authentication
- `proxy` - proxy authentication
- `oath` - oath authentication
- `noauth` - no authentication/login required.
- `Signup`: This boolean value indicates whether user signup is enabled on the login page. NOTE: Be mindful of `userDefaults` settings if enabled. Default: `false`
- `AdminUsername`: This is the username of the admin user. Default: `admin`
- `AdminPassword`: This is the password of the admin user. Default: `admin`
### Frontend configuration settings
- `name`: This is the name of the frontend.
- `disableExternal`: This boolean value determines whether external access is disabled.
- `disableUsedPercentage`: This boolean value determines whether used percentage is disabled.
- `files`: This is the files configuration.
- `theme`: This is the theme configuration.
- `color`: This is the color configuration.
### UserDefaults configuration settings
- `darkMode`: Determines whether dark mode is enabled for the user (`true` or `false`)
- `settingsAllowed`: Determines whether settings page is enabled for the user (`true` or `false`)
- `scope`: This is a scope of the permissions, "." or "./" means all directories, "./downloads" would mean only the downloads folder.
- `locale`: String locale configuration. Default: `en`
- `viewMode`: This is the view mode configuration. Possible values: `normal`, `compact`, `list`, and `gallery`. default: `normal`
- `singleClick`: This boolean value determines whether single-click is enabled. (`true` or `false`)
- `sorting`:
- `by`: This is the sorting method used (e.g., "asc").
- `asc`: This boolean value determines the sorting order is ascending or descending. (`true` or `false`)
- `permissions`:
- `admin`: This boolean value determines whether admin permissions are granted.
- `execute`: This boolean value determines whether execute permissions are granted.
- `create`: This boolean value determines whether create permissions are granted.
- `rename`: This boolean value determines whether rename permissions are granted.
- `modify`: This boolean value determines whether modify permissions are granted.
- `delete`: This boolean value determines whether delete permissions are granted.
- `share`: This boolean value determines whether share permissions are granted.
- `download`: This boolean value determines whether download permissions are granted.
- `api`: Ability to create and manage API keys.
- `hideDotfiles`: This boolean value determines whether dotfiles are hidden. (`true` or `false`)
- `dateFormat`: This boolean value determines whether date formatting is enabled. (`true` or `false`)
+25
View File
@@ -0,0 +1,25 @@
---
weight: 200
title: "API"
description: "API Help"
icon: "edit"
date: "2025-01-15"
lastmod: "2025-01-15"
draft: false
---
API tokens can be created to perform actions, access file information, and update user settings just like what can be done from the UI. You can create API tokens from the settings page via "API Management" section. This section will only show up if the user has "API" permissions, which can be granted by editing the user in user management.
Regardless of whether a user has API permissions, anyone can visit the swagger page which is found at `/swagger`. This swagger page uses a short-live token (2-hour exp) that the UI uses, but allows for quick access to all the API's and their described usage and requirements:
![image](https://github.com/user-attachments/assets/12abd1f6-21d3-4437-98ed-9b0da6cf2c73)
When using the API outside of swagger, you will need to set the API token as a bearer token authentication type. This means the authorization header will look like `Authorization: Bearer <token>`. For example in Postman:
Successful Request:
<p align="center"><img width="500" alt="image" src="https://github.com/user-attachments/assets/4f18fa8a-8d87-4f40-9dc7-3d4407769b59"></p>
Failed Request
<p align="center"><img width="500" alt="image" src="https://github.com/user-attachments/assets/4da0deae-f93d-4d94-83b1-68806afb343a"></p>
+13
View File
@@ -0,0 +1,13 @@
---
weight: 100
date: "2023-05-03T22:37:22+01:00"
draft: false
author: "Colin Wilson"
title: "Quickstart"
icon: "rocket_launch"
description: "A quickstart guide to creating new content in Lotus Docs"
publishdate: "2023-05-03T22:37:22+01:00"
tags: ["Beginners"]
---
## Requirements
+89
View File
@@ -0,0 +1,89 @@
# Note: Template blocks require a 'weight' parameter so they're correctly ordered on the landing page
# Hero
hero:
enable: true
weight: 10
width: 100%
template: hero
badge:
text: "v0.4.0"
color: success # primary, secondary, success, danger, warning, info, light, dark
pill: true # boolean
soft: false # boolean
titleLogo:
path: "images/logos"
filename: "title_logo.png"
alt: "Lotus Docs Logo"
height: 80px
title: "FileBrowser Quantum"
subtitle: A modern self-hosted **file manager** for the web.
ctaButton:
color: success
btnText: "Go to the Docs"
url: "docs"
info: "**Open Source** Apache 2.0 Licensed."
# Image compare
imageCompare:
enable: true
weight: 30
template: image compare
title: Beautifully Customizable.
subtitle: You can make it your own.
items:
- title: Dark Mode
config: {
startingPoint: 50,
addCircle: true,
addCircleBlur: false,
showLabels: true,
labelOptions: {
before: 'Dark',
after: 'Light',
onHover: false
}
}
imagePath: "images/screenshots"
imageBefore: "lotusdocs_dark_v0.8.webp"
imageAfter: "lotusdocs_light_v0.8.webp"
- title: Branding
config: {
controlColor: "#3C4257",
startingPoint: 25,
addCircle: true,
addCircleBlur: false,
showLabels: true,
labelOptions: {
before: 'Inter',
after: 'Life Saver',
onHover: false
}
}
imagePath: "images/screenshots"
imageBefore: "lotusdocs_google_font_demo_inter_screenshot.webp"
imageAfter: "lotusdocs_google_font_demo_lifesavers_screenshot.webp"
- title: Accent Color
config: {
startingPoint: 25,
addCircle: true,
addCircleBlur: true,
showLabels: true,
labelOptions: {
before: 'Blue',
after: 'Cardinal',
onHover: false
}
}
imagePath: "images/screenshots"
imageBefore: "lotusdocs_blue_theme_colour.webp"
imageAfter: "lotusdocs_cardinal_theme_colour.webp"
+8
View File
@@ -0,0 +1,8 @@
module github.com/gtsteffaniak/filebrowser
go 1.22.5
require (
github.com/gohugoio/hugo-mod-bootstrap-scss/v5 v5.20300.20200 // indirect
github.com/gtsteffaniak/filebrowserDocsTheme v0.0.0-20250118144715-40025a28c8d5 // indirect
)
+6
View File
@@ -0,0 +1,6 @@
github.com/gohugoio/hugo-mod-bootstrap-scss/v5 v5.20300.20200 h1:SmpwwN3DNzJWbV+IT8gaFu07ENUFpCvKou5BHYUKuVs=
github.com/gohugoio/hugo-mod-bootstrap-scss/v5 v5.20300.20200/go.mod h1:kx8MBj9T7SFR8ZClWvKZPmmUxBaltkoXvnWlZZcSnYA=
github.com/gohugoio/hugo-mod-jslibs-dist/popperjs/v2 v2.21100.20000/go.mod h1:mFberT6ZtcchrsDtfvJM7aAH2bDKLdOnruUHl0hlapI=
github.com/gtsteffaniak/filebrowserDocsTheme v0.0.0-20250118144715-40025a28c8d5 h1:dwZ1GlVhtfgigYUoPHZd+PBczUgl7FpSJrxMhPQXXJQ=
github.com/gtsteffaniak/filebrowserDocsTheme v0.0.0-20250118144715-40025a28c8d5/go.mod h1:9Ee1u+ac9+utA3Tt3SnoIYmoewMlx7uf/WLb0CVZ2ys=
github.com/twbs/bootstrap v5.3.2+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0=
+8
View File
@@ -0,0 +1,8 @@
baseURL = 'https://example.org/'
languageCode = 'en-us'
title = 'Docs'
[module]
[[module.imports]]
path = "github.com/gtsteffaniak/filebrowserDocsTheme"
disable = false
+2 -2
View File
@@ -3,13 +3,13 @@ package auth
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"strings"
"github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/users"
)
@@ -164,7 +164,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
return nil, fmt.Errorf("user: failed to mkdir user home dir: [%s]", userHome)
}
u.Scope = userHome
log.Printf("user: %s, home dir: [%s].", u.Username, userHome)
logger.Debug(fmt.Sprintf("user: %s, home dir: [%s].", u.Username, userHome))
err = a.Users.Save(u)
if err != nil {
+28 -26
View File
@@ -3,7 +3,6 @@ package cmd
import (
"flag"
"fmt"
"log"
"os"
"strings"
@@ -11,6 +10,7 @@ import (
"github.com/gtsteffaniak/filebrowser/backend/files"
fbhttp "github.com/gtsteffaniak/filebrowser/backend/http"
"github.com/gtsteffaniak/filebrowser/backend/img"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/backend/swagger/docs"
@@ -25,21 +25,22 @@ func getStore(config string) (*storage.Storage, bool) {
settings.Initialize(config)
store, hasDB, err := storage.InitializeDb(settings.Config.Server.Database)
if err != nil {
log.Fatal("could not load db info: ", err)
logger.Fatal(fmt.Sprintf("could not load db info: %v", err))
}
return store, hasDB
}
func generalUsage() {
fmt.Printf(`usage: ./filebrowser <command> [options]
commands:
-v Print the version
-c Print the default config file
set -u Username and password for the new user
set -a Create user as admin
set -s Specify a user scope
set -h Print this help message
` + "\n")
commands:
-h Print help
-c Print the default config file
version Print version information
set -u Username and password for the new user
set -a Create user as admin
set -s Specify a user scope
set -h Print this help message
`)
}
func StartFilebrowser() {
@@ -90,9 +91,9 @@ func StartFilebrowser() {
getStore(dbConfig)
// Create the user logic
if asAdmin {
log.Printf("Creating user as admin: %s\n", username)
logger.Info(fmt.Sprintf("Creating user as admin: %s\n", username))
} else {
log.Printf("Creating user: %s\n", username)
logger.Info(fmt.Sprintf("Creating non-admin user: %s\n", username))
}
newUser := users.User{
Username: username,
@@ -103,14 +104,15 @@ func StartFilebrowser() {
}
err = storage.CreateUser(newUser, asAdmin)
if err != nil {
log.Fatal("Could not create user: ", err)
logger.Fatal(fmt.Sprintf("could not create user: %v", 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)
fmt.Printf(`FileBrowser Quantum - A modern web-based file manager
Version : %v
Commit : %v
Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v
`, version.Version, version.CommitSHA, version.Version)
return
}
}
@@ -119,15 +121,15 @@ func StartFilebrowser() {
if !dbExists {
database = fmt.Sprintf("Creating new database : %v", settings.Config.Server.Database)
}
log.Printf("Initializing FileBrowser Quantum (%v)\n", version.Version)
log.Printf("Using Config file : %v", configPath)
log.Println("Embeded frontend :", os.Getenv("FILEBROWSER_NO_EMBEDED") != "true")
log.Println(database)
sources := []string{}
for _, v := range settings.Config.Server.Sources {
sources = append(sources, v.Name+": "+v.Path)
}
log.Println("Sources :", sources)
logger.Info(fmt.Sprintf("Initializing FileBrowser Quantum (%v)", version.Version))
logger.Info(fmt.Sprintf("Using Config file : %v", configPath))
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
@@ -136,19 +138,19 @@ func StartFilebrowser() {
// initialize indexing and schedule indexing ever n minutes (default 5)
sourceConfigs := settings.Config.Server.Sources
if len(sourceConfigs) == 0 {
log.Fatal("No sources configured, exiting...")
logger.Fatal("No sources configured, exiting...")
}
for _, source := range sourceConfigs {
go files.Initialize(source)
}
if err := rootCMD(store, &serverConfig); err != nil {
log.Fatal("Error starting filebrowser:", err)
logger.Fatal(fmt.Sprintf("Error starting filebrowser: %v", err))
}
}
func rootCMD(store *storage.Storage, serverConfig *settings.Server) error {
if serverConfig.NumImageProcessors < 1 {
log.Fatal("Image resize workers count could not be < 1")
logger.Fatal("Image resize workers count could not be < 1")
}
imgSvc := img.New(serverConfig.NumImageProcessors)
@@ -160,7 +162,7 @@ func rootCMD(store *storage.Storage, serverConfig *settings.Server) error {
var err error
fileCache, err = diskcache.NewFileCache(cacheDir)
if err != nil {
log.Fatalf("failed to create file cache: %v", err)
logger.Fatal(fmt.Sprintf("failed to create file cache: %v", err))
}
} else {
// No-op cache if no cacheDir is specified
-68
View File
@@ -1,68 +0,0 @@
package cmd
import (
"strconv"
"github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/backend/utils"
)
func init() {
rulesCmd.AddCommand(rulesRmCommand)
rulesRmCommand.Flags().Uint("index", 0, "index of rule to remove")
_ = rulesRmCommand.MarkFlagRequired("index")
}
var rulesRmCommand = &cobra.Command{
Use: "rm <index> [index_end]",
Short: "Remove a global rule or user rule",
Long: `Remove a global rule or user rule. The provided index
is the same that's printed when you run 'rules ls'. Note
that after each removal/addition, the index of the
commands change. So be careful when removing them after each
other.
You can also specify an optional parameter (index_end) so
you can remove all commands from 'index' to 'index_end',
including 'index_end'.`,
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil { //nolint:gomnd
return err
}
for _, arg := range args {
if _, err := strconv.Atoi(arg); err != nil {
return err
}
}
return nil
},
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
i, err := strconv.Atoi(args[0])
utils.CheckErr("strconv.Atoi", err)
f := i
if len(args) == 2 { //nolint:gomnd
f, err = strconv.Atoi(args[1])
utils.CheckErr("strconv.Atoi", err)
}
user := func(u *users.User) {
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
err := store.Users.Save(u)
utils.CheckErr("store.Users.Save", err)
}
global := func(s *settings.Settings) {
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
err := store.Settings.Save(s)
utils.CheckErr("store.Settings.Save", err)
}
runRules(store, cmd, user, global)
}),
}
-86
View File
@@ -1,86 +0,0 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/backend/utils"
)
func init() {
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")
}
var rulesCmd = &cobra.Command{
Use: "rules",
Short: "Rules management utility",
Long: `On each subcommand you'll have available at least two flags:
"username" and "id". You must either set only one of them
or none. If you set one of them, the command will apply to
an user, otherwise it will be applied to the global set or
rules.`,
Args: cobra.NoArgs,
}
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User), globalFn func(*settings.Settings)) {
id := getUserIdentifier(cmd.Flags())
if id != nil {
user, err := st.Users.Get("", id)
utils.CheckErr("st.Users.Get", err)
if usersFn != nil {
usersFn(user)
}
printRules(user.Rules, id)
return
}
s, err := st.Settings.Get()
utils.CheckErr("st.Settings.Get", err)
if globalFn != nil {
globalFn(s)
}
printRules(s.Rules, id)
}
func getUserIdentifier(flags *pflag.FlagSet) interface{} {
id := mustGetUint(flags, "id")
username := mustGetString(flags, "username")
if id != 0 {
return id
} else if username != "" {
return username
}
return nil
}
func printRules(rulez []users.Rule, id interface{}) {
for id, rule := range rulez {
fmt.Printf("(%d) ", id)
if rule.Regex {
if rule.Allow {
fmt.Printf("Allow Regex: \t%s\n", rule.Regexp.Raw)
} else {
fmt.Printf("Disallow Regex: \t%s\n", rule.Regexp.Raw)
}
} else {
if rule.Allow {
fmt.Printf("Allow Path: \t%s\n", rule.Path)
} else {
fmt.Printf("Disallow Path: \t%s\n", rule.Path)
}
}
}
}
-59
View File
@@ -1,59 +0,0 @@
package cmd
import (
"regexp"
"github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/backend/utils"
)
func init() {
rulesCmd.AddCommand(rulesAddCmd)
rulesAddCmd.Flags().BoolP("allow", "a", false, "indicates this is an allow rule")
rulesAddCmd.Flags().BoolP("regex", "r", false, "indicates this is a regex rule")
}
var rulesAddCmd = &cobra.Command{
Use: "add <path|expression>",
Short: "Add a global rule or user rule",
Long: `Add a global rule or user rule.`,
Args: cobra.ExactArgs(1),
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
allow := mustGetBool(cmd.Flags(), "allow")
regex := mustGetBool(cmd.Flags(), "regex")
exp := args[0]
if regex {
regexp.MustCompile(exp)
}
rule := users.Rule{
Allow: allow,
Regex: regex,
}
if regex {
rule.Regexp = &users.Regexp{Raw: exp}
} else {
rule.Path = exp
}
user := func(u *users.User) {
u.Rules = append(u.Rules, rule)
err := store.Users.Save(u)
utils.CheckErr("store.Users.Save", err)
}
global := func(s *settings.Settings) {
s.Rules = append(s.Rules, rule)
err := store.Settings.Save(s)
utils.CheckErr("store.Settings.Save", err)
}
runRules(store, cmd, user, global)
}),
}
-20
View File
@@ -1,20 +0,0 @@
package cmd
import (
"github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/spf13/cobra"
)
func init() {
rulesCmd.AddCommand(rulesLsCommand)
}
var rulesLsCommand = &cobra.Command{
Use: "ls",
Short: "List global rules or user specific rules",
Long: `List global rules or user specific rules.`,
Args: cobra.NoArgs,
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
runRules(store, cmd, nil, nil)
}),
}
-54
View File
@@ -1,54 +0,0 @@
package cmd
import (
"fmt"
"os"
"strconv"
"text/tabwriter"
"github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/backend/users"
)
var usersCmd = &cobra.Command{
Use: "users",
Short: "Users management utility",
Long: `Users management utility.`,
Args: cobra.NoArgs,
}
func printUsers(usrs []*users.User) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
for _, u := range usrs {
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
u.ID,
u.Username,
u.Scope,
u.Locale,
u.ViewMode,
u.SingleClick,
u.Perm.Admin,
u.Perm.Execute,
u.Perm.Create,
u.Perm.Rename,
u.Perm.Modify,
u.Perm.Delete,
u.Perm.Share,
u.Perm.Download,
u.LockPassword,
)
}
w.Flush()
}
func parseUsernameOrID(arg string) (username string, id uint) {
id64, err := strconv.ParseUint(arg, 10, 64)
if err != nil {
return arg, 0
}
return "", uint(id64)
}
-42
View File
@@ -1,42 +0,0 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/backend/utils"
)
func init() {
usersCmd.AddCommand(usersAddCmd)
}
var usersAddCmd = &cobra.Command{
Use: "add <username> <password>",
Short: "Create a new user",
Long: `Create a new user and add it to the database.`,
Args: cobra.ExactArgs(2), //nolint:gomnd
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
user := &users.User{
Username: args[0],
Password: args[1],
LockPassword: mustGetBool(cmd.Flags(), "lockPassword"),
}
servSettings, err := store.Settings.GetServer()
utils.CheckErr("store.Settings.GetServer()", err)
// since getUserDefaults() polluted s.Defaults.Scope
// which makes the Scope not the one saved in the db
// we need the right s.Defaults.Scope here
s2, err := store.Settings.Get()
utils.CheckErr("store.Settings.Get()", err)
userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root)
utils.CheckErr("s2.MakeUserDir", err)
user.Scope = userHome
err = store.Users.Save(user)
utils.CheckErr("store.Users.Save", err)
printUsers([]*users.User{user})
}),
}
-26
View File
@@ -1,26 +0,0 @@
package cmd
import (
"github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/backend/utils"
"github.com/spf13/cobra"
)
func init() {
usersCmd.AddCommand(usersExportCmd)
}
var usersExportCmd = &cobra.Command{
Use: "export <path>",
Short: "Export all users to a file.",
Long: `Export all users to a json or yaml file. Please indicate the
path to the file where you want to write the users.`,
Args: jsonYamlArg,
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
list, err := store.Users.Gets("")
utils.CheckErr("store.Users.Gets", err)
err = marshal(args[0], list)
utils.CheckErr("marshal", err)
}),
}
-53
View File
@@ -1,53 +0,0 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/backend/utils"
)
func init() {
usersCmd.AddCommand(usersFindCmd)
usersCmd.AddCommand(usersLsCmd)
}
var usersFindCmd = &cobra.Command{
Use: "find <id|username>",
Short: "Find a user by username or id",
Long: `Find a user by username or id. If no flag is set, all users will be printed.`,
Args: cobra.ExactArgs(1),
Run: findUsers,
}
var usersLsCmd = &cobra.Command{
Use: "ls",
Short: "List all users.",
Args: cobra.NoArgs,
Run: findUsers,
}
var findUsers = cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
var (
list []*users.User
user *users.User
err error
)
if len(args) == 1 {
username, id := parseUsernameOrID(args[0])
if username != "" {
user, err = store.Users.Get("", username)
} else {
user, err = store.Users.Get("", id)
}
list = []*users.User{user}
} else {
list, err = store.Users.Gets("")
}
utils.CheckErr("findUsers", err)
printUsers(list)
})
-88
View File
@@ -1,88 +0,0 @@
package cmd
import (
"errors"
"fmt"
"os"
"strconv"
"github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/backend/utils"
)
func init() {
usersCmd.AddCommand(usersImportCmd)
usersImportCmd.Flags().Bool("overwrite", false, "overwrite users with the same id/username combo")
usersImportCmd.Flags().Bool("replace", false, "replace the entire user base")
}
var usersImportCmd = &cobra.Command{
Use: "import <path>",
Short: "Import users from a file",
Long: `Import users from a file. The path must be for a json or yaml
file. You can use this command to import new users to your
installation. For that, just don't place their ID on the files
list or set it to 0.`,
Args: jsonYamlArg,
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
fd, err := os.Open(args[0])
utils.CheckErr("os.Open", err)
defer fd.Close()
list := []*users.User{}
err = unmarshal(args[0], &list)
utils.CheckErr("unmarshal", err)
if mustGetBool(cmd.Flags(), "replace") {
oldUsers, err := store.Users.Gets("")
utils.CheckErr("store.Users.Gets", err)
err = marshal("users.backup.json", list)
utils.CheckErr("marshal users.backup.json", err)
for _, user := range oldUsers {
err = store.Users.Delete(user.ID)
utils.CheckErr("store.Users.Delete", err)
}
}
overwrite := mustGetBool(cmd.Flags(), "overwrite")
for _, user := range list {
onDB, err := store.Users.Get("", user.ID)
// User exists in DB.
if err == nil {
if !overwrite {
newErr := errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered")
utils.CheckErr("", newErr)
}
// 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
// operation
if user.Username != onDB.Username {
if conflictuous, err := store.Users.Get("", user.Username); err == nil { //nolint:govet
newErr := usernameConflictError(user.Username, conflictuous.ID, user.ID)
utils.CheckErr("usernameConflictError", newErr)
}
}
} else {
// If it doesn't exist, set the ID to 0 to automatically get a new
// one that make sense in this DB.
user.ID = 0
}
err = store.Users.Save(user)
utils.CheckErr("store.Users.Save", err)
}
}),
}
func usernameConflictError(username string, originalID, newID uint) error {
return fmt.Errorf(`can't import user with ID %d and username "%s" because the username is already registred with the user %d`,
newID, username, originalID)
}
-33
View File
@@ -1,33 +0,0 @@
package cmd
import (
"log"
"github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/backend/utils"
"github.com/spf13/cobra"
)
func init() {
usersCmd.AddCommand(usersRmCmd)
}
var usersRmCmd = &cobra.Command{
Use: "rm <id|username>",
Short: "Delete a user by username or id",
Long: `Delete a user by username or id`,
Args: cobra.ExactArgs(1),
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
username, id := parseUsernameOrID(args[0])
var err error
if username != "" {
err = store.Users.Delete(username)
} else {
err = store.Users.Delete(id)
}
utils.CheckErr("usersRmCmd", err)
log.Println("user deleted successfully")
}),
}
-40
View File
@@ -1,40 +0,0 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/backend/utils"
)
func init() {
usersCmd.AddCommand(usersUpdateCmd)
}
var usersUpdateCmd = &cobra.Command{
Use: "update <id|username>",
Short: "Updates an existing user",
Long: `Updates an existing user. Set the flags for the
options you want to change.`,
Args: cobra.ExactArgs(1),
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
username, id := parseUsernameOrID(args[0])
var (
err error
user *users.User
)
if id != 0 {
user, err = store.Users.Get("", id)
} else {
user, err = store.Users.Get("", username)
}
utils.CheckErr("store.Users.Get", err)
err = store.Users.Update(user)
utils.CheckErr("store.Users.Update", err)
printUsers([]*users.User{user})
}),
}
-88
View File
@@ -1,88 +0,0 @@
package cmd
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/backend/utils"
)
func mustGetString(flags *pflag.FlagSet, flag string) string {
s, err := flags.GetString(flag)
utils.CheckErr("mustGetString", err)
return s
}
func mustGetBool(flags *pflag.FlagSet, flag string) bool {
b, err := flags.GetBool(flag)
utils.CheckErr("mustGetBool", err)
return b
}
func mustGetUint(flags *pflag.FlagSet, flag string) uint {
b, err := flags.GetUint(flag)
utils.CheckErr("mustGetUint", err)
return b
}
type cobraFunc func(cmd *cobra.Command, args []string)
type pythonFunc func(cmd *cobra.Command, args []string, store *storage.Storage)
func marshal(filename string, data interface{}) error {
fd, err := os.Create(filename)
utils.CheckErr("os.Create", err)
defer fd.Close()
switch ext := filepath.Ext(filename); ext {
case ".json":
encoder := json.NewEncoder(fd)
encoder.SetIndent("", " ")
return encoder.Encode(data)
case ".yml", ".yaml": //nolint:goconst
_, err := yaml.Marshal(fd)
return err
default:
return errors.New("invalid format: " + ext)
}
}
func unmarshal(filename string, data interface{}) error {
fd, err := os.Open(filename)
utils.CheckErr("os.Open", err)
defer fd.Close()
switch ext := filepath.Ext(filename); ext {
case ".json":
return json.NewDecoder(fd).Decode(data)
case ".yml", ".yaml":
return yaml.NewDecoder(fd).Decode(data)
default:
return errors.New("invalid format: " + ext)
}
}
func jsonYamlArg(cmd *cobra.Command, args []string) error {
if err := cobra.ExactArgs(1)(cmd, args); err != nil {
return err
}
switch ext := filepath.Ext(args[0]); ext {
case ".json", ".yml", ".yaml":
return nil
default:
return errors.New("invalid format: " + ext)
}
}
func cobraCmd(fn pythonFunc) cobraFunc {
return func(cmd *cobra.Command, args []string) {
}
}
+5 -5
View File
@@ -2,7 +2,6 @@ package files
import (
"fmt"
"log"
"os"
"path/filepath"
"slices"
@@ -10,6 +9,7 @@ import (
"sync"
"time"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/utils"
)
@@ -52,11 +52,11 @@ func Initialize(source settings.Source) {
if !newIndex.Source.Config.Disabled {
time.Sleep(time.Second)
log.Println("Initializing index and assessing file system complexity")
logger.Info("Initializing index and assessing file system complexity")
newIndex.RunIndexing("/", false)
go newIndex.setupIndexingScanners()
} else {
log.Println("Indexing disabled for source: ", newIndex.Source.Name)
logger.Debug("Indexing disabled for source: " + newIndex.Source.Name)
}
}
@@ -96,7 +96,7 @@ func (idx *Index) indexDirectory(adjustedPath string, quick, recursive bool) err
for _, item := range cacheDirItems {
err = idx.indexDirectory(combinedPath+item.Name, quick, true)
if err != nil {
fmt.Printf("error indexing directory %v : %v", combinedPath+item.Name, err)
logger.Error(fmt.Sprintf("error indexing directory %v : %v", combinedPath+item.Name, err))
}
}
return nil
@@ -147,7 +147,7 @@ func (idx *Index) indexDirectory(adjustedPath string, quick, recursive bool) err
// Recursively index the subdirectory
err = idx.indexDirectory(dirPath, quick, recursive)
if err != nil {
log.Printf("Failed to index directory %s: %v", dirPath, err)
logger.Error(fmt.Sprintf("Failed to index directory %s: %v", dirPath, err))
continue
}
}
+18 -7
View File
@@ -1,8 +1,10 @@
package files
import (
"log"
"fmt"
"time"
"github.com/gtsteffaniak/filebrowser/backend/logger"
)
// schedule in minutes
@@ -32,7 +34,7 @@ func (idx *Index) newScanner(origin string) {
}
// Log and sleep before indexing
log.Printf("Next scan in %v\n", sleepTime)
logger.Debug(fmt.Sprintf("Next scan in %v\n", sleepTime))
time.Sleep(sleepTime)
idx.scannerMu.Lock()
@@ -74,9 +76,9 @@ func (idx *Index) RunIndexing(origin string, quick bool) {
prevNumDirs := idx.NumDirs
prevNumFiles := idx.NumFiles
if quick {
log.Println("Starting quick scan")
logger.Debug("Starting quick scan")
} else {
log.Println("Starting full scan")
logger.Debug("Starting full scan")
idx.NumDirs = 0
idx.NumFiles = 0
}
@@ -85,8 +87,9 @@ func (idx *Index) RunIndexing(origin string, quick bool) {
// Perform the indexing operation
err := idx.indexDirectory("/", quick, true)
if err != nil {
log.Printf("Error during indexing: %v", err)
logger.Error(fmt.Sprintf("Error during indexing: %v", err))
}
firstRun := idx.LastIndexed == time.Time{}
// Update the LastIndexed time
idx.LastIndexed = time.Now()
idx.indexingTime = int(time.Since(startTime).Seconds())
@@ -102,12 +105,20 @@ func (idx *Index) RunIndexing(origin string, quick bool) {
} else {
idx.assessment = "normal"
}
log.Printf("Index assessment : complexity=%v directories=%v files=%v \n", idx.assessment, idx.NumDirs, idx.NumFiles)
if firstRun {
logger.Info(fmt.Sprintf("Index assessment : complexity=%v directories=%v files=%v", idx.assessment, idx.NumDirs, idx.NumFiles))
} else {
logger.Debug(fmt.Sprintf("Index assessment : complexity=%v directories=%v files=%v", idx.assessment, idx.NumDirs, idx.NumFiles))
}
if idx.NumDirs != prevNumDirs || idx.NumFiles != prevNumFiles {
idx.FilesChangedDuringIndexing = true
}
}
log.Printf("Time Spent Indexing : %v seconds\n", idx.indexingTime)
if firstRun {
logger.Info(fmt.Sprintf("Time spent indexing : %v seconds", idx.indexingTime))
} else {
logger.Debug(fmt.Sprintf("Time spent indexing : %v seconds", idx.indexingTime))
}
}
func (idx *Index) setupIndexingScanners() {
+3 -3
View File
@@ -4,7 +4,6 @@ import (
"encoding/json"
libError "errors"
"fmt"
"log"
"net/http"
"net/url"
"os"
@@ -18,6 +17,7 @@ import (
"github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/share"
"github.com/gtsteffaniak/filebrowser/backend/users"
@@ -129,12 +129,12 @@ func signupHandler(w http.ResponseWriter, r *http.Request) {
userHome, err := config.MakeUserDir(user.Username, user.Scope, files.RootPaths["default"])
if err != nil {
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
logger.Error(fmt.Sprintf("create user: failed to mkdir user home dir: [%s]", userHome))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
user.Scope = userHome
log.Printf("new user: %s, home dir: [%s].", user.Username, userHome)
logger.Debug(fmt.Sprintf("new user: %s, home dir: [%s].", user.Username, userHome))
err = store.Users.Save(&user)
if err == errors.ErrExist {
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
+28 -22
View File
@@ -4,7 +4,6 @@ import (
"compress/gzip"
"encoding/json"
"fmt"
"log"
"net/http"
"path/filepath"
"strings"
@@ -12,6 +11,7 @@ import (
"github.com/golang-jwt/jwt/v4"
"github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/runner"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/users"
@@ -131,6 +131,8 @@ func withUserHelper(fn handleFunc) handleFunc {
if err != nil {
return http.StatusInternalServerError, err
}
setUserInResponseWriter(w, data.user)
// Call the handler function, passing in the context
return fn(w, r, data)
}
@@ -172,14 +174,14 @@ func wrapHandler(fn handleFunc) http.HandlerFunc {
// Marshal the error response to JSON
errorBytes, marshalErr := json.Marshal(response)
if marshalErr != nil {
log.Printf("Error marshalling error response: %v", marshalErr)
logger.Error(fmt.Sprintf("Error marshalling error response: %v", marshalErr))
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Write the JSON error response
if _, writeErr := w.Write(errorBytes); writeErr != nil {
log.Printf("Error writing error response: %v", writeErr)
logger.Error(fmt.Sprintf("Error writing error response: %v", writeErr))
}
return
}
@@ -233,6 +235,7 @@ type ResponseWriterWrapper struct {
StatusCode int
wroteHeader bool
PayloadSize int
User string
}
// WriteHeader captures the status code and ensures it's only written once
@@ -255,6 +258,16 @@ func (w *ResponseWriterWrapper) Write(b []byte) (int, error) {
return w.ResponseWriter.Write(b)
}
// Helper function to set the user in the ResponseWriterWrapper
func setUserInResponseWriter(w http.ResponseWriter, user *users.User) {
// Wrap the response writer to set the user field
if wrappedWriter, ok := w.(*ResponseWriterWrapper); ok {
if user != nil {
wrappedWriter.User = user.Username
}
}
}
// LoggingMiddleware logs each request and its status code.
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -267,30 +280,23 @@ func LoggingMiddleware(next http.Handler) http.Handler {
// Call the next handler.
next.ServeHTTP(wrappedWriter, r)
// Determine the color based on the status code.
color := "\033[32m" // Default green color
if wrappedWriter.StatusCode >= 300 && wrappedWriter.StatusCode < 500 {
color = "\033[33m" // Yellow for client errors (4xx)
} else if wrappedWriter.StatusCode >= 500 {
color = "\033[31m" // Red for server errors (5xx)
}
// Capture the full URL path including the query parameters.
fullURL := r.URL.Path
if r.URL.RawQuery != "" {
fullURL += "?" + r.URL.RawQuery
}
// Log the request, status code, and response size.
log.Printf("%s%-7s | %3d | %-15s | %-12s | \"%s\"%s",
color,
r.Method,
wrappedWriter.StatusCode, // Captured status code
r.RemoteAddr,
time.Since(start).String(),
fullURL,
"\033[0m", // Reset color
)
truncUser := wrappedWriter.User
if len(truncUser) > 12 {
truncUser = truncUser[:10] + ".."
}
logger.Api(
fmt.Sprintf("%-7s | %3d | %-15s | %-12s | %-12s | \"%s\"",
r.Method,
wrappedWriter.StatusCode, // Captured status code
r.RemoteAddr,
truncUser,
time.Since(start).String(),
fullURL), wrappedWriter.StatusCode)
})
}
+2 -1
View File
@@ -13,6 +13,7 @@ import (
"github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/backend/img"
"github.com/gtsteffaniak/filebrowser/backend/logger"
)
type ImgService interface {
@@ -141,7 +142,7 @@ func createPreview(imgSvc ImgService, fileCache FileCache, file files.ExtendedFi
go func() {
cacheKey := previewCacheKey(file.RealPath, previewSize, file.FileInfo.ModTime)
if err := fileCache.Store(context.Background(), cacheKey, buf.Bytes()); err != nil {
fmt.Printf("failed to cache resized image: %v", err)
logger.Error(fmt.Sprintf("failed to cache resized image: %v", err))
}
}()
+6 -5
View File
@@ -7,7 +7,6 @@ import (
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
@@ -15,6 +14,7 @@ import (
"strings"
"github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/backend/logger"
)
func setContentDisposition(w http.ResponseWriter, r *http.Request, fileName string) {
@@ -211,13 +211,14 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
default:
return http.StatusInternalServerError, errors.New("format not implemented")
}
baseDirName := filepath.Base(filepath.Dir(realPath))
if baseDirName == "" || baseDirName == "/" {
baseDirName = "download"
}
if len(fileList) == 1 && isDir {
baseDirName = filepath.Base(realPath)
}
downloadFileName := url.PathEscape(baseDirName + extension)
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+downloadFileName)
// Create the archive and stream it directly to the response
if extension == ".zip" {
@@ -242,7 +243,7 @@ func createZip(w io.Writer, d *requestContext, filenames ...string) error {
for _, fname := range filenames {
err := addFile(fname, d, nil, zipWriter, false)
if err != nil {
log.Printf("Failed to add %s to ZIP: %v", fname, err)
logger.Error(fmt.Sprintf("Failed to add %s to ZIP: %v", fname, err))
}
}
@@ -261,7 +262,7 @@ func createTarGz(w io.Writer, d *requestContext, filenames ...string) error {
for _, fname := range filenames {
err := addFile(fname, d, tarWriter, nil, false)
if err != nil {
log.Printf("Failed to add %s to TAR.GZ: %v", fname, err)
logger.Error(fmt.Sprintf("Failed to add %s to TAR.GZ: %v", fname, err))
}
}
+8 -8
View File
@@ -5,11 +5,11 @@ import (
"embed"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"text/template"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/backend/version"
@@ -56,7 +56,7 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
// Embedded mode: Serve files from the embedded assets
assetFs, err = fs.Sub(assets, "embed")
if err != nil {
log.Fatal("Could not embed frontend. Does dist exist?")
logger.Fatal("Could not embed frontend. Does dist exist?")
}
} else {
assetFs = dirFS{Dir: http.Dir("http/dist")}
@@ -143,7 +143,7 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
// Load the TLS certificate and key
cer, err := tls.LoadX509KeyPair(config.Server.TLSCert, config.Server.TLSKey)
if err != nil {
log.Fatalf("could not load certificate: %v", err)
logger.Fatal(fmt.Sprintf("could not load certificate: %v", err))
}
// Create a custom TLS listener
@@ -158,17 +158,17 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
// Listen on TCP and wrap with TLS
listener, err := tls.Listen("tcp", fmt.Sprintf(":%v", config.Server.Port), tlsConfig)
if err != nil {
log.Fatalf("could not start TLS server: %v", err)
logger.Fatal(fmt.Sprintf("could not start TLS server: %v", err))
}
if config.Server.Port != 443 {
port = fmt.Sprintf(":%d", config.Server.Port)
}
// Build the full URL with host and port
fullURL := fmt.Sprintf("%s://localhost%s%s", scheme, port, config.Server.BaseURL)
log.Printf("Running at : %s", fullURL)
logger.Info(fmt.Sprintf("Running at : %s", fullURL))
err = http.Serve(listener, muxWithMiddleware(router))
if err != nil {
log.Fatalf("could not start server: %v", err)
logger.Fatal(fmt.Sprintf("could not start server: %v", err))
}
} else {
// Set HTTP scheme and the default port for HTTP
@@ -178,10 +178,10 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
}
// Build the full URL with host and port
fullURL := fmt.Sprintf("%s://localhost%s%s", scheme, port, config.Server.BaseURL)
log.Printf("Running at : %s", fullURL)
logger.Info(fmt.Sprintf("Running at : %s", fullURL))
err := http.ListenAndServe(fmt.Sprintf(":%v", config.Server.Port), muxWithMiddleware(router))
if err != nil {
log.Fatalf("could not start server: %v", err)
logger.Fatal(fmt.Sprintf("could not start server: %v", err))
}
}
}
+2 -2
View File
@@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
@@ -12,6 +11,7 @@ import (
"text/template"
"github.com/gtsteffaniak/filebrowser/backend/auth"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/version"
)
@@ -71,7 +71,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
_, err := os.Stat(fPath) //nolint:govet
if err != nil && !os.IsNotExist(err) {
log.Printf("couldn't load custom styles: %v", err)
logger.Error(fmt.Sprintf("couldn't load custom styles: %v", err))
}
if err == nil {
+132
View File
@@ -0,0 +1,132 @@
package logger
import (
"fmt"
"io"
"log"
"os"
"slices"
"strings"
)
// 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
}
var stdOutLoggerExists bool
// NewLogger creates a new Logger instance with separate file and stdout loggers
func NewLogger(filepath string, levels, apiLevels []LogLevel, noColors bool) (*Logger, error) {
var fileWriter io.Writer = io.Discard
stdout := filepath == ""
// Configure file logging
if !stdout {
file, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return nil, fmt.Errorf("failed to open log file: %v", err)
}
fileWriter = file
}
flags := log.Ldate | log.Ltime
if slices.Contains(levels, DEBUG) {
flags |= log.Lshortfile
}
logger := log.New(os.Stdout, "", flags)
if filepath != "" {
logger = log.New(fileWriter, "", flags)
}
if stdout {
stdOutLoggerExists = true
}
return &Logger{
logger: logger,
levels: levels,
apiLevels: apiLevels,
disabled: slices.Contains(levels, DISABLED),
disabledAPI: slices.Contains(apiLevels, DISABLED),
colors: !noColors,
stdout: stdout,
}, nil
}
// SetupLogger configures the logger with file and stdout options and their respective log levels
func SetupLogger(output, levels, apiLevels string, noColors bool) error {
upperLevels := []LogLevel{}
for _, level := range SplitByMultiple(levels) {
if level == "" {
break
}
upperLevel := strings.ToUpper(level)
if upperLevel == "WARNING" || upperLevel == "WARN" {
upperLevel = "WARN "
}
// Convert level strings to LogLevel
level, ok := stringToLevel[upperLevel]
if !ok {
loggers = []*Logger{}
return fmt.Errorf("invalid file log level: %s", upperLevel)
}
upperLevels = append(upperLevels, level)
}
if len(upperLevels) == 0 {
upperLevels = []LogLevel{INFO, ERROR, WARNING}
}
upperApiLevels := []LogLevel{}
for _, level := range SplitByMultiple(apiLevels) {
if level == "" {
break
}
upperLevel := strings.ToUpper(level)
if upperLevel == "WARNING" || upperLevel == "WARN" {
upperLevel = "WARN "
}
// Convert level strings to LogLevel
level, ok := stringToLevel[strings.ToUpper(upperLevel)]
if !ok {
return fmt.Errorf("invalid api log level: %s", upperLevel)
}
upperApiLevels = append(upperApiLevels, level)
}
if len(upperApiLevels) == 0 {
upperApiLevels = []LogLevel{INFO, ERROR, WARNING}
}
if slices.Contains(upperLevels, DISABLED) && slices.Contains(upperApiLevels, DISABLED) {
// both disabled, not creating a logger
loggers = []*Logger{}
return nil
}
outputStdout := strings.ToUpper(output)
if outputStdout == "STDOUT" {
output = ""
}
if output == "" && stdOutLoggerExists {
// stdout logger already exists... don't create another
return fmt.Errorf("stdout logger already exists, could not set config levels=[%v] apiLevels=[%v] noColors=[%v]", levels, apiLevels, noColors)
}
// Create the logger
logger, err := NewLogger(output, upperLevels, upperApiLevels, noColors)
if err != nil {
loggers = []*Logger{}
return err
}
loggers = append(loggers, logger)
return nil
}
func SplitByMultiple(str string) []string {
delimiters := []rune{'|', ',', ' '}
return strings.FieldsFunc(str, func(r rune) bool {
for _, d := range delimiters {
if r == d {
return true
}
}
return false
})
}
+137
View File
@@ -0,0 +1,137 @@
package logger
import (
"fmt"
"log"
"slices"
)
type LogLevel int
const (
DISABLED LogLevel = 0
ERROR LogLevel = 1
FATAL LogLevel = 1
WARNING LogLevel = 2
INFO LogLevel = 3
DEBUG LogLevel = 4
API LogLevel = 10
// COLORS
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
GRAY = "\033[37m"
)
var (
loggers []*Logger
)
type levelConsts struct {
INFO string
FATAL string
ERROR string
WARNING string
DEBUG string
API string
DISABLED string
}
var levels = levelConsts{
INFO: "INFO",
FATAL: "FATAL",
ERROR: "ERROR",
WARNING: "WARN ", // with consistent space padding
DEBUG: "DEBUG",
DISABLED: "DISABLED",
API: "API",
}
// stringToLevel maps string representation to LogLevel
var stringToLevel = map[string]LogLevel{
"DEBUG": DEBUG,
"INFO": INFO,
"ERROR": ERROR,
"DISABLED": DISABLED,
"WARN ": WARNING, // with consistent space padding
"FATAL": FATAL,
"API": API,
}
// Log prints a log message if its level is greater than or equal to the logger's levels
func Log(level string, msg string, prefix, api bool, color string) {
LEVEL := stringToLevel[level]
for _, logger := range loggers {
if api {
if logger.disabledAPI || !slices.Contains(logger.apiLevels, LEVEL) {
continue
}
} else {
if logger.disabled || !slices.Contains(logger.levels, LEVEL) {
continue
}
}
if logger.stdout && LEVEL == FATAL {
continue
}
writeOut := msg
if prefix {
writeOut = fmt.Sprintf("[%s] ", level) + writeOut
}
if logger.colors && color != "" {
writeOut = color + writeOut + "\033[0m"
}
err := logger.logger.Output(3, writeOut) // 3 skips this function for correct file:line
if err != nil {
log.Printf("failed to log message '%v' with error `%v`", msg, err)
}
}
}
func Api(msg string, statusCode int) {
if statusCode >= 300 && statusCode < 500 {
Log(levels.WARNING, msg, false, true, YELLOW)
} else if statusCode >= 500 {
Log(levels.ERROR, msg, false, true, RED)
} else {
Log(levels.INFO, msg, false, true, GREEN)
}
}
// Helper methods for specific log levels
func Debug(msg string) {
if len(loggers) > 0 {
Log(levels.DEBUG, msg, true, false, GRAY)
}
}
func Info(msg string) {
if len(loggers) > 0 {
Log(levels.INFO, msg, false, false, "")
} else {
log.Println(msg)
}
}
func Warning(msg string) {
if len(loggers) > 0 {
Log(levels.WARNING, msg, true, false, YELLOW)
} else {
log.Println("[WARN ]: " + msg)
}
}
func Error(msg string) {
if len(loggers) > 0 {
Log(levels.ERROR, msg, true, false, RED)
} else {
log.Println("[ERROR] : ", msg)
}
}
func Fatal(msg string) {
if len(loggers) > 0 {
Log(levels.FATAL, msg, true, false, RED)
}
log.Fatal("[FATAL] : ", msg)
}
-17
View File
@@ -1,17 +0,0 @@
#!/bin/sh
## TEST file used by docker testing containers
checkExit() {
if [ "$?" -ne 0 ];then
exit 1
fi
}
if command -v go &> /dev/null
then
printf "\n == Running tests == \n"
go test -race -parallel -v ./...
checkExit
else
echo "ERROR: unable to perform tests"
exit 1
fi
-14
View File
@@ -1,14 +0,0 @@
#!/usr/bin/env bash
set -e
if ! [ -x "$(command -v standard-version)" ]; then
echo "standard-version is not installed. please run 'npm i -g standard-version'"
exit 1
fi
standard-version --dry-run --skip
read -p "Continue (y/n)? " -n 1 -r
echo ;
if [[ $REPLY =~ ^[Yy]$ ]]; then
standard-version -s ;
fi
-11
View File
@@ -1,11 +0,0 @@
#!/usr/bin/env bash
set -e
if ! [ -x "$(command -v commitlint)" ]; then
echo "commitlint is not installed. please run 'npm i -g commitlint'"
exit 1
fi
for commit_hash in $(git log --pretty=format:%H origin/master..HEAD); do
commitlint -f ${commit_hash}~1 -t ${commit_hash}
done
+29 -12
View File
@@ -8,6 +8,7 @@ import (
"strings"
"github.com/goccy/go-yaml"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/backend/version"
)
@@ -19,25 +20,25 @@ func Initialize(configFile string) {
Config = setDefaults()
err := yaml.Unmarshal(yamlData, &Config)
if err != nil {
log.Fatalf("Error unmarshaling YAML data: %v", err)
logger.Fatal(fmt.Sprintf("Error unmarshaling YAML data: %v", err))
}
Config.UserDefaults.Perm = Config.UserDefaults.Permissions
// Convert relative path to absolute path
if len(Config.Server.Sources) > 0 {
// TODO allow multipe sources not named default
for _, source := range Config.Server.Sources {
realPath, err := filepath.Abs(source.Path)
if err != nil {
log.Fatalf("Error getting source path: %v", err)
realPath, err2 := filepath.Abs(source.Path)
if err2 != nil {
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
}
} else {
realPath, err := filepath.Abs(Config.Server.Root)
if err != nil {
log.Fatalf("Error getting source path: %v", err)
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": {
@@ -67,26 +68,43 @@ func Initialize(configFile string) {
Url: "https://github.com/gtsteffaniak/filebrowser/wiki",
})
}
if len(Config.Server.Logging) == 0 {
Config.Server.Logging = []LogConfig{
{
Output: "stdout",
},
}
}
for _, logConfig := range Config.Server.Logging {
err = logger.SetupLogger(
logConfig.Output,
logConfig.Levels,
logConfig.ApiLevels,
logConfig.NoColors,
)
if err != nil {
log.Println("[ERROR] Failed to set up logger:", err)
}
}
}
func loadConfigFile(configFile string) []byte {
// Open and read the YAML file
yamlFile, err := os.Open(configFile)
if err != nil {
log.Println(err)
os.Exit(1)
logger.Fatal(fmt.Sprintf("%v", err))
}
defer yamlFile.Close()
stat, err := yamlFile.Stat()
if err != nil {
log.Fatalf("error getting file information: %s", err.Error())
logger.Fatal(fmt.Sprintf("error getting file information: %s", err.Error()))
}
yamlData := make([]byte, stat.Size())
_, err = yamlFile.Read(yamlData)
if err != nil {
log.Fatalf("Error reading YAML data: %v", err)
logger.Fatal(fmt.Sprintf("Error reading YAML data: %v", err))
}
return yamlData
}
@@ -101,7 +119,6 @@ func setDefaults() Settings {
NumImageProcessors: 4,
BaseURL: "",
Database: "database.db",
Log: "stdout",
Root: ".",
},
Auth: Auth{
+3 -2
View File
@@ -3,12 +3,13 @@ package settings
import (
"errors"
"fmt"
"log"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/gtsteffaniak/filebrowser/backend/logger"
)
var (
@@ -22,7 +23,7 @@ func (s *Settings) MakeUserDir(username, userScope, serverRoot string) (string,
if userScope == "" && s.Server.CreateUserDir {
username = cleanUsername(username)
if username == "" || username == "-" || username == "." {
log.Printf("create user: invalid user for home dir creation: [%s]", username)
logger.Error(fmt.Sprintf("create user: invalid user for home dir creation: [%s]", username))
return "", errors.New("invalid user for home dir creation")
}
userScope = path.Join(s.Server.UserHomeBasePath, username)
+4 -3
View File
@@ -1,11 +1,12 @@
package settings
import (
"log"
"fmt"
"testing"
"github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/gtsteffaniak/filebrowser/backend/logger"
)
func TestConfigLoadChanged(t *testing.T) {
@@ -16,7 +17,7 @@ func TestConfigLoadChanged(t *testing.T) {
err := yaml.Unmarshal(yamlData, &newConfig)
if err != nil {
log.Fatalf("Error unmarshaling YAML data: %v", err)
logger.Fatal(fmt.Sprintf("Error unmarshaling YAML data: %v", err))
}
// Use go-cmp to compare the two structs
if diff := cmp.Diff(newConfig, Config); diff == "" {
@@ -32,7 +33,7 @@ func TestConfigLoadSpecificValues(t *testing.T) {
err := yaml.Unmarshal(yamlData, &newConfig)
if err != nil {
log.Fatalf("Error unmarshaling YAML data: %v", err)
logger.Fatal(fmt.Sprintf("Error unmarshaling YAML data: %v", err))
}
testCases := []struct {
fieldName string
+9 -1
View File
@@ -47,7 +47,7 @@ type Server struct {
Port int `json:"port"`
BaseURL string `json:"baseURL"`
Address string `json:"address"`
Log string `json:"log"`
Logging []LogConfig `json:"logging"`
Database string `json:"database"`
Root string `json:"root"`
UserHomeBasePath string `json:"userHomeBasePath"`
@@ -56,6 +56,14 @@ type Server struct {
ExternalUrl string `json:"externalUrl"`
}
type LogConfig struct {
Levels string `json:"levels"`
ApiLevels string `json:"apiLevels"`
Output string `json:"output"`
NoColors bool `json:"noColors"`
Json bool `json:"json"`
}
type Source struct {
Path string `json:"path"`
Name string
-1
View File
@@ -10,7 +10,6 @@ server:
port: 80
baseURL: "/"
address: ""
log: "stdout"
database: "mydb.db"
root: "/srv"
auth:
+3 -2
View File
@@ -1,13 +1,14 @@
package bolt
import (
"log"
"fmt"
"time"
"github.com/asdine/storm/v3"
"github.com/asdine/storm/v3/q"
"github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/share"
)
@@ -68,7 +69,7 @@ func (s shareBackend) Gets(path string, id uint) ([]*share.Link, error) {
if v[i].Expire < time.Now().Unix() {
err = s.Delete(v[i].PasswordHash)
if err != nil {
log.Println("expired share could not be deleted: ", err.Error())
logger.Error(fmt.Sprintf("expired share could not be deleted: %v", err.Error()))
}
} else {
filteredList = append(filteredList, v[i])
+4 -4
View File
@@ -2,7 +2,6 @@ package storage
import (
"fmt"
"log"
"os"
"path/filepath"
@@ -10,6 +9,7 @@ import (
"github.com/gtsteffaniak/filebrowser/backend/auth"
"github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/share"
"github.com/gtsteffaniak/filebrowser/backend/storage/bolt"
@@ -118,15 +118,15 @@ func CreateUser(userInfo users.User, asAdmin bool) error {
// create new home directory
userHome, err := settings.Config.MakeUserDir(newUser.Username, newUser.Scope, files.RootPaths["default"])
if err != nil {
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
logger.Error(fmt.Sprintf("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)
logger.Debug(fmt.Sprintf("user: %s, home dir: [%s].", newUser.Username, userHome))
idx := files.GetIndex("default")
_, _, err = idx.GetRealPath(newUser.Scope)
if err != nil {
log.Println("user path is not valid", newUser.Scope)
logger.Error(fmt.Sprintf("user path is not valid: %v", newUser.Scope))
return nil
}
err = store.Users.Save(&newUser)
View File
Whitespace-only changes.
+5 -4
View File
@@ -3,16 +3,17 @@ package utils
import (
"crypto/rand"
"fmt"
"log"
math "math/rand"
"reflect"
"strings"
"time"
"github.com/gtsteffaniak/filebrowser/backend/logger"
)
func CheckErr(source string, err error) {
if err != nil {
log.Fatalf("%s: %v", source, err)
logger.Fatal(fmt.Sprintf("%s: %v", source, err))
}
}
@@ -49,7 +50,7 @@ func PrintStructFields(v interface{}) {
// Ensure the input is a struct
if val.Kind() != reflect.Struct {
fmt.Println("Provided value is not a struct")
logger.Debug("Provided value is not a struct")
return
}
@@ -66,7 +67,7 @@ func PrintStructFields(v interface{}) {
fieldValue = fieldValue[:100] + "..."
}
fmt.Printf("Field: %s, %s\n", fieldType.Name, fieldValue)
logger.Debug(fmt.Sprintf("Field: %s, %s\n", fieldType.Name, fieldValue))
}
}
+1
View File
@@ -110,6 +110,7 @@ export default {
event.preventDefault();
try {
if (this.isNew) {
await usersApi.create(this.userPayload); // Use the computed property
this.$router.push({ path: "/settings", hash: "#users-main" });
notify.showSuccess(this.$t("settings.userCreated"));
} else {