Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1a73a8b18 | ||
|
|
2b5d6cbb99 | ||
|
|
364f391017 | ||
|
|
c13861e13c | ||
|
|
e6b750add5 | ||
|
|
70d59ec03e | ||
|
|
bf37f88c32 | ||
|
|
7354eb6cf9 | ||
|
|
10684e5390 | ||
|
|
58fe817349 |
10
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
10
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -20,22 +20,32 @@ body:
|
||||
render: Text
|
||||
description: |
|
||||
Enter the version of FileBrowser you are using.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: |
|
||||
A clear and concise description of what the issue is about. What are you trying to do?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What did you expect to happen?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What actually happened?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
description: |
|
||||
Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behavior as minimally as possible?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Files
|
||||
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -2,6 +2,21 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
### [2.34.2](https://github.com/filebrowser/filebrowser/compare/v2.34.1...v2.34.2) (2025-06-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* mitigate unprotected shares ([2b5d6cb](https://github.com/filebrowser/filebrowser/commit/2b5d6cbb996a61a769acc56af0acc12eec2d8d8f))
|
||||
|
||||
### [2.34.1](https://github.com/filebrowser/filebrowser/compare/v2.34.0...v2.34.1) (2025-06-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* exclude to-be-moved folder from move dialog ([#5235](https://github.com/filebrowser/filebrowser/issues/5235)) ([7354eb6](https://github.com/filebrowser/filebrowser/commit/7354eb6cf966244141277c2808988855c004f908))
|
||||
* passthrough the minimum password length ([#5236](https://github.com/filebrowser/filebrowser/issues/5236)) ([bf37f88](https://github.com/filebrowser/filebrowser/commit/bf37f88c32222ad9c186482bb97338a9c9b4a93c))
|
||||
|
||||
## [2.34.0](https://github.com/filebrowser/filebrowser/compare/v2.33.10...v2.34.0) (2025-06-29)
|
||||
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -187,7 +187,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2018 File Browser contributors
|
||||
Copyright 2018 File Browser Contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -28,3 +28,11 @@ Documentation on how to install, configure, and contribute to this project is ho
|
||||
|
||||
[issues]: https://github.com/filebrowser/filebrowser/issues
|
||||
[discussions]: https://github.com/filebrowser/filebrowser/discussions
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are always welcome. To start contributing to this project, read our [guidelines](CONTRIBUTING.md) first.
|
||||
|
||||
## License
|
||||
|
||||
[Apache License 2.0](LICENSE) © File Browser Contributors
|
||||
|
||||
@@ -150,7 +150,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||
}
|
||||
|
||||
if u == nil {
|
||||
pass, err := users.HashAndValidatePwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
|
||||
pass, err := users.ValidateAndHashPwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -186,7 +186,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||
|
||||
// update the password when it doesn't match the current
|
||||
if p {
|
||||
pass, err := users.HashAndValidatePwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
|
||||
pass, err := users.ValidateAndHashPwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func (a ProxyAuth) createUser(usr users.Store, setting *settings.Settings, srv *
|
||||
}
|
||||
|
||||
var hashedRandomPassword string
|
||||
hashedRandomPassword, err = users.HashAndValidatePwd(pwd, setting.MinimumPasswordLength)
|
||||
hashedRandomPassword, err = users.ValidateAndHashPwd(pwd, setting.MinimumPasswordLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -432,7 +432,7 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
||||
|
||||
log.Println("Randomly generated password for user 'admin':", pwd)
|
||||
|
||||
password, err = users.HashAndValidatePwd(pwd, set.MinimumPasswordLength)
|
||||
password, err = users.ValidateAndHashPwd(pwd, set.MinimumPasswordLength)
|
||||
checkErr(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ var usersAddCmd = &cobra.Command{
|
||||
checkErr(err)
|
||||
getUserDefaults(cmd.Flags(), &s.Defaults, false)
|
||||
|
||||
password, err := users.HashAndValidatePwd(args[1], s.MinimumPasswordLength)
|
||||
password, err := users.ValidateAndHashPwd(args[1], s.MinimumPasswordLength)
|
||||
checkErr(err)
|
||||
|
||||
user := &users.User{
|
||||
|
||||
@@ -66,7 +66,7 @@ options you want to change.`,
|
||||
}
|
||||
|
||||
if password != "" {
|
||||
user.Password, err = users.HashAndValidatePwd(password, s.MinimumPasswordLength)
|
||||
user.Password, err = users.ValidateAndHashPwd(password, s.MinimumPasswordLength)
|
||||
checkErr(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package errors
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyKey = errors.New("empty key")
|
||||
ErrExist = errors.New("the resource already exists")
|
||||
ErrNotExist = errors.New("the resource does not exist")
|
||||
ErrEmptyPassword = errors.New("password is empty")
|
||||
ErrShortPassword = errors.New("password is too short")
|
||||
ErrEasyPassword = errors.New("password is too easy")
|
||||
ErrEmptyUsername = errors.New("username is empty")
|
||||
ErrEmptyRequest = errors.New("empty request")
|
||||
ErrScopeIsRelative = errors.New("scope is a relative path")
|
||||
@@ -20,3 +23,11 @@ var (
|
||||
ErrSourceIsParent = errors.New("source is parent")
|
||||
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
|
||||
)
|
||||
|
||||
type ErrShortPassword struct {
|
||||
MinimumLength uint
|
||||
}
|
||||
|
||||
func (e ErrShortPassword) Error() string {
|
||||
return fmt.Sprintf("password is too short, minimum length is %d", e.MinimumLength)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,12 @@ import { StatusError } from "@/api/utils.js";
|
||||
|
||||
export default {
|
||||
name: "file-list",
|
||||
props: {
|
||||
exclude: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
items: [],
|
||||
@@ -90,6 +96,7 @@ export default {
|
||||
// move options.
|
||||
for (const item of req.items) {
|
||||
if (!item.isDir) continue;
|
||||
if (this.exclude?.includes(item.url)) continue;
|
||||
|
||||
this.items.push({
|
||||
name: item.name,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<file-list
|
||||
ref="fileList"
|
||||
@update:selected="(val) => (dest = val)"
|
||||
:exclude="excludedFolders"
|
||||
tabindex="1"
|
||||
/>
|
||||
</div>
|
||||
@@ -76,6 +77,11 @@ export default {
|
||||
computed: {
|
||||
...mapState(useFileStore, ["req", "selected"]),
|
||||
...mapState(useAuthStore, ["user"]),
|
||||
excludedFolders() {
|
||||
return this.selected
|
||||
.filter((idx) => this.req.items[idx].isDir)
|
||||
.map((idx) => this.req.items[idx].url);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
||||
|
||||
@@ -32,16 +32,6 @@
|
||||
<i class="material-icons">content_paste</i>
|
||||
</button>
|
||||
</td>
|
||||
<td class="small" v-if="hasDownloadLink()">
|
||||
<button
|
||||
class="action copy-clipboard"
|
||||
:aria-label="$t('buttons.copyDownloadLinkToClipboard')"
|
||||
:title="$t('buttons.copyDownloadLinkToClipboard')"
|
||||
@click="copyToClipboard(buildDownloadLink(link))"
|
||||
>
|
||||
<i class="material-icons">content_paste_go</i>
|
||||
</button>
|
||||
</td>
|
||||
<td class="small">
|
||||
<button
|
||||
class="action"
|
||||
@@ -142,7 +132,7 @@
|
||||
<script>
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { share as api, pub as pub_api } from "@/api";
|
||||
import { share as api } from "@/api";
|
||||
import dayjs from "dayjs";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { copy } from "@/utils/clipboard";
|
||||
@@ -257,14 +247,6 @@ export default {
|
||||
buildLink(share) {
|
||||
return api.getShareURL(share);
|
||||
},
|
||||
hasDownloadLink() {
|
||||
return (
|
||||
this.selected.length === 1 && !this.req.items[this.selected[0]].isDir
|
||||
);
|
||||
},
|
||||
buildDownloadLink(share) {
|
||||
return pub_api.getDownloadURL(share);
|
||||
},
|
||||
sort() {
|
||||
this.links = this.links.sort((a, b) => {
|
||||
if (a.expire === 0) return -1;
|
||||
|
||||
@@ -151,9 +151,9 @@ var signupHandler = func(_ http.ResponseWriter, r *http.Request, d *data) (int,
|
||||
|
||||
d.settings.Defaults.Apply(user)
|
||||
|
||||
pwd, err := users.HashAndValidatePwd(info.Password, d.settings.MinimumPasswordLength)
|
||||
pwd, err := users.ValidateAndHashPwd(info.Password, d.settings.MinimumPasswordLength)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
user.Password = pwd
|
||||
|
||||
@@ -125,13 +125,9 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
|
||||
return http.StatusBadRequest, fbErrors.ErrEmptyPassword
|
||||
}
|
||||
|
||||
if len(req.Data.Password) < int(d.settings.MinimumPasswordLength) {
|
||||
return http.StatusBadRequest, fbErrors.ErrShortPassword
|
||||
}
|
||||
|
||||
req.Data.Password, err = users.HashAndValidatePwd(req.Data.Password, d.settings.MinimumPasswordLength)
|
||||
req.Data.Password, err = users.ValidateAndHashPwd(req.Data.Password, d.settings.MinimumPasswordLength)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
userHome, err := d.settings.MakeUserDir(req.Data.Username, req.Data.Scope, d.server.Root)
|
||||
@@ -167,17 +163,19 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
if req.Data.Password != "" {
|
||||
req.Data.Password, err = users.HashAndValidatePwd(req.Data.Password, d.settings.MinimumPasswordLength)
|
||||
req.Data.Password, err = users.ValidateAndHashPwd(req.Data.Password, d.settings.MinimumPasswordLength)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
} else {
|
||||
var suser *users.User
|
||||
suser, err = d.store.Users.Get(d.server.Root, d.raw.(uint))
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
req.Data.Password = suser.Password
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
req.Which = []string{}
|
||||
}
|
||||
|
||||
@@ -190,13 +188,9 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
if len(req.Data.Password) < int(d.settings.MinimumPasswordLength) {
|
||||
return http.StatusBadRequest, fbErrors.ErrShortPassword
|
||||
}
|
||||
|
||||
req.Data.Password, err = users.HashAndValidatePwd(req.Data.Password, d.settings.MinimumPasswordLength)
|
||||
req.Data.Password, err = users.ValidateAndHashPwd(req.Data.Password, d.settings.MinimumPasswordLength)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
26
users/assets.go
Normal file
26
users/assets.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed assets
|
||||
var assets embed.FS
|
||||
var commonPasswords map[string]struct{}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
// Password list sourced from:
|
||||
// https://github.com/danielmiessler/SecLists/blob/master/Passwords/Common-Credentials/100k-most-used-passwords-NCSC.txt
|
||||
data, err := assets.ReadFile("assets/common-passwords.txt")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
passwords := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||
commonPasswords = make(map[string]struct{}, len(passwords))
|
||||
for _, password := range passwords {
|
||||
commonPasswords[strings.TrimSpace(password)] = struct{}{}
|
||||
}
|
||||
}
|
||||
100000
users/assets/common-passwords.txt
Normal file
100000
users/assets/common-passwords.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,14 @@ import (
|
||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||
)
|
||||
|
||||
// HashPwd hashes a password.
|
||||
func HashAndValidatePwd(password string, minimumLength uint) (string, error) {
|
||||
// ValidateAndHashPwd validates and hashes a password.
|
||||
func ValidateAndHashPwd(password string, minimumLength uint) (string, error) {
|
||||
if uint(len(password)) < minimumLength {
|
||||
return "", fbErrors.ErrShortPassword
|
||||
return "", fbErrors.ErrShortPassword{MinimumLength: minimumLength}
|
||||
}
|
||||
|
||||
if _, ok := commonPasswords[password]; ok {
|
||||
return "", fbErrors.ErrEasyPassword
|
||||
}
|
||||
|
||||
return HashPwd(password)
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
<img src="https://raw.githubusercontent.com/filebrowser/logo/master/banner.png" width="550"/>
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> This project is currently on **maintenance-only** mode. For more information, read the information on [GitHub](https://github.com/filebrowser/filebrowser#project-status).
|
||||
|
||||

|
||||
|
||||
File Browser provides a file managing interface within a specified directory and it can be used to upload, delete, preview and edit your files. It is a **create-your-own-cloud**-kind of software where you can just install it on your server, direct it to a path and access your files through a nice web interface.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -1,75 +1,68 @@
|
||||
# Installation
|
||||
|
||||
File Browser is a single binary and can be used as a standalone executable. Although, some might prefer to use it with [Docker](https://www.docker.com) or [Caddy](https://caddyserver.com), which is a fantastic web server that enables HTTPS by default. Its installation is quite straightforward independently on which system you want to use.
|
||||
File Browser is a single binary and can be used as standalone executable. However, it is also available as a [Docker](https://www.docker.com) image. The installation and first time setup is quite straightforward independently of which system you use.
|
||||
|
||||
## Quick Setup
|
||||
## Binary
|
||||
|
||||
The quickest way for beginners to start using File Browser is by opening your terminal and executing the following commands:
|
||||
The quickest and easiest way to install File Browser is to use a package manager, or our download script, which automatically fetches the latest version of File Browser for your platform.
|
||||
|
||||
### Brew
|
||||
=== "Brew"
|
||||
|
||||
```sh
|
||||
brew tap filebrowser/tap
|
||||
brew install filebrowser
|
||||
filebrowser -r /path/to/your/files
|
||||
```
|
||||
```sh
|
||||
brew tap filebrowser/tap
|
||||
brew install filebrowser
|
||||
filebrowser -r /path/to/your/files
|
||||
```
|
||||
|
||||
### Unix
|
||||
=== "Unix"
|
||||
|
||||
```sh
|
||||
curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
|
||||
filebrowser -r /path/to/your/files
|
||||
```
|
||||
```sh
|
||||
curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
|
||||
filebrowser -r /path/to/your/files
|
||||
```
|
||||
|
||||
### Windows
|
||||
=== "Windows"
|
||||
|
||||
```sh
|
||||
iwr -useb https://raw.githubusercontent.com/filebrowser/get/master/get.ps1 | iex
|
||||
filebrowser -r /path/to/your/files
|
||||
```
|
||||
```sh
|
||||
iwr -useb https://raw.githubusercontent.com/filebrowser/get/master/get.ps1 | iex
|
||||
filebrowser -r /path/to/your/files
|
||||
```
|
||||
|
||||
### Configuring
|
||||
|
||||
Done! It will bootstrap a database in which all the configurations and users are stored. Now, you can see on your command line the address in which your instance is running. You just need to go to that URL and use the following credentials:
|
||||
|
||||
* Username: `admin`
|
||||
* Password: (printed in your console)
|
||||
|
||||
Although this is the fastest way to bootstrap an instance, we recommend you to take a look at other possible options, by checking `config init --help` and `config set --help`, to make the installation as safe and customized as it can be.
|
||||
File Browser is now up and running. Read some [first boot](#first-boot) for more information.
|
||||
|
||||
## Docker
|
||||
|
||||
File Browser is available as two different Docker images, which can be found on [Docker Hub](https://hub.docker.com/r/filebrowser/filebrowser).
|
||||
|
||||
### Alpine
|
||||
=== "Alpine"
|
||||
|
||||
```sh
|
||||
docker run \
|
||||
-v /path/to/srv:/srv \
|
||||
-v /path/to/database:/database \
|
||||
-v /path/to/config:/config \
|
||||
-p 8080:80 \
|
||||
filebrowser/filebrowser
|
||||
```
|
||||
The
|
||||
|
||||
The default user has PID 1000 and GID 1000. Please make sure that this user has access to the different mounted volumes. To change the user running inside the Docker image, you need to use the [`--user` flag](https://docs.docker.com/engine/containers/run/#user).
|
||||
```sh
|
||||
docker run \
|
||||
-v /path/to/srv:/srv \
|
||||
-v /path/to/database:/database \
|
||||
-v /path/to/config:/config \
|
||||
-p 8080:80 \
|
||||
filebrowser/filebrowser
|
||||
```
|
||||
|
||||
### s6 overlay
|
||||
The default user has PID 1000 and GID 1000. Please make sure that this user has access to the different mounted volumes. To change the user running inside the Docker image, you need to use the [`--user` flag](https://docs.docker.com/engine/containers/run/#user).
|
||||
|
||||
The `s6` image is based on LinuxServer and leverages the [s6-overlay](https://github.com/just-containers/s6-overlay) system for a standard, highly customizable image. It should be used as follows:
|
||||
=== "s6 overlay"
|
||||
|
||||
```shell
|
||||
docker run \
|
||||
-v /path/to/srv:/srv \
|
||||
-v /path/to/database:/database \
|
||||
-v /path/to/config:/config \
|
||||
-e PUID=$(id -u) \
|
||||
-e PGID=$(id -g) \
|
||||
-p 8080:80 \
|
||||
filebrowser/filebrowser:s6
|
||||
```
|
||||
The `s6` image is based on LinuxServer and leverages the [s6-overlay](https://github.com/just-containers/s6-overlay) system for a standard, highly customizable image. It should be used as follows:
|
||||
|
||||
### Notes
|
||||
```shell
|
||||
docker run \
|
||||
-v /path/to/srv:/srv \
|
||||
-v /path/to/database:/database \
|
||||
-v /path/to/config:/config \
|
||||
-e PUID=$(id -u) \
|
||||
-e PGID=$(id -g) \
|
||||
-p 8080:80 \
|
||||
filebrowser/filebrowser:s6
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
@@ -78,3 +71,15 @@ Where:
|
||||
- `/path/to/database` contains a `filebrowser.db` file
|
||||
|
||||
Both `settings.json` and `filebrowser.db` will automatically be initialized if they don't exist.
|
||||
|
||||
File Browser is now up and running. Read some [first boot](#first-boot) for more information.
|
||||
|
||||
## First Boot
|
||||
|
||||
Your instance is now up and running. File Browser will automatically bootstrap a database, in which the configuration and the users are stored. You can find the address in which your instance is running, as well as the randomly generated password for the user `admin`, in the console logs.
|
||||
|
||||
Although this is the fastest way to bootstrap an instance, we recommend you to take a look at other possible options, by checking `config init --help` and `config set --help`, to make the installation as safe and customized as it can be.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> The automatically generated password for the user `admin` is only displayed once. If you fail to remember it, you will need to manually delete the database and start File Browser again.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
site_name: File Browser
|
||||
site_description: 'A web-based file browser and manager for your files'
|
||||
site_author: 'File Browser Community'
|
||||
site_author: 'File Browser Contributors'
|
||||
site_url: 'https://filebrowser.org'
|
||||
|
||||
repo_name: 'filebrowser/filebrowser'
|
||||
repo_url: 'https://github.com/filebrowser/filebrowser'
|
||||
|
||||
copyright: 'Copyright © 2025 File Browser Community'
|
||||
copyright: 'Copyright © 2025 File Browser Contributors'
|
||||
|
||||
theme:
|
||||
name: material
|
||||
|
||||
Reference in New Issue
Block a user