Beta/v0.5.0 (#333)
This commit is contained in:
parent
0bc789f9bd
commit
e269fc8c0a
|
@ -28,7 +28,7 @@ jobs:
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile.playwright
|
file: ./_docker/Dockerfile.playwright
|
||||||
push: false
|
push: false
|
||||||
push_latest_to_registry:
|
push_latest_to_registry:
|
||||||
needs: [ test_frontend ]
|
needs: [ test_frontend ]
|
||||||
|
@ -65,7 +65,7 @@ jobs:
|
||||||
VERSION=${{ env.LATEST_TAG }}
|
VERSION=${{ env.LATEST_TAG }}
|
||||||
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
file: ./Dockerfile
|
file: ./_docker/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: 'gtstef/filebrowser:latest'
|
tags: 'gtstef/filebrowser:latest'
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
|
@ -30,7 +30,7 @@ jobs:
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile.playwright
|
file: ./_docker/Dockerfile.playwright
|
||||||
push: false
|
push: false
|
||||||
push_pr_to_registry:
|
push_pr_to_registry:
|
||||||
name: Push PR
|
name: Push PR
|
||||||
|
@ -56,7 +56,7 @@ jobs:
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./_docker/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
|
@ -31,7 +31,7 @@ jobs:
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile.playwright
|
file: ./_docker/Dockerfile.playwright
|
||||||
push: false
|
push: false
|
||||||
create_release_tag:
|
create_release_tag:
|
||||||
needs: [ test_frontend ]
|
needs: [ test_frontend ]
|
||||||
|
@ -101,7 +101,7 @@ jobs:
|
||||||
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
||||||
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
file: ./Dockerfile
|
file: ./_docker/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
|
@ -44,7 +44,7 @@ jobs:
|
||||||
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
||||||
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
file: ./Dockerfile
|
file: ./_docker/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
|
@ -31,7 +31,7 @@ jobs:
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile.playwright
|
file: ./_docker/Dockerfile.playwright
|
||||||
push: false
|
push: false
|
||||||
create_release_tag:
|
create_release_tag:
|
||||||
needs: [ test_frontend ]
|
needs: [ test_frontend ]
|
||||||
|
@ -100,7 +100,7 @@ jobs:
|
||||||
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
||||||
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
file: ./Dockerfile
|
file: ./_docker/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
32
CHANGELOG.md
32
CHANGELOG.md
|
@ -2,9 +2,27 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
|
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
|
||||||
|
|
||||||
|
## v0.5.0-beta
|
||||||
|
|
||||||
|
> Note: This Beta release includes a configuration change: `auth.method` is now deprecated. This is done to allow multiple login methods at once. Auth methods are specified via `auth.methods` instead. see [example on the wiki](https://github.com/gtsteffaniak/filebrowser/wiki/Configuration#example-auth-config).
|
||||||
|
|
||||||
|
**New Features**:
|
||||||
|
- Upload progress notification https://github.com/gtsteffaniak/filebrowser/issues/303
|
||||||
|
- proxy auth auto create user when `auth.methods.proxy.createUser: true` while using proxy auth.
|
||||||
|
|
||||||
|
**Notes**:
|
||||||
|
- Context menu positioning tweaks.
|
||||||
|
- using /tmp cachedir is disabled by default, cache dir can be specified via `server.cacheDir: /tmp` to enable it. https://github.com/gtsteffaniak/filebrowser/issues/326
|
||||||
|
|
||||||
|
**Bugfixes**:
|
||||||
|
- Gracefully shutdown to protect database. https://github.com/gtsteffaniak/filebrowser/issues/317
|
||||||
|
- validates auth method provided before server startup.
|
||||||
|
- fix sidebar disk space usage calculation. https://github.com/gtsteffaniak/filebrowser/issues/315
|
||||||
|
- Fixed proxy auth header support (make sure your proxy and server are secure!). https://github.com/gtsteffaniak/filebrowser/issues/322
|
||||||
|
|
||||||
## v0.4.2-beta
|
## v0.4.2-beta
|
||||||
|
|
||||||
**New Features**
|
**New Features**:
|
||||||
- Hidden files changes
|
- Hidden files changes
|
||||||
- windows hidden file properties are respected -- when running on windows binary (not docker) with NTFS filesystem.
|
- windows hidden file properties are respected -- when running on windows binary (not docker) with NTFS filesystem.
|
||||||
- windows "system" files are considered hidden.
|
- windows "system" files are considered hidden.
|
||||||
|
@ -19,10 +37,10 @@ All notable changes to this project will be documented in this file. For commit
|
||||||
|
|
||||||
## v0.4.1-beta
|
## v0.4.1-beta
|
||||||
|
|
||||||
**New Features**
|
**New Features**:
|
||||||
- right-click actions are available on search. https://github.com/gtsteffaniak/filebrowser/issues/273
|
- right-click actions are available on search. https://github.com/gtsteffaniak/filebrowser/issues/273
|
||||||
|
|
||||||
**Notes**
|
**Notes**:
|
||||||
- delete prompt now lists all items that will be affected by delete
|
- delete prompt now lists all items that will be affected by delete
|
||||||
- Debug and logger output tweaks.
|
- Debug and logger output tweaks.
|
||||||
|
|
||||||
|
@ -34,13 +52,13 @@ All notable changes to this project will be documented in this file. For commit
|
||||||
|
|
||||||
## v0.4.0-beta
|
## v0.4.0-beta
|
||||||
|
|
||||||
**New Features**
|
**New Features**:
|
||||||
- Better logging https://github.com/gtsteffaniak/filebrowser/issues/288
|
- Better logging https://github.com/gtsteffaniak/filebrowser/issues/288
|
||||||
- highly configurable
|
- highly configurable
|
||||||
- api logs include user
|
- api logs include user
|
||||||
- onlyOffice support for editing only office files (inspired from https://github.com/filebrowser/filebrowser/pull/2954)
|
- onlyOffice support for editing only office files (inspired from https://github.com/filebrowser/filebrowser/pull/2954)
|
||||||
|
|
||||||
**Notes**
|
**Notes**:
|
||||||
- Breadcrumbs will only show on file listing (not on previews or editors)
|
- Breadcrumbs will only show on file listing (not on previews or editors)
|
||||||
- Config file is now optional. It will run with default settings without one and throw a `[WARN ]` message.
|
- Config file is now optional. It will run with default settings without one and throw a `[WARN ]` message.
|
||||||
- Added more descriptions to swagger API
|
- Added more descriptions to swagger API
|
||||||
|
@ -58,7 +76,7 @@ All notable changes to this project will be documented in this file. For commit
|
||||||
|
|
||||||
## v0.3.6-beta
|
## v0.3.6-beta
|
||||||
|
|
||||||
**New Features**
|
**New Features**:
|
||||||
- Adds "externalUrl" server config https://github.com/gtsteffaniak/filebrowser/issues/272
|
- Adds "externalUrl" server config https://github.com/gtsteffaniak/filebrowser/issues/272
|
||||||
|
|
||||||
**Notes**:
|
**Notes**:
|
||||||
|
@ -74,7 +92,7 @@ All notable changes to this project will be documented in this file. For commit
|
||||||
|
|
||||||
## v0.3.5
|
## v0.3.5
|
||||||
|
|
||||||
**New Features**
|
**New Features**:
|
||||||
- More indexing configuration options possible. However consider waiting on using this feature, because I will soon have a full onboarding experience in the UI to manage sources instead.
|
- More indexing configuration options possible. However consider waiting on using this feature, because I will soon have a full onboarding experience in the UI to manage sources instead.
|
||||||
- added config file options "sources" in the server config.
|
- added config file options "sources" in the server config.
|
||||||
- can enable/disable indexing a specified list of directories/files
|
- can enable/disable indexing a specified list of directories/files
|
||||||
|
|
22
README.md
22
README.md
|
@ -1,13 +1,17 @@
|
||||||
<p align="center">
|
<div align="center">
|
||||||
<a href="https://opensource.org/license/apache-2-0/"><img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg" alt="License: Apache-2.0"></a>
|
|
||||||
</p>
|
[](https://goreportcard.com/report/github.com/gtsteffaniak/filebrowser/backend)
|
||||||
<p align="center">
|
[](https://github.com/gtsteffaniak/filebrowser/releases)
|
||||||
<img src="frontend/public/img/icons/favicon-256x256.png" width="100" title="Login With Custom URL">
|
[](https://www.apache.org/licenses/LICENSE-2.0)
|
||||||
</p>
|
[](https://www.codacy.com/gh/gtsteffaniak/filebrowser/dashboard)
|
||||||
<h3 align="center">FileBrowser Quantum - A modern web-based file manager</h3>
|
[](https://hub.docker.com/r/gtstef/filebrowser)
|
||||||
<p align="center">
|
|
||||||
|
<img width="150" src="https://github.com/user-attachments/assets/59986a2a-f960-4536-aa35-4a9a7c98ad48" title="Logo">
|
||||||
|
<h3>FileBrowser Quantum</h3>
|
||||||
|
A modern web-based file manager
|
||||||
|
<br/><br/>
|
||||||
<img width="800" src="https://github.com/user-attachments/assets/c991fc69-a05b-4f34-b915-0d3cded887a7" title="Main Screenshot">
|
<img width="800" src="https://github.com/user-attachments/assets/c991fc69-a05b-4f34-b915-0d3cded887a7" title="Main Screenshot">
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> There is no stable version -- planned 2025.
|
> There is no stable version -- planned 2025.
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
services:
|
||||||
|
nginx-proxy-auth:
|
||||||
|
image: nginx
|
||||||
|
container_name: nginx-proxy-auth
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- ./src/default.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
filebrowser:
|
||||||
|
volumes:
|
||||||
|
- '../frontend:/home/frontend'
|
||||||
|
- "./src/config.yaml:/home/filebrowser/config.yaml"
|
||||||
|
build:
|
||||||
|
context: ../
|
||||||
|
dockerfile: ./_docker/Dockerfile
|
|
@ -0,0 +1,8 @@
|
||||||
|
server:
|
||||||
|
port: 80
|
||||||
|
baseURL: "/"
|
||||||
|
root: "../frontend/tests/playwright-files"
|
||||||
|
auth:
|
||||||
|
method: proxy
|
||||||
|
header: X-Username
|
||||||
|
signup: false
|
|
@ -0,0 +1,13 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost 127.0.0.1;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://filebrowser;
|
||||||
|
proxy_set_header X-Username "proxy-user";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,8 +17,25 @@ type Storage struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStorage creates a auth storage from a backend.
|
// NewStorage creates a auth storage from a backend.
|
||||||
func NewStorage(back StorageBackend, userStore *users.Storage) *Storage {
|
func NewStorage(back StorageBackend, userStore *users.Storage) (*Storage, error) {
|
||||||
return &Storage{back: back, users: userStore}
|
store := &Storage{back: back, users: userStore}
|
||||||
|
err := store.Save(&JSONAuth{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = store.Save(&ProxyAuth{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = store.Save(&HookAuth{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = store.Save(&NoAuth{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return store, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get wraps a StorageBackend.Get.
|
// Get wraps a StorageBackend.Get.
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/backend/diskcache"
|
"github.com/gtsteffaniak/filebrowser/backend/diskcache"
|
||||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||||
|
@ -71,6 +74,15 @@ func StartFilebrowser() {
|
||||||
setCmd.StringVar(&scope, "s", "", "Specify a user scope, otherwise default user config scope is used")
|
setCmd.StringVar(&scope, "s", "", "Specify a user scope, otherwise default user config scope is used")
|
||||||
setCmd.StringVar(&dbConfig, "c", "config.yaml", "Path to the config file, default: config.yaml")
|
setCmd.StringVar(&dbConfig, "c", "config.yaml", "Path to the config file, default: config.yaml")
|
||||||
|
|
||||||
|
// Create context and channels for graceful shutdown
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
done := make(chan struct{}) // Signals server has stopped
|
||||||
|
shutdownComplete := make(chan struct{}) // Signals shutdown process is complete
|
||||||
|
signalChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
// Parse subcommand flags only if a subcommand is specified
|
// Parse subcommand flags only if a subcommand is specified
|
||||||
if len(os.Args) > 1 {
|
if len(os.Args) > 1 {
|
||||||
switch os.Args[1] {
|
switch os.Args[1] {
|
||||||
|
@ -116,6 +128,7 @@ Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
store, dbExists := getStore(configPath)
|
store, dbExists := getStore(configPath)
|
||||||
database := fmt.Sprintf("Using existing database : %v", settings.Config.Server.Database)
|
database := fmt.Sprintf("Using existing database : %v", settings.Config.Server.Database)
|
||||||
if !dbExists {
|
if !dbExists {
|
||||||
|
@ -125,9 +138,22 @@ Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v
|
||||||
for _, v := range settings.Config.Server.Sources {
|
for _, v := range settings.Config.Server.Sources {
|
||||||
sources = append(sources, v.Name+": "+v.Path)
|
sources = append(sources, v.Name+": "+v.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authMethods := []string{}
|
||||||
|
if settings.Config.Auth.Methods.PasswordAuth {
|
||||||
|
authMethods = append(authMethods, "Password")
|
||||||
|
}
|
||||||
|
if settings.Config.Auth.Methods.ProxyAuth.Enabled {
|
||||||
|
authMethods = append(authMethods, "Proxy")
|
||||||
|
}
|
||||||
|
if settings.Config.Auth.Methods.NoAuth {
|
||||||
|
logger.Warning("Configured with no authentication, this is not recommended.")
|
||||||
|
authMethods = []string{"Disabled"}
|
||||||
|
}
|
||||||
|
|
||||||
logger.Info(fmt.Sprintf("Initializing FileBrowser Quantum (%v)", version.Version))
|
logger.Info(fmt.Sprintf("Initializing FileBrowser Quantum (%v)", version.Version))
|
||||||
logger.Info(fmt.Sprintf("Using Config file : %v", configPath))
|
logger.Info(fmt.Sprintf("Using Config file : %v", configPath))
|
||||||
logger.Debug(fmt.Sprintf("Embeded frontend : %v", os.Getenv("FILEBROWSER_NO_EMBEDED") != "true"))
|
logger.Info(fmt.Sprintf("Auth Methods : %v", authMethods))
|
||||||
logger.Info(database)
|
logger.Info(database)
|
||||||
logger.Info(fmt.Sprintf("Sources : %v", sources))
|
logger.Info(fmt.Sprintf("Sources : %v", sources))
|
||||||
serverConfig := settings.Config.Server
|
serverConfig := settings.Config.Server
|
||||||
|
@ -142,18 +168,34 @@ Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v
|
||||||
for _, source := range sourceConfigs {
|
for _, source := range sourceConfigs {
|
||||||
go files.Initialize(source)
|
go files.Initialize(source)
|
||||||
}
|
}
|
||||||
if err := rootCMD(store, &serverConfig); err != nil {
|
// Start the rootCMD in a goroutine
|
||||||
logger.Fatal(fmt.Sprintf("Error starting filebrowser: %v", err))
|
go func() {
|
||||||
|
if err := rootCMD(ctx, store, &serverConfig, shutdownComplete); err != nil {
|
||||||
|
logger.Fatal(fmt.Sprintf("Error starting filebrowser: %v", err))
|
||||||
|
}
|
||||||
|
close(done) // Signal that the server has stopped
|
||||||
|
}()
|
||||||
|
// Wait for a shutdown signal or the server to stop
|
||||||
|
select {
|
||||||
|
case <-signalChan:
|
||||||
|
logger.Info("Received shutdown signal. Shutting down gracefully...")
|
||||||
|
cancel() // Trigger context cancellation
|
||||||
|
case <-done:
|
||||||
|
logger.Info("Server stopped unexpectedly. Shutting down...")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<-shutdownComplete // Ensure we don't exit prematurely
|
||||||
|
// Wait for the server to stop
|
||||||
|
logger.Info("Shutdown complete.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func rootCMD(store *storage.Storage, serverConfig *settings.Server) error {
|
func rootCMD(ctx context.Context, store *storage.Storage, serverConfig *settings.Server, shutdownComplete chan struct{}) error {
|
||||||
if serverConfig.NumImageProcessors < 1 {
|
if serverConfig.NumImageProcessors < 1 {
|
||||||
logger.Fatal("Image resize workers count could not be < 1")
|
logger.Fatal("Image resize workers count could not be < 1")
|
||||||
}
|
}
|
||||||
imgSvc := img.New(serverConfig.NumImageProcessors)
|
imgSvc := img.New(serverConfig.NumImageProcessors)
|
||||||
|
|
||||||
cacheDir := "/tmp"
|
cacheDir := settings.Config.Server.CacheDir
|
||||||
var fileCache diskcache.Interface
|
var fileCache diskcache.Interface
|
||||||
|
|
||||||
// Use file cache if cacheDir is specified
|
// Use file cache if cacheDir is specified
|
||||||
|
@ -167,7 +209,7 @@ func rootCMD(store *storage.Storage, serverConfig *settings.Server) error {
|
||||||
// No-op cache if no cacheDir is specified
|
// No-op cache if no cacheDir is specified
|
||||||
fileCache = diskcache.NewNoOp()
|
fileCache = diskcache.NewNoOp()
|
||||||
}
|
}
|
||||||
fbhttp.StartHttp(imgSvc, store, fileCache)
|
fbhttp.StartHttp(ctx, imgSvc, store, fileCache, shutdownComplete)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ server:
|
||||||
baseURL: "/"
|
baseURL: "/"
|
||||||
root: "/srv"
|
root: "/srv"
|
||||||
auth:
|
auth:
|
||||||
method: password
|
|
||||||
signup: false
|
signup: false
|
||||||
userDefaults:
|
userDefaults:
|
||||||
darkMode: true
|
darkMode: true
|
||||||
|
|
|
@ -75,8 +75,13 @@ func extractToken(r *http.Request) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loginHandler(w http.ResponseWriter, r *http.Request) {
|
func loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !config.Auth.Methods.PasswordAuth {
|
||||||
|
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// currently only supports user/pass
|
||||||
// Get the authentication method from the settings
|
// Get the authentication method from the settings
|
||||||
auther, err := store.Auth.Get(config.Auth.Method)
|
auther, err := store.Auth.Get("password")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||||
"github.com/gtsteffaniak/filebrowser/backend/runner"
|
"github.com/gtsteffaniak/filebrowser/backend/runner"
|
||||||
|
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -93,15 +94,44 @@ func withAdminHelper(fn handleFunc) handleFunc {
|
||||||
// Middleware to retrieve and authenticate user
|
// Middleware to retrieve and authenticate user
|
||||||
func withUserHelper(fn handleFunc) handleFunc {
|
func withUserHelper(fn handleFunc) handleFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
|
return func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
|
||||||
if config.Auth.Method == "noauth" {
|
if config.Auth.Methods.NoAuth {
|
||||||
var err error
|
var err error
|
||||||
// Retrieve the user from the store and store it in the context
|
// Retrieve the user from the store and store it in the context
|
||||||
data.user, err = store.Users.Get(files.RootPaths["default"], "admin")
|
data.user, err = store.Users.Get(files.RootPaths["default"], 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("no auth: %v", err))
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
return fn(w, r, data)
|
return fn(w, r, data)
|
||||||
}
|
}
|
||||||
|
proxyUser := r.Header.Get(config.Auth.Methods.ProxyAuth.Header)
|
||||||
|
if config.Auth.Methods.ProxyAuth.Enabled && proxyUser != "" {
|
||||||
|
var err error
|
||||||
|
// Retrieve the user from the store and store it in the context
|
||||||
|
data.user, err = store.Users.Get(files.RootPaths["default"], proxyUser)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() != "the resource does not exist" {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
if config.Auth.Methods.ProxyAuth.CreateUser {
|
||||||
|
newUser := settings.ApplyUserDefaults(users.User{
|
||||||
|
Username: proxyUser,
|
||||||
|
})
|
||||||
|
err := store.Users.Save(&newUser)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
data.user, err = store.Users.Get(files.RootPaths["default"], proxyUser)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return http.StatusUnauthorized, fmt.Errorf("proxy authentication failed - no user found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setUserInResponseWriter(w, data.user)
|
||||||
|
return fn(w, r, data)
|
||||||
|
}
|
||||||
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
||||||
return config.Auth.Key, nil
|
return config.Auth.Key, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -8,6 +9,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||||
|
@ -43,7 +45,7 @@ var (
|
||||||
assetFs fs.FS
|
assetFs fs.FS
|
||||||
)
|
)
|
||||||
|
|
||||||
func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
|
func StartHttp(ctx context.Context, Service ImgService, storage *storage.Storage, cache FileCache, shutdownComplete chan struct{}) {
|
||||||
|
|
||||||
store = storage
|
store = storage
|
||||||
fileCache = cache
|
fileCache = cache
|
||||||
|
@ -140,51 +142,74 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
|
||||||
|
|
||||||
var scheme string
|
var scheme string
|
||||||
port := ""
|
port := ""
|
||||||
|
srv := &http.Server{
|
||||||
// Determine whether to use HTTPS (TLS) or HTTP
|
Addr: fmt.Sprintf(":%v", config.Server.Port),
|
||||||
if config.Server.TLSCert != "" && config.Server.TLSKey != "" {
|
Handler: muxWithMiddleware(router),
|
||||||
// Load the TLS certificate and key
|
|
||||||
cer, err := tls.LoadX509KeyPair(config.Server.TLSCert, config.Server.TLSKey)
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal(fmt.Sprintf("could not load certificate: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a custom TLS listener
|
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
MinVersion: tls.VersionTLS12,
|
|
||||||
Certificates: []tls.Certificate{cer},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set HTTPS scheme and default port for TLS
|
|
||||||
scheme = "https"
|
|
||||||
|
|
||||||
// Listen on TCP and wrap with TLS
|
|
||||||
listener, err := tls.Listen("tcp", fmt.Sprintf(":%v", config.Server.Port), tlsConfig)
|
|
||||||
if err != nil {
|
|
||||||
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)
|
|
||||||
logger.Info(fmt.Sprintf("Running at : %s", fullURL))
|
|
||||||
err = http.Serve(listener, muxWithMiddleware(router))
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal(fmt.Sprintf("could not start server: %v", err))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Set HTTP scheme and the default port for HTTP
|
|
||||||
scheme = "http"
|
|
||||||
if config.Server.Port != 80 {
|
|
||||||
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)
|
|
||||||
logger.Info(fmt.Sprintf("Running at : %s", fullURL))
|
|
||||||
err := http.ListenAndServe(fmt.Sprintf(":%v", config.Server.Port), muxWithMiddleware(router))
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal(fmt.Sprintf("could not start server: %v", err))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
go func() {
|
||||||
|
// Determine whether to use HTTPS (TLS) or HTTP
|
||||||
|
if config.Server.TLSCert != "" && config.Server.TLSKey != "" {
|
||||||
|
// Load the TLS certificate and key
|
||||||
|
cer, err := tls.LoadX509KeyPair(config.Server.TLSCert, config.Server.TLSKey)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(fmt.Sprintf("Could not load certificate: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a custom TLS configuration
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
Certificates: []tls.Certificate{cer},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set HTTPS scheme and default port for TLS
|
||||||
|
scheme = "https"
|
||||||
|
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)
|
||||||
|
logger.Info(fmt.Sprintf("Running at : %s", fullURL))
|
||||||
|
|
||||||
|
// Create a TLS listener and serve
|
||||||
|
listener, err := tls.Listen("tcp", srv.Addr, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(fmt.Sprintf("Could not start TLS server: %v", err))
|
||||||
|
}
|
||||||
|
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||||
|
logger.Fatal(fmt.Sprintf("Server error: %v", err))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Set HTTP scheme and the default port for HTTP
|
||||||
|
scheme = "http"
|
||||||
|
if config.Server.Port != 80 {
|
||||||
|
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)
|
||||||
|
logger.Info(fmt.Sprintf("Running at : %s", fullURL))
|
||||||
|
|
||||||
|
// Start HTTP server
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
logger.Fatal(fmt.Sprintf("Server error: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for context cancellation to shut down the server
|
||||||
|
<-ctx.Done()
|
||||||
|
logger.Info("Shutting down HTTP server...")
|
||||||
|
|
||||||
|
// Graceful shutdown with a timeout - 30 seconds, in case downloads are happening
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("HTTP server forced to shut down: %v", err))
|
||||||
|
} else {
|
||||||
|
logger.Info("HTTP server shut down gracefully.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal that shutdown is complete
|
||||||
|
close(shutdownComplete)
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ func (t *TemplateRenderer) Render(w http.ResponseWriter, name string, data inter
|
||||||
func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentType string) {
|
func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentType string) {
|
||||||
w.Header().Set("Content-Type", contentType)
|
w.Header().Set("Content-Type", contentType)
|
||||||
|
|
||||||
auther, err := store.Auth.Get(config.Auth.Method)
|
auther, err := store.Auth.Get("password")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
@ -53,8 +53,8 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
|
||||||
"CommitSHA": version.CommitSHA,
|
"CommitSHA": version.CommitSHA,
|
||||||
"StaticURL": config.Server.BaseURL + "static",
|
"StaticURL": config.Server.BaseURL + "static",
|
||||||
"Signup": settings.Config.Auth.Signup,
|
"Signup": settings.Config.Auth.Signup,
|
||||||
"NoAuth": config.Auth.Method == "noauth",
|
"NoAuth": config.Auth.Methods.NoAuth,
|
||||||
"AuthMethod": config.Auth.Method,
|
"PasswordAuth": config.Auth.Methods.PasswordAuth,
|
||||||
"LoginPage": auther.LoginPage(),
|
"LoginPage": auther.LoginPage(),
|
||||||
"CSS": false,
|
"CSS": false,
|
||||||
"ReCaptcha": false,
|
"ReCaptcha": false,
|
||||||
|
@ -80,8 +80,8 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Auth.Method == "password" {
|
if config.Auth.Methods.PasswordAuth {
|
||||||
raw, err := store.Auth.Get(config.Auth.Method) //nolint:govet
|
raw, err := store.Auth.Get("password") //nolint:govet
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
|
@ -46,7 +46,9 @@ func Initialize(configFile string) {
|
||||||
log.Println("[ERROR] Failed to set up logger:", err)
|
log.Println("[ERROR] Failed to set up logger:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if Config.Auth.Method != "" {
|
||||||
|
logger.Warning("The `auth.method` setting is deprecated and will be removed in a future version. Please use `auth.methods` instead.")
|
||||||
|
}
|
||||||
Config.UserDefaults.Perm = Config.UserDefaults.Permissions
|
Config.UserDefaults.Perm = Config.UserDefaults.Permissions
|
||||||
// Convert relative path to absolute path
|
// Convert relative path to absolute path
|
||||||
if len(Config.Server.Sources) > 0 {
|
if len(Config.Server.Sources) > 0 {
|
||||||
|
@ -129,14 +131,22 @@ func setDefaults() Settings {
|
||||||
Root: ".",
|
Root: ".",
|
||||||
},
|
},
|
||||||
Auth: Auth{
|
Auth: Auth{
|
||||||
TokenExpirationHours: 2,
|
|
||||||
AdminUsername: "admin",
|
AdminUsername: "admin",
|
||||||
AdminPassword: "admin",
|
AdminPassword: "admin",
|
||||||
Method: "password",
|
TokenExpirationHours: 2,
|
||||||
Signup: false,
|
Signup: false,
|
||||||
Recaptcha: Recaptcha{
|
Recaptcha: Recaptcha{
|
||||||
Host: "",
|
Host: "",
|
||||||
},
|
},
|
||||||
|
Methods: LoginMethods{
|
||||||
|
ProxyAuth: ProxyAuthConfig{
|
||||||
|
Enabled: false,
|
||||||
|
CreateUser: false,
|
||||||
|
Header: "",
|
||||||
|
},
|
||||||
|
NoAuth: false,
|
||||||
|
PasswordAuth: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Frontend: Frontend{
|
Frontend: Frontend{
|
||||||
Name: "FileBrowser Quantum",
|
Name: "FileBrowser Quantum",
|
||||||
|
@ -145,8 +155,8 @@ func setDefaults() Settings {
|
||||||
StickySidebar: true,
|
StickySidebar: true,
|
||||||
Scope: ".",
|
Scope: ".",
|
||||||
LockPassword: false,
|
LockPassword: false,
|
||||||
ShowHidden: true,
|
ShowHidden: false,
|
||||||
DarkMode: false,
|
DarkMode: true,
|
||||||
DisableSettings: false,
|
DisableSettings: false,
|
||||||
ViewMode: "normal",
|
ViewMode: "normal",
|
||||||
Locale: "en",
|
Locale: "en",
|
||||||
|
|
|
@ -40,8 +40,6 @@ func TestConfigLoadSpecificValues(t *testing.T) {
|
||||||
globalVal interface{}
|
globalVal interface{}
|
||||||
newVal interface{}
|
newVal interface{}
|
||||||
}{
|
}{
|
||||||
{"Auth.Method", Config.Auth.Method, newConfig.Auth.Method},
|
|
||||||
{"Auth.Method", Config.Auth.Method, newConfig.Auth.Method},
|
|
||||||
{"Server.Database", Config.Server.Database, newConfig.Server.Database},
|
{"Server.Database", Config.Server.Database, newConfig.Server.Database},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,14 @@ import (
|
||||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type AllowedMethods string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProxyAuth AllowedMethods = "proxyAuth"
|
||||||
|
NoAuth AllowedMethods = "noAuth"
|
||||||
|
PasswordAuth AllowedMethods = "passwordAuth"
|
||||||
|
)
|
||||||
|
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
Commands map[string][]string `json:"commands"`
|
Commands map[string][]string `json:"commands"`
|
||||||
Shell []string `json:"shell"`
|
Shell []string `json:"shell"`
|
||||||
|
@ -17,16 +25,28 @@ type Settings struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
TokenExpirationHours int `json:"tokenExpirationHours"`
|
TokenExpirationHours int `json:"tokenExpirationHours"`
|
||||||
Recaptcha Recaptcha `json:"recaptcha"`
|
Recaptcha Recaptcha `json:"recaptcha"`
|
||||||
Header string `json:"header"`
|
Methods LoginMethods `json:"methods"`
|
||||||
Method string `json:"method"`
|
Command string `json:"command"`
|
||||||
Command string `json:"command"`
|
Signup bool `json:"signup"`
|
||||||
Signup bool `json:"signup"`
|
Method string `json:"method"`
|
||||||
Shell string `json:"shell"`
|
Shell string `json:"shell"`
|
||||||
AdminUsername string `json:"adminUsername"`
|
Key []byte `json:"key"`
|
||||||
AdminPassword string `json:"adminPassword"`
|
AdminUsername string `json:"adminUsername"`
|
||||||
Key []byte `json:"key"`
|
AdminPassword string `json:"adminPassword"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginMethods struct {
|
||||||
|
ProxyAuth ProxyAuthConfig `json:"proxy"`
|
||||||
|
NoAuth bool `json:"noauth"`
|
||||||
|
PasswordAuth bool `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProxyAuthConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
CreateUser bool `json:"createUser"`
|
||||||
|
Header string `json:"header"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Recaptcha struct {
|
type Recaptcha struct {
|
||||||
|
@ -54,6 +74,7 @@ type Server struct {
|
||||||
Sources []Source `json:"sources"`
|
Sources []Source `json:"sources"`
|
||||||
ExternalUrl string `json:"externalUrl"`
|
ExternalUrl string `json:"externalUrl"`
|
||||||
InternalUrl string `json:"internalUrl"` // used by integrations
|
InternalUrl string `json:"internalUrl"` // used by integrations
|
||||||
|
CacheDir string `json:"cacheDir"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Integrations struct {
|
type Integrations struct {
|
||||||
|
|
|
@ -16,7 +16,6 @@ auth:
|
||||||
key: ""
|
key: ""
|
||||||
secret: ""
|
secret: ""
|
||||||
header: ""
|
header: ""
|
||||||
method: json
|
|
||||||
command: ""
|
command: ""
|
||||||
signup: false
|
signup: false
|
||||||
shell: ""
|
shell: ""
|
||||||
|
|
|
@ -14,6 +14,9 @@ func NewStorage(db *storm.DB) (*auth.Storage, *users.Storage, *share.Storage, *s
|
||||||
userStore := users.NewStorage(usersBackend{db: db})
|
userStore := users.NewStorage(usersBackend{db: db})
|
||||||
shareStore := share.NewStorage(shareBackend{db: db})
|
shareStore := share.NewStorage(shareBackend{db: db})
|
||||||
settingsStore := settings.NewStorage(settingsBackend{db: db})
|
settingsStore := settings.NewStorage(settingsBackend{db: db})
|
||||||
authStore := auth.NewStorage(authBackend{db: db}, userStore)
|
authStore, err := auth.NewStorage(authBackend{db: db}, userStore)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
return authStore, userStore, shareStore, settingsStore, nil
|
return authStore, userStore, shareStore, settingsStore, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,9 +19,11 @@ func (st usersBackend) GetBy(i interface{}) (user *users.User, err error) {
|
||||||
user = &users.User{}
|
user = &users.User{}
|
||||||
|
|
||||||
var arg string
|
var arg string
|
||||||
switch i.(type) {
|
switch val := i.(type) {
|
||||||
case uint:
|
case uint:
|
||||||
arg = "ID"
|
arg = "ID"
|
||||||
|
case int:
|
||||||
|
i = uint(val)
|
||||||
case string:
|
case string:
|
||||||
arg = "Username"
|
arg = "Username"
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -80,14 +80,6 @@ func dbExists(path string) (bool, error) {
|
||||||
|
|
||||||
func quickSetup(store *Storage) {
|
func quickSetup(store *Storage) {
|
||||||
settings.Config.Auth.Key = utils.GenerateKey()
|
settings.Config.Auth.Key = utils.GenerateKey()
|
||||||
if settings.Config.Auth.Method == "noauth" {
|
|
||||||
err := store.Auth.Save(&auth.NoAuth{})
|
|
||||||
utils.CheckErr("store.Auth.Save", err)
|
|
||||||
} else {
|
|
||||||
settings.Config.Auth.Method = "password"
|
|
||||||
err := store.Auth.Save(&auth.JSONAuth{})
|
|
||||||
utils.CheckErr("store.Auth.Save", err)
|
|
||||||
}
|
|
||||||
err := store.Settings.Save(&settings.Config)
|
err := store.Settings.Save(&settings.Config)
|
||||||
utils.CheckErr("store.Settings.Save", err)
|
utils.CheckErr("store.Settings.Save", err)
|
||||||
err = store.Settings.SaveServer(&settings.Config.Server)
|
err = store.Settings.SaveServer(&settings.Config.Server)
|
||||||
|
|
|
@ -1184,6 +1184,10 @@ const docTemplate = `{
|
||||||
"$ref": "#/definitions/files.ItemInfo"
|
"$ref": "#/definitions/files.ItemInfo"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"hidden": {
|
||||||
|
"description": "whether the file is hidden",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"modified": {
|
"modified": {
|
||||||
"description": "modification time",
|
"description": "modification time",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
@ -1209,6 +1213,10 @@ const docTemplate = `{
|
||||||
"files.ItemInfo": {
|
"files.ItemInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"hidden": {
|
||||||
|
"description": "whether the file is hidden",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"modified": {
|
"modified": {
|
||||||
"description": "modification time",
|
"description": "modification time",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
@ -1362,9 +1370,6 @@ const docTemplate = `{
|
||||||
"gallerySize": {
|
"gallerySize": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"hideDotfiles": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"locale": {
|
"locale": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -1386,6 +1391,9 @@ const docTemplate = `{
|
||||||
"scope": {
|
"scope": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"showHidden": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"singleClick": {
|
"singleClick": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
@ -1570,9 +1578,6 @@ const docTemplate = `{
|
||||||
"gallerySize": {
|
"gallerySize": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"hideDotfiles": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
@ -1597,6 +1602,9 @@ const docTemplate = `{
|
||||||
"scope": {
|
"scope": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"showHidden": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"singleClick": {
|
"singleClick": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1173,6 +1173,10 @@
|
||||||
"$ref": "#/definitions/files.ItemInfo"
|
"$ref": "#/definitions/files.ItemInfo"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"hidden": {
|
||||||
|
"description": "whether the file is hidden",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"modified": {
|
"modified": {
|
||||||
"description": "modification time",
|
"description": "modification time",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
@ -1198,6 +1202,10 @@
|
||||||
"files.ItemInfo": {
|
"files.ItemInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"hidden": {
|
||||||
|
"description": "whether the file is hidden",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"modified": {
|
"modified": {
|
||||||
"description": "modification time",
|
"description": "modification time",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
@ -1351,9 +1359,6 @@
|
||||||
"gallerySize": {
|
"gallerySize": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"hideDotfiles": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"locale": {
|
"locale": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -1375,6 +1380,9 @@
|
||||||
"scope": {
|
"scope": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"showHidden": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"singleClick": {
|
"singleClick": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
@ -1559,9 +1567,6 @@
|
||||||
"gallerySize": {
|
"gallerySize": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"hideDotfiles": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
@ -1586,6 +1591,9 @@
|
||||||
"scope": {
|
"scope": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"showHidden": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"singleClick": {
|
"singleClick": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,6 +11,9 @@ definitions:
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/files.ItemInfo'
|
$ref: '#/definitions/files.ItemInfo'
|
||||||
type: array
|
type: array
|
||||||
|
hidden:
|
||||||
|
description: whether the file is hidden
|
||||||
|
type: boolean
|
||||||
modified:
|
modified:
|
||||||
description: modification time
|
description: modification time
|
||||||
type: string
|
type: string
|
||||||
|
@ -29,6 +32,9 @@ definitions:
|
||||||
type: object
|
type: object
|
||||||
files.ItemInfo:
|
files.ItemInfo:
|
||||||
properties:
|
properties:
|
||||||
|
hidden:
|
||||||
|
description: whether the file is hidden
|
||||||
|
type: boolean
|
||||||
modified:
|
modified:
|
||||||
description: modification time
|
description: modification time
|
||||||
type: string
|
type: string
|
||||||
|
@ -130,8 +136,6 @@ definitions:
|
||||||
type: boolean
|
type: boolean
|
||||||
gallerySize:
|
gallerySize:
|
||||||
type: integer
|
type: integer
|
||||||
hideDotfiles:
|
|
||||||
type: boolean
|
|
||||||
locale:
|
locale:
|
||||||
type: string
|
type: string
|
||||||
lockPassword:
|
lockPassword:
|
||||||
|
@ -146,6 +150,8 @@ definitions:
|
||||||
type: array
|
type: array
|
||||||
scope:
|
scope:
|
||||||
type: string
|
type: string
|
||||||
|
showHidden:
|
||||||
|
type: boolean
|
||||||
singleClick:
|
singleClick:
|
||||||
type: boolean
|
type: boolean
|
||||||
sorting:
|
sorting:
|
||||||
|
@ -269,8 +275,6 @@ definitions:
|
||||||
type: boolean
|
type: boolean
|
||||||
gallerySize:
|
gallerySize:
|
||||||
type: integer
|
type: integer
|
||||||
hideDotfiles:
|
|
||||||
type: boolean
|
|
||||||
id:
|
id:
|
||||||
type: integer
|
type: integer
|
||||||
locale:
|
locale:
|
||||||
|
@ -287,6 +291,8 @@ definitions:
|
||||||
type: array
|
type: array
|
||||||
scope:
|
scope:
|
||||||
type: string
|
type: string
|
||||||
|
showHidden:
|
||||||
|
type: boolean
|
||||||
singleClick:
|
singleClick:
|
||||||
type: boolean
|
type: boolean
|
||||||
sorting:
|
sorting:
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default defineConfig({
|
||||||
/* Run tests in files in parallel */
|
/* Run tests in files in parallel */
|
||||||
fullyParallel: false,
|
fullyParallel: false,
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
forbidOnly: Boolean(process.env.CI),
|
forbidOnly: false,
|
||||||
/* Retry on CI only */
|
/* Retry on CI only */
|
||||||
retries: 2,
|
retries: 2,
|
||||||
/* Opt out of parallel tests on CI. */
|
/* Opt out of parallel tests on CI. */
|
||||||
|
|
|
@ -28,6 +28,11 @@
|
||||||
<meta name="msapplication-TileImage" content="{{ .StaticURL }}/img/icons/mstile-144x144.png">
|
<meta name="msapplication-TileImage" content="{{ .StaticURL }}/img/icons/mstile-144x144.png">
|
||||||
<meta name="msapplication-TileColor" content="{{ if .Color }}{{ .Color }}{{ else }}#2979ff{{ end }}">
|
<meta name="msapplication-TileColor" content="{{ if .Color }}{{ .Color }}{{ else }}#2979ff{{ end }}">
|
||||||
|
|
||||||
|
<meta name="description" content="FileBrowser Quantum is a file manager for the web which can be used to manage files on your server">
|
||||||
|
<link rel="canonical" href="https://github.com/gtsteffaniak/filebrowser">
|
||||||
|
<link rel="canonical" href="https://github.com/gtsteffaniak">
|
||||||
|
<link rel="canonical" href="https://github.com/quantumx-apps">
|
||||||
|
|
||||||
<!-- Inject Some Variables and generate the manifest json -->
|
<!-- Inject Some Variables and generate the manifest json -->
|
||||||
<script>
|
<script>
|
||||||
window.FileBrowser = JSON.parse('{{ .globalVars }}');
|
window.FileBrowser = JSON.parse('{{ .globalVars }}');
|
||||||
|
|
|
@ -79,7 +79,7 @@ export function download(format, files) {
|
||||||
|
|
||||||
export async function post(url, content = "", overwrite = false, onupload) {
|
export async function post(url, content = "", overwrite = false, onupload) {
|
||||||
try {
|
try {
|
||||||
url = removePrefix(url,"files");
|
url = removePrefix(url, "files");
|
||||||
|
|
||||||
let bufferContent;
|
let bufferContent;
|
||||||
if (
|
if (
|
||||||
|
@ -100,7 +100,12 @@ export async function post(url, content = "", overwrite = false, onupload) {
|
||||||
request.setRequestHeader("X-Auth", state.jwt);
|
request.setRequestHeader("X-Auth", state.jwt);
|
||||||
|
|
||||||
if (typeof onupload === "function") {
|
if (typeof onupload === "function") {
|
||||||
request.upload.onprogress = onupload;
|
request.upload.onprogress = (event) => {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
const percentComplete = Math.round((event.loaded / event.total) * 100);
|
||||||
|
onupload(percentComplete); // Pass the percentage to the callback
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
|
|
|
@ -174,7 +174,7 @@ export default {
|
||||||
|
|
||||||
// if y is too close to the bottom edge, move it up by 400px
|
// if y is too close to the bottom edge, move it up by 400px
|
||||||
if (tempY > screenHeight - 400) {
|
if (tempY > screenHeight - 400) {
|
||||||
tempY -= 400;
|
tempY -= 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.posX = tempX;
|
this.posX = tempX;
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div class="card" id="popup-notification">
|
||||||
|
<i v-on:click="closePopUp" class="material-icons">close</i>
|
||||||
|
<canvas class="notification-spinner hidden" width="100" height="100"></canvas>
|
||||||
|
<div id="popup-notification-content">no info</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { notify } from "@/notify";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "notifications",
|
||||||
|
data: function () {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closePopUp() {
|
||||||
|
return notify.closePopUp();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
canvas.notification-spinner {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!user.perm.admin && !isNew">
|
<div v-if="!stateUser.perm.admin && !isNew">
|
||||||
<label for="password">{{ $t("settings.password") }}</label>
|
<label for="password">{{ $t("settings.password") }}</label>
|
||||||
<input
|
<input
|
||||||
class="input input--block"
|
class="input input--block"
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
<p v-if="!isDefault">
|
<p v-if="!isDefault">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:disabled="user.perm?.admin"
|
:disabled="stateUser.perm?.admin"
|
||||||
v-model="user.lockPassword"
|
v-model="user.lockPassword"
|
||||||
@input="emitUpdate"
|
@input="emitUpdate"
|
||||||
/>
|
/>
|
||||||
|
@ -81,6 +81,7 @@ import Languages from "./Languages.vue";
|
||||||
import Permissions from "./Permissions.vue";
|
import Permissions from "./Permissions.vue";
|
||||||
import Commands from "./Commands.vue";
|
import Commands from "./Commands.vue";
|
||||||
import { enableExec } from "@/utils/constants";
|
import { enableExec } from "@/utils/constants";
|
||||||
|
import { state } from "@/store";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "UserForm",
|
name: "UserForm",
|
||||||
|
@ -119,6 +120,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
stateUser() {
|
||||||
|
return state.user;
|
||||||
|
},
|
||||||
passwordPlaceholder() {
|
passwordPlaceholder() {
|
||||||
return this.isNew ? "" : this.$t("settings.avoidChanges");
|
return this.isNew ? "" : this.$t("settings.avoidChanges");
|
||||||
},
|
},
|
||||||
|
|
|
@ -154,8 +154,8 @@ export default {
|
||||||
}
|
}
|
||||||
let usage = await filesApi.usage("default");
|
let usage = await filesApi.usage("default");
|
||||||
usageStats = {
|
usageStats = {
|
||||||
used: getHumanReadableFilesize(usage.used / 1024),
|
used: getHumanReadableFilesize(usage.used),
|
||||||
total: getHumanReadableFilesize(usage.total / 1024),
|
total: getHumanReadableFilesize(usage.total),
|
||||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<a :href="item.url" target="_blank" :title="item.title">{{ item.text }}</a>
|
<a :href="item.url" target="_blank" :title="item.title">{{ item.text }}</a>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="name != ''">
|
<span v-if="name != ''">
|
||||||
<h4>{{ name }}</h4>
|
<h4 style="margin: 0">{{ name }}</h4>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -90,7 +90,7 @@ body.rtl #listingView {
|
||||||
|
|
||||||
#listingView {
|
#listingView {
|
||||||
padding-top: 1em;
|
padding-top: 1em;
|
||||||
padding-bottom: 1em;
|
padding-bottom: 30em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listingView.gallery .item,
|
#listingView.gallery .item,
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
|
import * as messageFunctions from "./message.js";
|
||||||
|
import * as loadingSpinnerFunctions from "./loadingSpinner.js";
|
||||||
|
|
||||||
import * as notify from "./message.js";
|
const notify = {
|
||||||
export {
|
...messageFunctions,
|
||||||
notify,
|
...loadingSpinnerFunctions
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export { notify };
|
|
@ -0,0 +1,87 @@
|
||||||
|
export function startLoading (from, to) {
|
||||||
|
if (from == to) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('startLoading', from, to)
|
||||||
|
// Get the spinner canvas element
|
||||||
|
let spinner = document.querySelector('.notification-spinner')
|
||||||
|
if (!spinner) {
|
||||||
|
console.error('Spinner canvas element not found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
spinner.classList.remove('hidden')
|
||||||
|
|
||||||
|
// Get the 2D context of the canvas
|
||||||
|
let ctx = spinner.getContext('2d')
|
||||||
|
if (!ctx) {
|
||||||
|
console.error('Could not get 2D context')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set canvas dimensions
|
||||||
|
let width = spinner.width
|
||||||
|
let height = spinner.height
|
||||||
|
|
||||||
|
// Initialize variables
|
||||||
|
let degrees = from * 3.6 // Convert percentage to degrees
|
||||||
|
let new_degrees = to * 3.6 // Convert percentage to degrees
|
||||||
|
let difference = new_degrees - degrees
|
||||||
|
let color = spinner.style.color || '#666'
|
||||||
|
let bgcolor = '#fff'
|
||||||
|
let animation_loop
|
||||||
|
|
||||||
|
// Clear any existing animation loop
|
||||||
|
if (animation_loop !== undefined) clearInterval(animation_loop)
|
||||||
|
|
||||||
|
// Calculate the increment per 10ms
|
||||||
|
let duration = 300 // Duration of the animation in ms
|
||||||
|
let increment = difference / (duration / 10)
|
||||||
|
|
||||||
|
// Start the animation loop
|
||||||
|
animation_loop = setInterval(function () {
|
||||||
|
// Check if the animation should stop
|
||||||
|
if (
|
||||||
|
(increment > 0 && degrees >= new_degrees) ||
|
||||||
|
(increment < 0 && degrees <= new_degrees)
|
||||||
|
) {
|
||||||
|
clearInterval(animation_loop)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the degrees
|
||||||
|
degrees += increment
|
||||||
|
|
||||||
|
// Clear the canvas
|
||||||
|
ctx.clearRect(0, 0, width, height)
|
||||||
|
|
||||||
|
// Draw the background circle
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.strokeStyle = bgcolor
|
||||||
|
ctx.lineWidth = 10
|
||||||
|
ctx.arc(width / 2, height / 2, height / 3, 0, Math.PI * 2, false)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// Draw the foreground circle
|
||||||
|
let radians = (degrees * Math.PI) / 180
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.strokeStyle = color
|
||||||
|
ctx.lineWidth = 10
|
||||||
|
ctx.arc(
|
||||||
|
width / 2,
|
||||||
|
height / 2,
|
||||||
|
height / 3,
|
||||||
|
0 - (90 * Math.PI) / 180,
|
||||||
|
radians - (90 * Math.PI) / 180,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// Draw the text
|
||||||
|
ctx.fillStyle = '#fff'
|
||||||
|
ctx.font = '1.2em Roboto'
|
||||||
|
let text = Math.floor((degrees / 360) * 100) + '%'
|
||||||
|
let text_width = ctx.measureText(text).width
|
||||||
|
ctx.fillText(text, width / 2 - text_width / 2, height / 2 + 8)
|
||||||
|
}, 10) // Update every 10ms
|
||||||
|
}
|
|
@ -1,78 +1,96 @@
|
||||||
import { mutations, state } from "@/store";
|
import { mutations, state } from '@/store'
|
||||||
|
|
||||||
export function showPopup(type, message) {
|
let active = false
|
||||||
const [popup, popupContent] = getElements();
|
let closeTimeout // Store timeout ID
|
||||||
if (popup === undefined) {
|
|
||||||
return;
|
export function showPopup(type, message, autoclose = true) {
|
||||||
|
if (active) {
|
||||||
|
clearTimeout(closeTimeout) // Clear the existing timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
const [popup, popupContent] = getElements()
|
||||||
|
if (popup === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
popup.classList.remove('success', 'error') // Clear previous types
|
||||||
|
popup.classList.add(type)
|
||||||
|
active = true
|
||||||
|
|
||||||
|
let apiMessage
|
||||||
|
|
||||||
|
try {
|
||||||
|
apiMessage = JSON.parse(message)
|
||||||
|
if (
|
||||||
|
apiMessage &&
|
||||||
|
Object.prototype.hasOwnProperty.call(apiMessage, 'status') &&
|
||||||
|
Object.prototype.hasOwnProperty.call(apiMessage, 'message')
|
||||||
|
) {
|
||||||
|
popupContent.textContent =
|
||||||
|
'Error ' + apiMessage.status + ': ' + apiMessage.message
|
||||||
}
|
}
|
||||||
popup.classList.remove('success', 'error'); // Clear previous types
|
} catch (error) {
|
||||||
popup.classList.add(type);
|
popupContent.textContent = message
|
||||||
|
}
|
||||||
|
|
||||||
let apiMessage;
|
popup.style.right = '0em'
|
||||||
|
|
||||||
try {
|
// Don't auto-hide for 'action' type popups
|
||||||
apiMessage = JSON.parse(message);
|
if (type === 'action') {
|
||||||
// Check if 'apiMessage' has 'status' and 'message' properties
|
popup.classList.add('success')
|
||||||
if (apiMessage &&
|
return
|
||||||
Object.prototype.hasOwnProperty.call(apiMessage, "status") &&
|
}
|
||||||
Object.prototype.hasOwnProperty.call(apiMessage, "message")) {
|
|
||||||
popupContent.textContent = "Error " + apiMessage.status + ": " + apiMessage.message;
|
if (!autoclose || !active) {
|
||||||
}
|
return
|
||||||
} catch (error) {
|
}
|
||||||
popupContent.textContent = message;
|
|
||||||
|
// Set a new timeout for closing
|
||||||
|
closeTimeout = setTimeout(() => {
|
||||||
|
if (active) {
|
||||||
|
closePopUp()
|
||||||
}
|
}
|
||||||
|
}, 5000)
|
||||||
popup.style.right = '1em';
|
|
||||||
|
|
||||||
// don't hide for actions
|
|
||||||
if (type === "action") {
|
|
||||||
popup.classList.add("success");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start animation: bring the popup into view
|
|
||||||
// Automatically hide after 10 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
closePopUp();
|
|
||||||
}, 10000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function closePopUp() {
|
export function closePopUp() {
|
||||||
const [popup, popupContent] = getElements();
|
active = false
|
||||||
if (popupContent == undefined) {
|
const [popup, popupContent] = getElements()
|
||||||
return
|
if (popupContent == undefined) {
|
||||||
}
|
return
|
||||||
if (popupContent.textContent == "Multiple Selection Enabled" && state.multiple) {
|
}
|
||||||
mutations.setMultiple(false)
|
if (
|
||||||
}
|
popupContent.textContent == 'Multiple Selection Enabled' &&
|
||||||
popup.style.right = '-50em'; // Slide out
|
state.multiple
|
||||||
popupContent.textContent = "no content";
|
) {
|
||||||
|
mutations.setMultiple(false)
|
||||||
|
}
|
||||||
|
popup.style.right = '-50em' // Slide out
|
||||||
|
popupContent.textContent = 'no content'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getElements() {
|
function getElements() {
|
||||||
const popup = document.getElementById('popup-notification');
|
const popup = document.getElementById('popup-notification')
|
||||||
if (!popup) {
|
if (!popup) {
|
||||||
return [null, null];
|
return [null, null]
|
||||||
}
|
}
|
||||||
|
|
||||||
const popupContent = popup.querySelector('#popup-notification-content');
|
const popupContent = popup.querySelector('#popup-notification-content')
|
||||||
if (!popupContent) {
|
if (!popupContent) {
|
||||||
return [null, null];
|
return [null, null]
|
||||||
}
|
}
|
||||||
|
|
||||||
return [popup, popupContent];
|
return [popup, popupContent]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showSuccess(message) {
|
export function showSuccess(message) {
|
||||||
showPopup('success', message);
|
showPopup('success', message)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showError(message) {
|
export function showError(message) {
|
||||||
showPopup('error', message);
|
showPopup('error', message)
|
||||||
console.error(message)
|
console.error(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showMultipleSelection() {
|
export function showMultipleSelection() {
|
||||||
showPopup("action", "Multiple Selection Enabled");
|
showPopup('action', 'Multiple Selection Enabled')
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ export const state = reactive({
|
||||||
perm: {},
|
perm: {},
|
||||||
rules: [], // Default to an empty array
|
rules: [], // Default to an empty array
|
||||||
permissions: {}, // Default to an empty object for permissions
|
permissions: {}, // Default to an empty object for permissions
|
||||||
darkMode: false, // Default to false, assuming this is a boolean
|
darkMode: true, // Default to false, assuming this is a boolean
|
||||||
profile: { // Example of additional user properties
|
profile: { // Example of additional user properties
|
||||||
username: '', // Default to an empty string
|
username: '', // Default to an empty string
|
||||||
email: '', // Default to an empty string
|
email: '', // Default to an empty string
|
||||||
|
|
|
@ -12,7 +12,6 @@ const version = window.FileBrowser.Version;
|
||||||
const commitSHA = window.FileBrowser.CommitSHA;
|
const commitSHA = window.FileBrowser.CommitSHA;
|
||||||
const logoURL = `${staticURL}/img/logo.png`;
|
const logoURL = `${staticURL}/img/logo.png`;
|
||||||
const noAuth = window.FileBrowser.NoAuth;
|
const noAuth = window.FileBrowser.NoAuth;
|
||||||
const authMethod = window.FileBrowser.AuthMethod;
|
|
||||||
const loginPage = window.FileBrowser.LoginPage;
|
const loginPage = window.FileBrowser.LoginPage;
|
||||||
const enableThumbs = window.FileBrowser.EnableThumbs;
|
const enableThumbs = window.FileBrowser.EnableThumbs;
|
||||||
const resizePreview = window.FileBrowser.ResizePreview;
|
const resizePreview = window.FileBrowser.ResizePreview;
|
||||||
|
@ -43,7 +42,6 @@ export {
|
||||||
version,
|
version,
|
||||||
commitSHA,
|
commitSHA,
|
||||||
noAuth,
|
noAuth,
|
||||||
authMethod,
|
|
||||||
loginPage,
|
loginPage,
|
||||||
enableThumbs,
|
enableThumbs,
|
||||||
resizePreview,
|
resizePreview,
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { getHumanReadableFilesize } from './filesizes.js';
|
||||||
|
|
||||||
|
describe('testSort', () => {
|
||||||
|
|
||||||
|
it('validate human readable sizes', () => {
|
||||||
|
const tests = [
|
||||||
|
{input: 1, expected:"1.0 bytes"},
|
||||||
|
{input: 1150, expected:"1.1 KB"},
|
||||||
|
{input: 5105650, expected:"4.9 MB"},
|
||||||
|
{input: 156518899684, expected:"145.8 GB"},
|
||||||
|
{input: 1020993183744, expected:"950.9 GB"},
|
||||||
|
{input: 4891498498488, expected:"4.4 TB"},
|
||||||
|
{input: 11991498498488488, expected:"10.7 PB"},
|
||||||
|
]
|
||||||
|
for (let i in tests) {
|
||||||
|
expect(getHumanReadableFilesize(tests[i].input)).toEqual(tests[i].expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -1,6 +1,7 @@
|
||||||
import { state } from "@/store";
|
import { state } from "@/store";
|
||||||
import url from "@/utils/url.js";
|
import url from "@/utils/url.js";
|
||||||
import { filesApi } from "@/api";
|
import { filesApi } from "@/api";
|
||||||
|
import { notify } from "@/notify";
|
||||||
|
|
||||||
export function checkConflict(files, items) {
|
export function checkConflict(files, items) {
|
||||||
if (typeof items === "undefined" || items === null) {
|
if (typeof items === "undefined" || items === null) {
|
||||||
|
@ -102,7 +103,11 @@ export function scanFiles(dt) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleFiles(files, base, overwrite = false) {
|
export async function handleFiles(files, base, overwrite = false) {
|
||||||
|
let blockUpdates = false;
|
||||||
|
let c = 0
|
||||||
|
let count = files.length
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
|
c += 1
|
||||||
const id = state.upload.id;
|
const id = state.upload.id;
|
||||||
let path = base;
|
let path = base;
|
||||||
|
|
||||||
|
@ -122,15 +127,39 @@ export async function handleFiles(files, base, overwrite = false) {
|
||||||
file: file.file, // Ensure `file.file` is the Blob or File
|
file: file.file, // Ensure `file.file` is the Blob or File
|
||||||
overwrite,
|
overwrite,
|
||||||
};
|
};
|
||||||
|
let last = 0;
|
||||||
await filesApi.post(item.path, item.file, item.overwrite, (event) => {
|
notify.showPopup("success",`(${c} of ${count}) Uploading ${file.name}`,false);
|
||||||
console.log(`Upload progress: ${Math.round((event.loaded / event.total) * 100)}%`);
|
await filesApi.post(
|
||||||
})
|
item.path,
|
||||||
|
item.file,
|
||||||
|
item.overwrite,
|
||||||
|
(percentComplete) => {
|
||||||
|
if (blockUpdates) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
blockUpdates = true;
|
||||||
|
// Set a timeout to reset blockUpdates after 500ms
|
||||||
|
notify.startLoading(last, percentComplete);
|
||||||
|
last = percentComplete;
|
||||||
|
setTimeout(() => {
|
||||||
|
blockUpdates = false;
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
console.log("Upload successful:", response);
|
let spinner = document.querySelector('.notification-spinner')
|
||||||
|
if (spinner) {
|
||||||
|
spinner.classList.add('hidden')
|
||||||
|
}
|
||||||
|
console.log("Upload successful!",response);
|
||||||
|
notify.showSuccess("Upload successful!");
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("Upload error:", error);
|
let spinner = document.querySelector('.notification-spinner')
|
||||||
|
if (spinner) {
|
||||||
|
spinner.classList.add('hidden')
|
||||||
|
}
|
||||||
|
notify.showError("Error uploading file: "+error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -30,11 +30,7 @@
|
||||||
</main>
|
</main>
|
||||||
<prompts :class="{ 'dark-mode': isDarkMode }"></prompts>
|
<prompts :class="{ 'dark-mode': isDarkMode }"></prompts>
|
||||||
</div>
|
</div>
|
||||||
|
<Notifications />
|
||||||
<div class="card" id="popup-notification">
|
|
||||||
<i v-on:click="closePopUp" class="material-icons">close</i>
|
|
||||||
<div id="popup-notification-content">no info</div>
|
|
||||||
</div>
|
|
||||||
<ContextMenu></ContextMenu>
|
<ContextMenu></ContextMenu>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
@ -44,8 +40,8 @@ import listingBar from "./bars/ListingBar.vue";
|
||||||
import Prompts from "@/components/prompts/Prompts.vue";
|
import Prompts from "@/components/prompts/Prompts.vue";
|
||||||
import Sidebar from "@/components/sidebar/Sidebar.vue";
|
import Sidebar from "@/components/sidebar/Sidebar.vue";
|
||||||
import ContextMenu from "@/components/ContextMenu.vue";
|
import ContextMenu from "@/components/ContextMenu.vue";
|
||||||
|
import Notifications from "@/components/Notifications.vue";
|
||||||
|
|
||||||
import { notify } from "@/notify";
|
|
||||||
import { enableExec } from "@/utils/constants";
|
import { enableExec } from "@/utils/constants";
|
||||||
import { state, getters, mutations } from "@/store";
|
import { state, getters, mutations } from "@/store";
|
||||||
|
|
||||||
|
@ -53,6 +49,7 @@ export default {
|
||||||
name: "layout",
|
name: "layout",
|
||||||
components: {
|
components: {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
|
Notifications,
|
||||||
defaultBar,
|
defaultBar,
|
||||||
editorBar,
|
editorBar,
|
||||||
listingBar,
|
listingBar,
|
||||||
|
@ -75,7 +72,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
showPadding() {
|
showPadding() {
|
||||||
return getters.showBreadCrumbs();
|
return getters.showBreadCrumbs() || getters.currentView() === "settings";
|
||||||
},
|
},
|
||||||
isLoggedIn() {
|
isLoggedIn() {
|
||||||
return getters.isLoggedIn();
|
return getters.isLoggedIn();
|
||||||
|
@ -83,9 +80,6 @@ export default {
|
||||||
moveWithSidebar() {
|
moveWithSidebar() {
|
||||||
return getters.isSidebarVisible() && getters.isStickySidebar();
|
return getters.isSidebarVisible() && getters.isStickySidebar();
|
||||||
},
|
},
|
||||||
closePopUp() {
|
|
||||||
return notify.closePopUp;
|
|
||||||
},
|
|
||||||
progress() {
|
progress() {
|
||||||
return getters.progress(); // Access getter directly from the store
|
return getters.progress(); // Access getter directly from the store
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<div class="login-brand brand-text">
|
<div class="login-brand brand-text">
|
||||||
<h3>{{ loginName }}</h3>
|
<h3>{{ loginName }}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error !== ''" class="wrong">{{ error }}</div>
|
<div v-if="error !== ''" class="wrong">{{ error }}</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
@ -70,7 +70,7 @@ export default {
|
||||||
name: () => name || "FileBrowser Quantum",
|
name: () => name || "FileBrowser Quantum",
|
||||||
logoURL: () => logoURL,
|
logoURL: () => logoURL,
|
||||||
isDarkMode() {
|
isDarkMode() {
|
||||||
return darkMode === true;
|
return darkMode;
|
||||||
},
|
},
|
||||||
loginName() {
|
loginName() {
|
||||||
return name
|
return name
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="no-select" style="padding-bottom: 35vh">
|
<div class="no-select">
|
||||||
<div v-if="loading">
|
<div v-if="loading">
|
||||||
<h2 class="message delayed">
|
<h2 class="message delayed">
|
||||||
<div class="spinner">
|
<div class="spinner">
|
||||||
|
|
4
makefile
4
makefile
|
@ -14,7 +14,7 @@ update:
|
||||||
cd backend && go get -u ./... && go mod tidy && cd ../frontend && npm update
|
cd backend && go get -u ./... && go mod tidy && cd ../frontend && npm update
|
||||||
|
|
||||||
build:
|
build:
|
||||||
docker build --build-arg="VERSION=testing" --build-arg="REVISION=n/a" -t gtstef/filebrowser .
|
docker build --build-arg="VERSION=testing" --build-arg="REVISION=n/a" -t gtstef/filebrowser -f _docker/Dockerfile .
|
||||||
|
|
||||||
build-backend:
|
build-backend:
|
||||||
cd backend && go build -o filebrowser --ldflags="-w -s -X 'github.com/gtsteffaniak/filebrowser/backend/version.CommitSHA=testingCommit' -X 'github.com/gtsteffaniak/filebrowser/backend/version.Version=testing'"
|
cd backend && go build -o filebrowser --ldflags="-w -s -X 'github.com/gtsteffaniak/filebrowser/backend/version.CommitSHA=testingCommit' -X 'github.com/gtsteffaniak/filebrowser/backend/version.Version=testing'"
|
||||||
|
@ -53,7 +53,7 @@ test-frontend:
|
||||||
|
|
||||||
test-playwright: run-frontend
|
test-playwright: run-frontend
|
||||||
cd backend && GOOS=linux go build -o filebrowser . && cd .. && \
|
cd backend && GOOS=linux go build -o filebrowser . && cd .. && \
|
||||||
docker build -t filebrowser-playwright-tests -f Dockerfile.playwright .
|
docker build -t filebrowser-playwright-tests -f _docker/Dockerfile.playwright .
|
||||||
docker run --rm --name filebrowser-playwright-tests filebrowser-playwright-tests
|
docker run --rm --name filebrowser-playwright-tests filebrowser-playwright-tests
|
||||||
|
|
||||||
# Run on a windows machine!
|
# Run on a windows machine!
|
||||||
|
|
Loading…
Reference in New Issue