mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-13 12:41:59 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ac9802e5c | ||
|
|
742776c492 | ||
|
|
3ceaf43b86 | ||
|
|
bb420e9939 | ||
|
|
710d9c523b | ||
|
|
ac75b5344d | ||
|
|
64c5a24d1f | ||
|
|
f3567c01e8 | ||
|
|
69de3d712b | ||
|
|
4c8e35ebe0 | ||
|
|
9983cde9b6 | ||
|
|
b4b92bf852 | ||
|
|
2ec0c457cf | ||
|
|
c272eb1e61 | ||
|
|
bae39c8586 | ||
|
|
62d1cd88a1 | ||
|
|
0bad14b51e | ||
|
|
74ccfab070 | ||
|
|
5f185597c9 | ||
|
|
9e9109984d |
No files matched your search
@@ -12,6 +12,12 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Find latest tag
|
||||
run: |
|
||||
echo "LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV
|
||||
echo "latest tag is $LATEST_TAG"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
@@ -26,22 +32,15 @@ jobs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: gtstef/filebrowser
|
||||
- name: Get latest release tag and commit SHA
|
||||
id: get_tag_and_sha
|
||||
run: |
|
||||
latest_tag=$(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
latest_commit=$(git rev-list -n 1 $latest_tag)
|
||||
echo "latest_tag=${latest_tag}" >> $GITHUB_ENV
|
||||
echo "latest_commit=${latest_commit}" >> $GITHUB_ENV
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
build-args: |
|
||||
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
||||
VERSION=${{ env.LATEST_TAG }}
|
||||
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: 'gtstef/filebrowser:latest'
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -52,5 +52,5 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
version=${{ steps.meta.outputs.version }}
|
||||
commitSHA=${{ steps.meta.outputs.revision }}
|
||||
VERSION=${{ steps.meta.outputs.version }}
|
||||
REVISION=${{ steps.meta.outputs.revision }}
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
JSON="${{ steps.meta.outputs.tags }}"
|
||||
# Use jq to remove 'v' from the version field
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
|
||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||
echo "CLEANED_TAG=$JSON" >> $GITHUB_ENV
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
@@ -46,5 +46,5 @@ jobs:
|
||||
platforms: linux/amd64
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||
tags: ${{ env.CLEANED_TAG }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -2,6 +2,42 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
|
||||
|
||||
## v0.2.10
|
||||
|
||||
**New Features**:
|
||||
- Allows user creation command line arguments https://github.com/gtsteffaniak/filebrowser/issues/196
|
||||
- Folder sizes are always shown, leveraging the index. https://github.com/gtsteffaniak/filebrowser/issues/138
|
||||
- Searching files based on filesize is no longer slower.
|
||||
|
||||
**Bugfixes**:
|
||||
- fixes file selection usage when in single-click mode https://github.com/gtsteffaniak/filebrowser/issues/214
|
||||
- Fixed displayed search context on root directory
|
||||
- Fixed issue searching "smaller than" actually returned files "larger than"
|
||||
|
||||
**Notes**:
|
||||
- Memory usage from index is reduced by ~40%
|
||||
- Indexing time has increased 2x due to the extra processing time required to calculate directory sizes.
|
||||
- File size calcuations use 1024 base vs previous 1000 base (matching windows explorer)
|
||||
|
||||
## v0.2.9
|
||||
|
||||
This release focused on UI navigation experience. Improving keyboard navigation and adds right click context menu.
|
||||
|
||||
**New Features**:
|
||||
- listing view items are middle-clickable on selected listing or when in single-click mode.
|
||||
- listing view items can be navigated via arrow keys.
|
||||
- listing view can jump to items using letters and number keys to cycle through files that start with that character.
|
||||
- You can use the enter key and backspace key to navigate backwards and forwards on selected items.
|
||||
- ctr-space will open/close the search (leaving ctr-f to browser default find prompt)
|
||||
- Added right-click context menu to replace the file selection prompt.
|
||||
|
||||
**Bugfixes**:
|
||||
- Fixed drag to upload not working.
|
||||
- Fixed shared video link issues.
|
||||
- Fixed user edit bug related to other user.
|
||||
- Fixed password reset bug.
|
||||
- Fixed loading state getting stuck.
|
||||
|
||||
## v0.2.8
|
||||
|
||||
- **Feature**: New gallary view scaling options (closes [#141](https://github.com/gtsteffaniak/filebrowser/issues/141))
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<p align="center">
|
||||
<img src="frontend/public/img/icons/favicon-256x256.png" width="100" title="Login With Custom URL">
|
||||
</p>
|
||||
<h3 align="center">Filebrowser Quantum - A modern web-based file manager</h3>
|
||||
<h3 align="center">FileBrowser Quantum - A modern web-based file manager</h3>
|
||||
<p align="center">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/8ba93582-aba2-4996-8ac3-25f763a2e596" title="Main Screenshot">
|
||||
<img width="800" src="https://private-user-images.githubusercontent.com/42989099/367975355-3d6f4619-4985-4ce3-952f-286510dff4f1.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjgxNTA2MjEsIm5iZiI6MTcyODE1MDMyMSwicGF0aCI6Ii80Mjk4OTA5OS8zNjc5NzUzNTUtM2Q2ZjQ2MTktNDk4NS00Y2UzLTk1MmYtMjg2NTEwZGZmNGYxLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEwMDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMDA1VDE3NDUyMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTg1OGNlMWM3M2I1ZmY3MDcxMGU1ODc3N2ZkMjI5YWQ3YzEyODRmNDU0ZDkxMjJhNTU0ZGY1MDQ2YmIwOWRmMTgmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.mOl0Hm70XmQEk-DPzx1FbwrpxNMDAqb-WDprs1HK-mc" title="Main Screenshot">
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
@@ -15,12 +15,12 @@
|
||||
> Starting with v0.2.4 *ALL* share links need to be re-created (due to
|
||||
> security fix).
|
||||
|
||||
Filebrowser Quantum is a fork of the filebrowser opensource project with the
|
||||
FileBrowser Quantum is a fork of the filebrowser opensource project with the
|
||||
following changes:
|
||||
|
||||
1. [x] Enhanced lightning fast indexed search
|
||||
- Real-time results as you type
|
||||
- Works with more type filters
|
||||
1. [x] Efficiently indexed files
|
||||
- Real-time search results as you type
|
||||
- Search Works with more type filters
|
||||
- Enhanced interactive results page.
|
||||
2. [x] Revamped and simplified GUI navbar and sidebar menu.
|
||||
- Additional compact view mode as well as refreshed view mode
|
||||
@@ -33,7 +33,7 @@ following changes:
|
||||
|
||||
## About
|
||||
|
||||
Filebrowser Quantum provides a file managing interface within a specified directory
|
||||
FileBrowser Quantum provides a file managing interface within a specified directory
|
||||
and can be used to upload, delete, preview, rename, and edit your files.
|
||||
It allows the creation of multiple users and each user can have its
|
||||
directory.
|
||||
@@ -44,7 +44,7 @@ aesthetics and performance. Improved search, simplified ui
|
||||
(without removing features) and more secure and up-to-date
|
||||
build are just a few examples.
|
||||
|
||||
Filebrowser Quantum differs significantly to the original.
|
||||
FileBrowser Quantum differs significantly to the original.
|
||||
There are hundreds of thousands of lines changed and they are generally
|
||||
no longer compatible with each other. This has been intentional -- the
|
||||
focus of this fork is on a few key principles:
|
||||
@@ -131,39 +131,30 @@ Not using docker (not recommended), download your binary from releases and run w
|
||||
./filebrowser -c <filebrowser.yml or other /path/to/config.yaml>
|
||||
```
|
||||
|
||||
## Command Line Usage
|
||||
|
||||
There are very few commands available. There are 3 actions done via command line:
|
||||
|
||||
1. Running the program, as shown on install step. Only argument used is the config file, if you choose to override default "filebrowser.yaml"
|
||||
2. Checking the version info via `./filebrowser version`
|
||||
3. Updating the DB, which currently only supports adding users via `./filebrowser set -u username,password [-a] [-s "example/scope"]`
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration is now done via a single configuration file:
|
||||
`filebrowser.yaml`, here is an example of minimal [configuration
|
||||
file](./backend/filebrowser.yaml).
|
||||
|
||||
View the [Configuration Help Page](./configuration.md) for available
|
||||
View the [Configuration Help Page](./docs/configuration.md) for available
|
||||
configuration options and other help.
|
||||
|
||||
|
||||
## Migration from filebrowser/filebrowser
|
||||
|
||||
If you currently use the original opensource filebrowser
|
||||
but want to try using this. I recommend you start fresh without
|
||||
reusing the database, but there are a few things you'll need to do if you
|
||||
must migrate:
|
||||
|
||||
1. Create a configuration file as mentioned above.
|
||||
2. Copy your database file from the original filebrowser to the path of
|
||||
the new one.
|
||||
3. Update the configuration file to use the database (under server in
|
||||
filebrowser.yml)
|
||||
4. If you are using docker, update the docker-compose file or docker run
|
||||
command to use the config file as described in the install section
|
||||
above.
|
||||
5. If you are not using docker, just make sure you run filebrowser -c
|
||||
filebrowser.yml and have a valid filebrowser config.
|
||||
|
||||
|
||||
The filebrowser Quantum application should run with the same user and rules that
|
||||
you have from the original. But keep in mind the differences that are
|
||||
mentioned at the top of this readme.
|
||||
|
||||
If you currently use the original filebrowser but want to try using this.
|
||||
I recommend you start fresh without reusing the database. If you want to
|
||||
migrate your existing database to FileBrowser Quantum, visit the [migration
|
||||
readme](./docs/migration.md)
|
||||
|
||||
## Comparison Chart
|
||||
|
||||
@@ -217,4 +208,4 @@ Chromecast support | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
|
||||
## Roadmap
|
||||
|
||||
see [Roadmap Page](./roadmap.md)
|
||||
see [Roadmap Page](./docs/roadmap.md)
|
||||
@@ -187,7 +187,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||
func (a *HookAuth) GetUser(d *users.User) *users.User {
|
||||
// adds all permissions when user is admin
|
||||
isAdmin := d.Perm.Admin
|
||||
perms := users.Permissions{
|
||||
perms := settings.Permissions{
|
||||
Admin: isAdmin,
|
||||
Execute: isAdmin || d.Perm.Execute,
|
||||
Create: isAdmin || d.Perm.Create,
|
||||
|
||||
@@ -5,49 +5,39 @@
|
||||
? github.com/gtsteffaniak/filebrowser/auth [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/cmd [no test files]
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/diskcache 0.003s
|
||||
ok github.com/gtsteffaniak/filebrowser/diskcache 0.004s
|
||||
? github.com/gtsteffaniak/filebrowser/errors [no test files]
|
||||
2024/10/07 12:46:34 could not update unknown type: unknown
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
pkg: github.com/gtsteffaniak/filebrowser/files
|
||||
cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz
|
||||
BenchmarkFillIndex-8 10 3587120 ns/op 273640 B/op 2013 allocs/op
|
||||
BenchmarkSearchAllIndexes-8 10 31291180 ns/op 19500700 B/op 298636 allocs/op
|
||||
BenchmarkFillIndex-8 10 3847878 ns/op 758424 B/op 5567 allocs/op
|
||||
BenchmarkSearchAllIndexes-8 10 780431 ns/op 173444 B/op 2014 allocs/op
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/files 0.408s
|
||||
ok github.com/gtsteffaniak/filebrowser/files 0.073s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/fileutils 0.003s
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 h: 401 <nil>
|
||||
2024/02/07 07:16:43 h: 401 <nil>
|
||||
2024/02/07 07:16:43 h: 401 <nil>
|
||||
2024/02/07 07:16:43 h: 401 <nil>
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 h: 401 <nil>
|
||||
2024/02/07 07:16:43 h: 401 <nil>
|
||||
2024/10/07 12:46:34 h: 401 <nil>
|
||||
2024/10/07 12:46:34 h: 401 <nil>
|
||||
2024/10/07 12:46:34 h: 401 <nil>
|
||||
2024/10/07 12:46:34 h: 401 <nil>
|
||||
2024/10/07 12:46:34 h: 401 <nil>
|
||||
2024/10/07 12:46:34 h: 401 <nil>
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/http 0.202s
|
||||
ok github.com/gtsteffaniak/filebrowser/http 0.080s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/img 0.125s
|
||||
ok github.com/gtsteffaniak/filebrowser/img 0.137s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/rules 0.002s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/runner 0.003s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/settings 0.005s
|
||||
ok github.com/gtsteffaniak/filebrowser/settings 0.004s
|
||||
? github.com/gtsteffaniak/filebrowser/share [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/storage [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/storage/bolt [no test files]
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/users 0.003s
|
||||
ok github.com/gtsteffaniak/filebrowser/users 0.002s
|
||||
? github.com/gtsteffaniak/filebrowser/utils [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/version [no test files]
|
||||
+185
-128
@@ -11,27 +11,28 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"embed"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/diskcache"
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
fbhttp "github.com/gtsteffaniak/filebrowser/http"
|
||||
"github.com/gtsteffaniak/filebrowser/img"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/version"
|
||||
)
|
||||
|
||||
//go:embed dist/*
|
||||
var assets embed.FS
|
||||
|
||||
var nonEmbededFS = os.Getenv("FILEBROWSER_NO_EMBEDED") == "true"
|
||||
var (
|
||||
nonEmbededFS = os.Getenv("FILEBROWSER_NO_EMBEDED") == "true"
|
||||
)
|
||||
|
||||
type dirFS struct {
|
||||
http.Dir
|
||||
@@ -41,102 +42,119 @@ func (d dirFS) Open(name string) (fs.File, error) {
|
||||
return d.Dir.Open(name)
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Define a flag for the config option (-c or --config)
|
||||
configFlag := pflag.StringP("config", "c", "filebrowser.yaml", "Path to the config file")
|
||||
// Bind the flags to the pflag command line parser
|
||||
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
|
||||
pflag.Parse()
|
||||
log.Println("Initializing with config file:", *configFlag)
|
||||
log.Println("Embeded Frontend:", !nonEmbededFS)
|
||||
settings.Initialize(*configFlag)
|
||||
func getStore(config string) (*storage.Storage, bool) {
|
||||
// Use the config file (global flag)
|
||||
log.Printf("Using Config file : %v", config)
|
||||
settings.Initialize(config)
|
||||
store, hasDB, err := storage.InitializeDb(settings.Config.Server.Database)
|
||||
if err != nil {
|
||||
log.Fatal("could not load db info: ", err)
|
||||
}
|
||||
return store, hasDB
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "filebrowser",
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
serverConfig := settings.Config.Server
|
||||
if !d.hadDB {
|
||||
quickSetup(d)
|
||||
}
|
||||
if serverConfig.NumImageProcessors < 1 {
|
||||
log.Fatal("Image resize workers count could not be < 1")
|
||||
}
|
||||
imgSvc := img.New(serverConfig.NumImageProcessors)
|
||||
|
||||
cacheDir := "/tmp"
|
||||
var fileCache diskcache.Interface
|
||||
|
||||
// Use file cache if cacheDir is specified
|
||||
if cacheDir != "" {
|
||||
var err error
|
||||
fileCache, err = diskcache.NewFileCache(cacheDir)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create file cache: %v", err)
|
||||
}
|
||||
} else {
|
||||
// No-op cache if no cacheDir is specified
|
||||
fileCache = diskcache.NewNoOp()
|
||||
}
|
||||
// initialize indexing and schedule indexing ever n minutes (default 5)
|
||||
go files.InitializeIndex(serverConfig.IndexingInterval, serverConfig.Indexing)
|
||||
_, err := os.Stat(serverConfig.Root)
|
||||
checkErr(fmt.Sprint("cmd os.Stat ", serverConfig.Root), err)
|
||||
var listener net.Listener
|
||||
address := serverConfig.Address + ":" + strconv.Itoa(serverConfig.Port)
|
||||
switch {
|
||||
case serverConfig.Socket != "":
|
||||
listener, err = net.Listen("unix", serverConfig.Socket)
|
||||
checkErr("net.Listen", err)
|
||||
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
|
||||
checkErr("cmd.Flags().GetUint32", err)
|
||||
err = os.Chmod(serverConfig.Socket, os.FileMode(socketPerm))
|
||||
checkErr("os.Chmod", err)
|
||||
case serverConfig.TLSKey != "" && serverConfig.TLSCert != "":
|
||||
cer, err := tls.LoadX509KeyPair(serverConfig.TLSCert, serverConfig.TLSKey) //nolint:govet
|
||||
checkErr("tls.LoadX509KeyPair", err)
|
||||
listener, err = tls.Listen("tcp", address, &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
Certificates: []tls.Certificate{cer}},
|
||||
)
|
||||
checkErr("tls.Listen", err)
|
||||
default:
|
||||
listener, err = net.Listen("tcp", address)
|
||||
checkErr("net.Listen", err)
|
||||
}
|
||||
sigc := make(chan os.Signal, 1)
|
||||
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
||||
go cleanupHandler(listener, sigc)
|
||||
if !nonEmbededFS {
|
||||
assetsFs, err := fs.Sub(assets, "dist")
|
||||
if err != nil {
|
||||
log.Fatal("Could not embed frontend. Does backend/cmd/dist exist? Must be built and exist first")
|
||||
}
|
||||
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, &serverConfig, assetsFs)
|
||||
checkErr("fbhttp.NewHandler", err)
|
||||
defer listener.Close()
|
||||
log.Println("Listening on", listener.Addr().String())
|
||||
//nolint: gosec
|
||||
if err := http.Serve(listener, handler); err != nil {
|
||||
log.Fatalf("Could not start server on port %d: %v", serverConfig.Port, err)
|
||||
}
|
||||
} else {
|
||||
assetsFs := dirFS{Dir: http.Dir("frontend/dist")}
|
||||
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, &serverConfig, assetsFs)
|
||||
checkErr("fbhttp.NewHandler", err)
|
||||
defer listener.Close()
|
||||
log.Println("Listening on", listener.Addr().String())
|
||||
//nolint: gosec
|
||||
if err := http.Serve(listener, handler); err != nil {
|
||||
log.Fatalf("Could not start server on port %d: %v", serverConfig.Port, err)
|
||||
}
|
||||
}
|
||||
|
||||
}, pythonConfig{allowNoDB: true}),
|
||||
func generalUsage() {
|
||||
fmt.Printf(`usage: ./html-web-crawler <command> [options] --urls <urls>
|
||||
commands:
|
||||
collect Collect data from URLs
|
||||
crawl Crawl URLs and collect data
|
||||
install Install chrome browser for javascript enabled scraping.
|
||||
Note: Consider instead to install via native package manager,
|
||||
then set "CHROME_EXECUTABLE" in the environment
|
||||
` + "\n")
|
||||
}
|
||||
|
||||
func StartFilebrowser() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
// Global flags
|
||||
var configPath string
|
||||
var help bool
|
||||
// Override the default usage output to use generalUsage()
|
||||
flag.Usage = generalUsage
|
||||
flag.StringVar(&configPath, "c", "filebrowser.yaml", "Path to the config file.")
|
||||
flag.BoolVar(&help, "h", false, "Get help about commands")
|
||||
|
||||
// Parse global flags (before subcommands)
|
||||
flag.Parse() // print generalUsage on error
|
||||
|
||||
// Show help if requested
|
||||
if help {
|
||||
generalUsage()
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new FlagSet for the 'set' subcommand
|
||||
setCmd := flag.NewFlagSet("set", flag.ExitOnError)
|
||||
var user, scope, dbConfig string
|
||||
var asAdmin bool
|
||||
|
||||
setCmd.StringVar(&user, "u", "", "Comma-separated username and password: \"set -u <username>,<password>\"")
|
||||
setCmd.BoolVar(&asAdmin, "a", false, "Create user as admin user, used in combination with -u")
|
||||
setCmd.StringVar(&scope, "s", "", "Specify a user scope, otherwise default user config scope is used")
|
||||
setCmd.StringVar(&dbConfig, "c", "filebrowser.yaml", "Path to the config file.")
|
||||
|
||||
// Parse subcommand flags only if a subcommand is specified
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "set":
|
||||
err := setCmd.Parse(os.Args)
|
||||
if err != nil {
|
||||
setCmd.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
userInfo := strings.Split(user, ",")
|
||||
if len(userInfo) < 2 {
|
||||
fmt.Println("not enough info to create user: \"set -u username,password\"")
|
||||
setCmd.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
username := userInfo[0]
|
||||
password := userInfo[1]
|
||||
getStore(dbConfig)
|
||||
// Create the user logic
|
||||
if asAdmin {
|
||||
log.Printf("Creating user as admin: %s\n", username)
|
||||
} else {
|
||||
log.Printf("Creating user: %s\n", username)
|
||||
}
|
||||
newUser := users.User{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
if scope != "" {
|
||||
newUser.Scope = scope
|
||||
}
|
||||
err = storage.CreateUser(newUser, asAdmin)
|
||||
if err != nil {
|
||||
log.Fatal("Could not create user: ", err)
|
||||
}
|
||||
return
|
||||
case "version":
|
||||
fmt.Println("FileBrowser Quantum - A modern web-based file manager")
|
||||
fmt.Printf("Version : %v\n", version.Version)
|
||||
fmt.Printf("Commit : %v\n", version.CommitSHA)
|
||||
fmt.Printf("Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v\n", version.Version)
|
||||
return
|
||||
}
|
||||
}
|
||||
store, dbExists := getStore(configPath)
|
||||
indexingInterval := fmt.Sprint(settings.Config.Server.IndexingInterval, " minutes")
|
||||
if !settings.Config.Server.Indexing {
|
||||
indexingInterval = "disabled"
|
||||
}
|
||||
database := fmt.Sprintf("Using existing database : %v", settings.Config.Server.Database)
|
||||
if !dbExists {
|
||||
database = fmt.Sprintf("Creating new database : %v", settings.Config.Server.Database)
|
||||
}
|
||||
log.Printf("Initializing FileBrowser Quantum (%v)\n", version.Version)
|
||||
log.Println("Embeded frontend :", !nonEmbededFS)
|
||||
log.Println(database)
|
||||
log.Println("Sources :", settings.Config.Server.Root)
|
||||
log.Print("Indexing interval : ", indexingInterval)
|
||||
|
||||
serverConfig := settings.Config.Server
|
||||
// initialize indexing and schedule indexing ever n minutes (default 5)
|
||||
go files.InitializeIndex(serverConfig.IndexingInterval, serverConfig.Indexing)
|
||||
if err := rootCMD(store, &serverConfig); err != nil {
|
||||
log.Fatal("Error starting filebrowser:", err)
|
||||
}
|
||||
}
|
||||
@@ -148,38 +166,77 @@ func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfac
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func quickSetup(d pythonData) {
|
||||
settings.Config.Auth.Key = generateKey()
|
||||
if settings.Config.Auth.Method == "noauth" {
|
||||
err := d.store.Auth.Save(&auth.NoAuth{})
|
||||
checkErr("d.store.Auth.Save", err)
|
||||
func rootCMD(store *storage.Storage, serverConfig *settings.Server) error {
|
||||
if serverConfig.NumImageProcessors < 1 {
|
||||
log.Fatal("Image resize workers count could not be < 1")
|
||||
}
|
||||
imgSvc := img.New(serverConfig.NumImageProcessors)
|
||||
|
||||
cacheDir := "/tmp"
|
||||
var fileCache diskcache.Interface
|
||||
|
||||
// Use file cache if cacheDir is specified
|
||||
if cacheDir != "" {
|
||||
var err error
|
||||
fileCache, err = diskcache.NewFileCache(cacheDir)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create file cache: %v", err)
|
||||
}
|
||||
} else {
|
||||
settings.Config.Auth.Method = "password"
|
||||
err := d.store.Auth.Save(&auth.JSONAuth{})
|
||||
checkErr("d.store.Auth.Save", err)
|
||||
// No-op cache if no cacheDir is specified
|
||||
fileCache = diskcache.NewNoOp()
|
||||
}
|
||||
err := d.store.Settings.Save(&settings.Config)
|
||||
checkErr("d.store.Settings.Save", err)
|
||||
err = d.store.Settings.SaveServer(&settings.Config.Server)
|
||||
checkErr("d.store.Settings.SaveServer", err)
|
||||
user := &users.User{}
|
||||
settings.Config.UserDefaults.Apply(user)
|
||||
user.Username = settings.Config.Auth.AdminUsername
|
||||
user.Password = settings.Config.Auth.AdminPassword
|
||||
user.Perm.Admin = true
|
||||
user.Scope = "./"
|
||||
user.DarkMode = true
|
||||
user.ViewMode = "normal"
|
||||
user.LockPassword = false
|
||||
user.Perm = users.Permissions{
|
||||
Create: true,
|
||||
Rename: true,
|
||||
Modify: true,
|
||||
Delete: true,
|
||||
Share: true,
|
||||
Download: true,
|
||||
Admin: true,
|
||||
|
||||
fbhttp.SetupEnv(store, serverConfig, fileCache)
|
||||
|
||||
_, err := os.Stat(serverConfig.Root)
|
||||
utils.CheckErr(fmt.Sprint("cmd os.Stat ", serverConfig.Root), err)
|
||||
var listener net.Listener
|
||||
address := serverConfig.Address + ":" + strconv.Itoa(serverConfig.Port)
|
||||
switch {
|
||||
case serverConfig.Socket != "":
|
||||
listener, err = net.Listen("unix", serverConfig.Socket)
|
||||
utils.CheckErr("net.Listen", err)
|
||||
err = os.Chmod(serverConfig.Socket, os.FileMode(0666)) // socket-perm
|
||||
utils.CheckErr("os.Chmod", err)
|
||||
case serverConfig.TLSKey != "" && serverConfig.TLSCert != "":
|
||||
cer, err := tls.LoadX509KeyPair(serverConfig.TLSCert, serverConfig.TLSKey) //nolint:govet
|
||||
utils.CheckErr("tls.LoadX509KeyPair", err)
|
||||
listener, err = tls.Listen("tcp", address, &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
Certificates: []tls.Certificate{cer}},
|
||||
)
|
||||
utils.CheckErr("tls.Listen", err)
|
||||
default:
|
||||
listener, err = net.Listen("tcp", address)
|
||||
utils.CheckErr("net.Listen", err)
|
||||
}
|
||||
err = d.store.Users.Save(user)
|
||||
checkErr("d.store.Users.Save", err)
|
||||
sigc := make(chan os.Signal, 1)
|
||||
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
||||
go cleanupHandler(listener, sigc)
|
||||
if !nonEmbededFS {
|
||||
assetsFs, err := fs.Sub(assets, "dist")
|
||||
if err != nil {
|
||||
log.Fatal("Could not embed frontend. Does backend/cmd/dist exist? Must be built and exist first")
|
||||
}
|
||||
handler, err := fbhttp.NewHandler(imgSvc, assetsFs)
|
||||
utils.CheckErr("fbhttp.NewHandler", err)
|
||||
defer listener.Close()
|
||||
log.Println("Listening on", listener.Addr().String())
|
||||
//nolint: gosec
|
||||
if err := http.Serve(listener, handler); err != nil {
|
||||
log.Fatalf("Could not start server on port %d: %v", serverConfig.Port, err)
|
||||
}
|
||||
} else {
|
||||
assetsFs := dirFS{Dir: http.Dir("frontend/dist")}
|
||||
handler, err := fbhttp.NewHandler(imgSvc, assetsFs)
|
||||
utils.CheckErr("fbhttp.NewHandler", err)
|
||||
defer listener.Close()
|
||||
log.Println("Listening on", listener.Addr().String())
|
||||
//nolint: gosec
|
||||
if err := http.Serve(listener, handler); err != nil {
|
||||
log.Fatalf("Could not start server on port %d: %v", serverConfig.Port, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+11
-9
@@ -6,7 +6,9 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -40,27 +42,27 @@ including 'index_end'.`,
|
||||
|
||||
return nil
|
||||
},
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
i, err := strconv.Atoi(args[0])
|
||||
checkErr("strconv.Atoi", err)
|
||||
utils.CheckErr("strconv.Atoi", err)
|
||||
f := i
|
||||
if len(args) == 2 { //nolint:gomnd
|
||||
f, err = strconv.Atoi(args[1])
|
||||
checkErr("strconv.Atoi", err)
|
||||
utils.CheckErr("strconv.Atoi", err)
|
||||
}
|
||||
|
||||
user := func(u *users.User) {
|
||||
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
|
||||
err := d.store.Users.Save(u)
|
||||
checkErr("d.store.Users.Save", err)
|
||||
err := store.Users.Save(u)
|
||||
utils.CheckErr("store.Users.Save", err)
|
||||
}
|
||||
|
||||
global := func(s *settings.Settings) {
|
||||
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
|
||||
err := d.store.Settings.Save(s)
|
||||
checkErr("d.store.Settings.Save", err)
|
||||
err := store.Settings.Save(s)
|
||||
utils.CheckErr("store.Settings.Save", err)
|
||||
}
|
||||
|
||||
runRules(d.store, cmd, user, global)
|
||||
}, pythonConfig{}),
|
||||
runRules(store, cmd, user, global)
|
||||
}),
|
||||
}
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(rulesCmd)
|
||||
rulesCmd.PersistentFlags().StringP("username", "u", "", "username of user to which the rules apply")
|
||||
rulesCmd.PersistentFlags().UintP("id", "i", 0, "id of user to which the rules apply")
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User)
|
||||
id := getUserIdentifier(cmd.Flags())
|
||||
if id != nil {
|
||||
user, err := st.Users.Get("", id)
|
||||
checkErr("st.Users.Get", err)
|
||||
utils.CheckErr("st.Users.Get", err)
|
||||
|
||||
if usersFn != nil {
|
||||
usersFn(user)
|
||||
@@ -44,7 +44,7 @@ func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User)
|
||||
}
|
||||
|
||||
s, err := st.Settings.Get()
|
||||
checkErr("st.Settings.Get", err)
|
||||
utils.CheckErr("st.Settings.Get", err)
|
||||
|
||||
if globalFn != nil {
|
||||
globalFn(s)
|
||||
@@ -67,11 +67,6 @@ func getUserIdentifier(flags *pflag.FlagSet) interface{} {
|
||||
}
|
||||
|
||||
func printRules(rulez []rules.Rule, id interface{}) {
|
||||
if id == nil {
|
||||
fmt.Printf("Global Rules:\n\n")
|
||||
} else {
|
||||
fmt.Printf("Rules for user %v:\n\n", id)
|
||||
}
|
||||
|
||||
for id, rule := range rulez {
|
||||
fmt.Printf("(%d) ", id)
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/rules"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -21,7 +23,7 @@ var rulesAddCmd = &cobra.Command{
|
||||
Short: "Add a global rule or user rule",
|
||||
Long: `Add a global rule or user rule.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
allow := mustGetBool(cmd.Flags(), "allow")
|
||||
regex := mustGetBool(cmd.Flags(), "regex")
|
||||
exp := args[0]
|
||||
@@ -43,16 +45,16 @@ var rulesAddCmd = &cobra.Command{
|
||||
|
||||
user := func(u *users.User) {
|
||||
u.Rules = append(u.Rules, rule)
|
||||
err := d.store.Users.Save(u)
|
||||
checkErr("d.store.Users.Save", err)
|
||||
err := store.Users.Save(u)
|
||||
utils.CheckErr("store.Users.Save", err)
|
||||
}
|
||||
|
||||
global := func(s *settings.Settings) {
|
||||
s.Rules = append(s.Rules, rule)
|
||||
err := d.store.Settings.Save(s)
|
||||
checkErr("d.store.Settings.Save", err)
|
||||
err := store.Settings.Save(s)
|
||||
utils.CheckErr("store.Settings.Save", err)
|
||||
}
|
||||
|
||||
runRules(d.store, cmd, user, global)
|
||||
}, pythonConfig{}),
|
||||
runRules(store, cmd, user, global)
|
||||
}),
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -13,7 +14,7 @@ var rulesLsCommand = &cobra.Command{
|
||||
Short: "List global rules or user specific rules",
|
||||
Long: `List global rules or user specific rules.`,
|
||||
Args: cobra.NoArgs,
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
runRules(d.store, cmd, nil, nil)
|
||||
}, pythonConfig{}),
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
runRules(store, cmd, nil, nil)
|
||||
}),
|
||||
}
|
||||
@@ -11,10 +11,6 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(usersCmd)
|
||||
}
|
||||
|
||||
var usersCmd = &cobra.Command{
|
||||
Use: "users",
|
||||
Short: "Users management utility",
|
||||
|
||||
@@ -3,7 +3,9 @@ package cmd
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -15,26 +17,26 @@ var usersAddCmd = &cobra.Command{
|
||||
Short: "Create a new user",
|
||||
Long: `Create a new user and add it to the database.`,
|
||||
Args: cobra.ExactArgs(2), //nolint:gomnd
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
user := &users.User{
|
||||
Username: args[0],
|
||||
Password: args[1],
|
||||
LockPassword: mustGetBool(cmd.Flags(), "lockPassword"),
|
||||
}
|
||||
servSettings, err := d.store.Settings.GetServer()
|
||||
checkErr("d.store.Settings.GetServer()", err)
|
||||
servSettings, err := store.Settings.GetServer()
|
||||
utils.CheckErr("store.Settings.GetServer()", err)
|
||||
// since getUserDefaults() polluted s.Defaults.Scope
|
||||
// which makes the Scope not the one saved in the db
|
||||
// we need the right s.Defaults.Scope here
|
||||
s2, err := d.store.Settings.Get()
|
||||
checkErr("d.store.Settings.Get()", err)
|
||||
s2, err := store.Settings.Get()
|
||||
utils.CheckErr("store.Settings.Get()", err)
|
||||
|
||||
userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root)
|
||||
checkErr("s2.MakeUserDir", err)
|
||||
utils.CheckErr("s2.MakeUserDir", err)
|
||||
user.Scope = userHome
|
||||
|
||||
err = d.store.Users.Save(user)
|
||||
checkErr("d.store.Users.Save", err)
|
||||
err = store.Users.Save(user)
|
||||
utils.CheckErr("store.Users.Save", err)
|
||||
printUsers([]*users.User{user})
|
||||
}, pythonConfig{}),
|
||||
}),
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -14,11 +16,11 @@ var usersExportCmd = &cobra.Command{
|
||||
Long: `Export all users to a json or yaml file. Please indicate the
|
||||
path to the file where you want to write the users.`,
|
||||
Args: jsonYamlArg,
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
list, err := d.store.Users.Gets("")
|
||||
checkErr("d.store.Users.Gets", err)
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
list, err := store.Users.Gets("")
|
||||
utils.CheckErr("store.Users.Gets", err)
|
||||
|
||||
err = marshal(args[0], list)
|
||||
checkErr("marshal", err)
|
||||
}, pythonConfig{}),
|
||||
utils.CheckErr("marshal", err)
|
||||
}),
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package cmd
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -26,7 +28,7 @@ var usersLsCmd = &cobra.Command{
|
||||
Run: findUsers,
|
||||
}
|
||||
|
||||
var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
var findUsers = cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
var (
|
||||
list []*users.User
|
||||
user *users.User
|
||||
@@ -36,16 +38,16 @@ var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
if len(args) == 1 {
|
||||
username, id := parseUsernameOrID(args[0])
|
||||
if username != "" {
|
||||
user, err = d.store.Users.Get("", username)
|
||||
user, err = store.Users.Get("", username)
|
||||
} else {
|
||||
user, err = d.store.Users.Get("", id)
|
||||
user, err = store.Users.Get("", id)
|
||||
}
|
||||
|
||||
list = []*users.User{user}
|
||||
} else {
|
||||
list, err = d.store.Users.Gets("")
|
||||
list, err = store.Users.Gets("")
|
||||
}
|
||||
|
||||
checkErr("findUsers", err)
|
||||
utils.CheckErr("findUsers", err)
|
||||
printUsers(list)
|
||||
}, pythonConfig{})
|
||||
})
|
||||
+17
-15
@@ -8,7 +8,9 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -25,47 +27,47 @@ file. You can use this command to import new users to your
|
||||
installation. For that, just don't place their ID on the files
|
||||
list or set it to 0.`,
|
||||
Args: jsonYamlArg,
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
fd, err := os.Open(args[0])
|
||||
checkErr("os.Open", err)
|
||||
utils.CheckErr("os.Open", err)
|
||||
defer fd.Close()
|
||||
|
||||
list := []*users.User{}
|
||||
err = unmarshal(args[0], &list)
|
||||
checkErr("unmarshal", err)
|
||||
utils.CheckErr("unmarshal", err)
|
||||
|
||||
if mustGetBool(cmd.Flags(), "replace") {
|
||||
oldUsers, err := d.store.Users.Gets("")
|
||||
checkErr("d.store.Users.Gets", err)
|
||||
oldUsers, err := store.Users.Gets("")
|
||||
utils.CheckErr("store.Users.Gets", err)
|
||||
|
||||
err = marshal("users.backup.json", list)
|
||||
checkErr("marshal users.backup.json", err)
|
||||
utils.CheckErr("marshal users.backup.json", err)
|
||||
|
||||
for _, user := range oldUsers {
|
||||
err = d.store.Users.Delete(user.ID)
|
||||
checkErr("d.store.Users.Delete", err)
|
||||
err = store.Users.Delete(user.ID)
|
||||
utils.CheckErr("store.Users.Delete", err)
|
||||
}
|
||||
}
|
||||
|
||||
overwrite := mustGetBool(cmd.Flags(), "overwrite")
|
||||
|
||||
for _, user := range list {
|
||||
onDB, err := d.store.Users.Get("", user.ID)
|
||||
onDB, err := store.Users.Get("", user.ID)
|
||||
|
||||
// User exists in DB.
|
||||
if err == nil {
|
||||
if !overwrite {
|
||||
newErr := errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered")
|
||||
checkErr("", newErr)
|
||||
utils.CheckErr("", newErr)
|
||||
}
|
||||
|
||||
// If the usernames mismatch, check if there is another one in the DB
|
||||
// with the new username. If there is, print an error and cancel the
|
||||
// operation
|
||||
if user.Username != onDB.Username {
|
||||
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
|
||||
if conflictuous, err := store.Users.Get("", user.Username); err == nil { //nolint:govet
|
||||
newErr := usernameConflictError(user.Username, conflictuous.ID, user.ID)
|
||||
checkErr("usernameConflictError", newErr)
|
||||
utils.CheckErr("usernameConflictError", newErr)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -74,10 +76,10 @@ list or set it to 0.`,
|
||||
user.ID = 0
|
||||
}
|
||||
|
||||
err = d.store.Users.Save(user)
|
||||
checkErr("d.store.Users.Save", err)
|
||||
err = store.Users.Save(user)
|
||||
utils.CheckErr("store.Users.Save", err)
|
||||
}
|
||||
}, pythonConfig{}),
|
||||
}),
|
||||
}
|
||||
|
||||
func usernameConflictError(username string, originalID, newID uint) error {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -15,17 +17,17 @@ var usersRmCmd = &cobra.Command{
|
||||
Short: "Delete a user by username or id",
|
||||
Long: `Delete a user by username or id`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
username, id := parseUsernameOrID(args[0])
|
||||
var err error
|
||||
|
||||
if username != "" {
|
||||
err = d.store.Users.Delete(username)
|
||||
err = store.Users.Delete(username)
|
||||
} else {
|
||||
err = d.store.Users.Delete(id)
|
||||
err = store.Users.Delete(id)
|
||||
}
|
||||
|
||||
checkErr("usersRmCmd", err)
|
||||
fmt.Println("user deleted successfully")
|
||||
}, pythonConfig{}),
|
||||
utils.CheckErr("usersRmCmd", err)
|
||||
log.Println("user deleted successfully")
|
||||
}),
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package cmd
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -16,7 +18,7 @@ var usersUpdateCmd = &cobra.Command{
|
||||
Long: `Updates an existing user. Set the flags for the
|
||||
options you want to change.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
username, id := parseUsernameOrID(args[0])
|
||||
|
||||
var (
|
||||
@@ -25,14 +27,14 @@ options you want to change.`,
|
||||
)
|
||||
|
||||
if id != 0 {
|
||||
user, err = d.store.Users.Get("", id)
|
||||
user, err = store.Users.Get("", id)
|
||||
} else {
|
||||
user, err = d.store.Users.Get("", username)
|
||||
user, err = store.Users.Get("", username)
|
||||
}
|
||||
checkErr("d.store.Users.Get", err)
|
||||
utils.CheckErr("store.Users.Get", err)
|
||||
|
||||
err = d.store.Users.Update(user)
|
||||
checkErr("d.store.Users.Update", err)
|
||||
err = store.Users.Update(user)
|
||||
utils.CheckErr("store.Users.Update", err)
|
||||
printUsers([]*users.User{user})
|
||||
}, pythonConfig{}),
|
||||
}),
|
||||
}
|
||||
+12
-78
@@ -3,113 +3,42 @@ package cmd
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/storage/bolt"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
func checkErr(source string, err error) {
|
||||
if err != nil {
|
||||
log.Fatalf("%s: %v", source, err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustGetString(flags *pflag.FlagSet, flag string) string {
|
||||
s, err := flags.GetString(flag)
|
||||
checkErr("mustGetString", err)
|
||||
utils.CheckErr("mustGetString", err)
|
||||
return s
|
||||
}
|
||||
|
||||
func mustGetBool(flags *pflag.FlagSet, flag string) bool {
|
||||
b, err := flags.GetBool(flag)
|
||||
checkErr("mustGetBool", err)
|
||||
utils.CheckErr("mustGetBool", err)
|
||||
return b
|
||||
}
|
||||
|
||||
func mustGetUint(flags *pflag.FlagSet, flag string) uint {
|
||||
b, err := flags.GetUint(flag)
|
||||
checkErr("mustGetUint", err)
|
||||
utils.CheckErr("mustGetUint", err)
|
||||
return b
|
||||
}
|
||||
|
||||
func generateKey() []byte {
|
||||
k, err := settings.GenerateKey()
|
||||
checkErr("generateKey", err)
|
||||
return k
|
||||
}
|
||||
|
||||
type cobraFunc func(cmd *cobra.Command, args []string)
|
||||
type pythonFunc func(cmd *cobra.Command, args []string, data pythonData)
|
||||
|
||||
type pythonConfig struct {
|
||||
noDB bool
|
||||
allowNoDB bool
|
||||
}
|
||||
|
||||
type pythonData struct {
|
||||
hadDB bool
|
||||
store *storage.Storage
|
||||
}
|
||||
|
||||
func dbExists(path string) (bool, error) {
|
||||
stat, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return stat.Size() != 0, nil
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
d := filepath.Dir(path)
|
||||
_, err = os.Stat(d)
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet,gomnd
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
|
||||
return func(cmd *cobra.Command, args []string) {
|
||||
data := pythonData{hadDB: true}
|
||||
path := settings.Config.Server.Database
|
||||
exists, err := dbExists(path)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
} else if exists && cfg.noDB {
|
||||
log.Fatal(path + " already exists")
|
||||
} else if !exists && !cfg.noDB && !cfg.allowNoDB {
|
||||
log.Fatal(path + " does not exist. Please run 'filebrowser config init' first.")
|
||||
}
|
||||
|
||||
data.hadDB = exists
|
||||
db, err := storm.Open(path)
|
||||
checkErr(fmt.Sprintf("storm.Open path %v", path), err)
|
||||
|
||||
defer db.Close()
|
||||
data.store, err = bolt.NewStorage(db)
|
||||
checkErr("bolt.NewStorage", err)
|
||||
fn(cmd, args, data)
|
||||
}
|
||||
}
|
||||
type pythonFunc func(cmd *cobra.Command, args []string, store *storage.Storage)
|
||||
|
||||
func marshal(filename string, data interface{}) error {
|
||||
fd, err := os.Create(filename)
|
||||
|
||||
checkErr("os.Create", err)
|
||||
utils.CheckErr("os.Create", err)
|
||||
defer fd.Close()
|
||||
|
||||
switch ext := filepath.Ext(filename); ext {
|
||||
@@ -127,7 +56,7 @@ func marshal(filename string, data interface{}) error {
|
||||
|
||||
func unmarshal(filename string, data interface{}) error {
|
||||
fd, err := os.Open(filename)
|
||||
checkErr("os.Open", err)
|
||||
utils.CheckErr("os.Open", err)
|
||||
defer fd.Close()
|
||||
|
||||
switch ext := filepath.Ext(filename); ext {
|
||||
@@ -152,3 +81,8 @@ func jsonYamlArg(cmd *cobra.Command, args []string) error {
|
||||
return errors.New("invalid format: " + ext)
|
||||
}
|
||||
}
|
||||
|
||||
func cobraCmd(fn pythonFunc) cobraFunc {
|
||||
return func(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/version"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version number",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA)
|
||||
},
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func ParseSearch(value string) *SearchOptions {
|
||||
opts.LargerThan = updateSize(size)
|
||||
}
|
||||
if strings.HasPrefix(filter, "smallerThan=") {
|
||||
opts.Conditions["larger"] = true
|
||||
opts.Conditions["smaller"] = true
|
||||
size := strings.TrimPrefix(filter, "smallerThan=")
|
||||
opts.SmallerThan = updateSize(size)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Helper function to create error messages dynamically
|
||||
func errorMsg(extension, expectedType string, expectedMatch bool) string {
|
||||
matchStatus := "to match"
|
||||
if !expectedMatch {
|
||||
matchStatus = "to not match"
|
||||
}
|
||||
return fmt.Sprintf("Expected %s %s type '%s'", extension, matchStatus, expectedType)
|
||||
}
|
||||
|
||||
func TestIsMatchingType(t *testing.T) {
|
||||
// Test cases where IsMatchingType should return true
|
||||
trueTestCases := []struct {
|
||||
extension string
|
||||
expectedType string
|
||||
}{
|
||||
{".pdf", "pdf"},
|
||||
{".doc", "doc"},
|
||||
{".docx", "doc"},
|
||||
{".json", "text"},
|
||||
{".sh", "text"},
|
||||
{".zip", "archive"},
|
||||
{".rar", "archive"},
|
||||
}
|
||||
|
||||
for _, tc := range trueTestCases {
|
||||
assert.True(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, true))
|
||||
}
|
||||
|
||||
// Test cases where IsMatchingType should return false
|
||||
falseTestCases := []struct {
|
||||
extension string
|
||||
expectedType string
|
||||
}{
|
||||
{".mp4", "doc"},
|
||||
{".mp4", "text"},
|
||||
{".mp4", "archive"},
|
||||
}
|
||||
|
||||
for _, tc := range falseTestCases {
|
||||
assert.False(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, false))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSize(t *testing.T) {
|
||||
// Helper function for size error messages
|
||||
sizeErrorMsg := func(input string, expected, actual int) string {
|
||||
return fmt.Sprintf("Expected size for input '%s' to be %d, got %d", input, expected, actual)
|
||||
}
|
||||
|
||||
// Test cases for updateSize
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected int
|
||||
}{
|
||||
{"150", 150},
|
||||
{"invalid", 100},
|
||||
{"", 100},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
actual := updateSize(tc.input)
|
||||
assert.Equal(t, tc.expected, actual, sizeErrorMsg(tc.input, tc.expected, actual))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDoc(t *testing.T) {
|
||||
// Test cases where IsMatchingType should return true for document types
|
||||
docTrueTestCases := []struct {
|
||||
extension string
|
||||
expectedType string
|
||||
}{
|
||||
{".doc", "doc"},
|
||||
{".pdf", "doc"},
|
||||
}
|
||||
|
||||
for _, tc := range docTrueTestCases {
|
||||
assert.True(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, true))
|
||||
}
|
||||
|
||||
// Test case where IsMatchingType should return false for document types
|
||||
docFalseTestCases := []struct {
|
||||
extension string
|
||||
expectedType string
|
||||
}{
|
||||
{".mp4", "doc"},
|
||||
}
|
||||
|
||||
for _, tc := range docFalseTestCases {
|
||||
assert.False(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, false))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsText(t *testing.T) {
|
||||
// Test cases where IsMatchingType should return true for text types
|
||||
textTrueTestCases := []struct {
|
||||
extension string
|
||||
expectedType string
|
||||
}{
|
||||
{".json", "text"},
|
||||
{".sh", "text"},
|
||||
}
|
||||
|
||||
for _, tc := range textTrueTestCases {
|
||||
assert.True(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, true))
|
||||
}
|
||||
|
||||
// Test case where IsMatchingType should return false for text types
|
||||
textFalseTestCases := []struct {
|
||||
extension string
|
||||
expectedType string
|
||||
}{
|
||||
{".mp4", "text"},
|
||||
}
|
||||
|
||||
for _, tc := range textFalseTestCases {
|
||||
assert.False(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, false))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsArchive(t *testing.T) {
|
||||
// Test cases where IsMatchingType should return true for archive types
|
||||
archiveTrueTestCases := []struct {
|
||||
extension string
|
||||
expectedType string
|
||||
}{
|
||||
{".zip", "archive"},
|
||||
{".rar", "archive"},
|
||||
}
|
||||
|
||||
for _, tc := range archiveTrueTestCases {
|
||||
assert.True(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, true))
|
||||
}
|
||||
|
||||
// Test case where IsMatchingType should return false for archive types
|
||||
archiveFalseTestCases := []struct {
|
||||
extension string
|
||||
expectedType string
|
||||
}{
|
||||
{".mp4", "archive"},
|
||||
}
|
||||
|
||||
for _, tc := range archiveFalseTestCases {
|
||||
assert.False(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, false))
|
||||
}
|
||||
}
|
||||
+114
-113
@@ -1,8 +1,8 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"crypto/md5" //nolint:gosec
|
||||
"crypto/sha1" //nolint:gosec
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
@@ -21,37 +21,48 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/rules"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
var (
|
||||
bytesInMegabyte int64 = 1000000
|
||||
pathMutexes = make(map[string]*sync.Mutex)
|
||||
pathMutexesMu sync.Mutex // Mutex to protect the pathMutexes map
|
||||
pathMutexes = make(map[string]*sync.Mutex)
|
||||
pathMutexesMu sync.Mutex // Mutex to protect the pathMutexes map
|
||||
)
|
||||
|
||||
type ReducedItem struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
ModTime time.Time `json:"modified"`
|
||||
IsDir bool `json:"isDir,omitempty"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// FileInfo describes a file.
|
||||
// reduced item is non-recursive reduced "Items", used to pass flat items array
|
||||
type FileInfo struct {
|
||||
*Listing
|
||||
Path string `json:"path,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Extension string `json:"-"`
|
||||
ModTime time.Time `json:"modified"`
|
||||
CacheTime time.Time `json:"-"`
|
||||
Mode os.FileMode `json:"-"`
|
||||
IsDir bool `json:"isDir,omitempty"`
|
||||
IsSymlink bool `json:"isSymlink,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Subtitles []string `json:"subtitles,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Checksums map[string]string `json:"checksums,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Items []*FileInfo `json:"-"`
|
||||
ReducedItems []ReducedItem `json:"items,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Extension string `json:"-"`
|
||||
ModTime time.Time `json:"modified"`
|
||||
CacheTime time.Time `json:"-"`
|
||||
Mode os.FileMode `json:"-"`
|
||||
IsDir bool `json:"isDir,omitempty"`
|
||||
IsSymlink bool `json:"isSymlink,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Subtitles []string `json:"subtitles,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Checksums map[string]string `json:"checksums,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
NumDirs int `json:"numDirs"`
|
||||
NumFiles int `json:"numFiles"`
|
||||
}
|
||||
|
||||
// FileOptions are the options when getting a file info.
|
||||
type FileOptions struct {
|
||||
Path string // realpath
|
||||
IsDir bool
|
||||
Modify bool
|
||||
Expand bool
|
||||
ReadHeader bool
|
||||
@@ -60,30 +71,15 @@ type FileOptions struct {
|
||||
Content bool
|
||||
}
|
||||
|
||||
// Sorting constants
|
||||
const (
|
||||
SortingByName = "name"
|
||||
SortingBySize = "size"
|
||||
SortingByModified = "modified"
|
||||
)
|
||||
|
||||
// Listing is a collection of files.
|
||||
type Listing struct {
|
||||
Items []*FileInfo `json:"items"`
|
||||
Path string `json:"path"`
|
||||
NumDirs int `json:"numDirs"`
|
||||
NumFiles int `json:"numFiles"`
|
||||
Sorting users.Sorting `json:"sorting"`
|
||||
}
|
||||
|
||||
// NewFileInfo creates a File object from a path and a given user. This File
|
||||
// object will be automatically filled depending on if it is a directory
|
||||
// or a file. If it's a video file, it will also detect any subtitles.
|
||||
// Legacy file info method, only called on non-indexed directories.
|
||||
// Once indexing completes for the first time, NewFileInfo is never called.
|
||||
func NewFileInfo(opts FileOptions) (*FileInfo, error) {
|
||||
|
||||
index := GetIndex(rootPath)
|
||||
if !opts.Checker.Check(opts.Path) {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
file, err := stat(opts.Path, opts) // Pass opts.Path here
|
||||
file, err := stat(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -92,6 +88,26 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
|
||||
if err = file.readListing(opts.Path, opts.Checker, opts.ReadHeader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cleanedItems := []ReducedItem{}
|
||||
for _, item := range file.Items {
|
||||
// This is particularly useful for root of index, while indexing hasn't finished.
|
||||
// adds the directory sizes for directories that have been indexed already.
|
||||
if item.IsDir {
|
||||
adjustedPath := index.makeIndexPath(opts.Path+"/"+item.Name, true)
|
||||
info, _ := index.GetMetadataInfo(adjustedPath)
|
||||
item.Size = info.Size
|
||||
}
|
||||
cleanedItems = append(cleanedItems, ReducedItem{
|
||||
Name: item.Name,
|
||||
Size: item.Size,
|
||||
IsDir: item.IsDir,
|
||||
ModTime: item.ModTime,
|
||||
Type: item.Type,
|
||||
})
|
||||
}
|
||||
|
||||
file.Items = nil
|
||||
file.ReducedItems = cleanedItems
|
||||
return file, nil
|
||||
}
|
||||
err = file.detectType(opts.Path, opts.Modify, opts.Content, true)
|
||||
@@ -111,71 +127,64 @@ func FileInfoFaster(opts FileOptions) (*FileInfo, error) {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
index := GetIndex(rootPath)
|
||||
trimmed := strings.TrimPrefix(opts.Path, "/")
|
||||
if trimmed == "" {
|
||||
trimmed = "/"
|
||||
adjustedPath := index.makeIndexPath(opts.Path, opts.IsDir)
|
||||
if opts.IsDir {
|
||||
info, exists := index.GetMetadataInfo(adjustedPath)
|
||||
if exists && !opts.Content {
|
||||
// Let's not refresh if less than a second has passed
|
||||
if time.Since(info.CacheTime) > time.Second {
|
||||
go RefreshFileInfo(opts) //nolint:errcheck
|
||||
}
|
||||
// refresh cache after
|
||||
return &info, nil
|
||||
}
|
||||
}
|
||||
adjustedPath := makeIndexPath(trimmed, index.Root)
|
||||
var info FileInfo
|
||||
info, exists := index.GetMetadataInfo(adjustedPath)
|
||||
if exists && !opts.Content {
|
||||
// Check if the cache time is less than 1 second
|
||||
if time.Since(info.CacheTime) > time.Second {
|
||||
go RefreshFileInfo(opts)
|
||||
}
|
||||
// refresh cache after
|
||||
return &info, nil
|
||||
} else {
|
||||
// don't bother caching content
|
||||
if opts.Content {
|
||||
file, err := NewFileInfo(opts)
|
||||
return file, err
|
||||
}
|
||||
updated := RefreshFileInfo(opts)
|
||||
if !updated {
|
||||
file, err := NewFileInfo(opts)
|
||||
return file, err
|
||||
}
|
||||
info, exists = index.GetMetadataInfo(adjustedPath)
|
||||
if !exists || info.Name == "" {
|
||||
return &FileInfo{}, errors.ErrEmptyKey
|
||||
}
|
||||
return &info, nil
|
||||
// don't bother caching content
|
||||
if opts.Content {
|
||||
file, err := NewFileInfo(opts)
|
||||
return file, err
|
||||
}
|
||||
err := RefreshFileInfo(opts)
|
||||
if err != nil {
|
||||
file, err := NewFileInfo(opts)
|
||||
return file, err
|
||||
}
|
||||
info, exists := index.GetMetadataInfo(adjustedPath + "/" + filepath.Base(opts.Path))
|
||||
if !exists || info.Name == "" {
|
||||
return NewFileInfo(opts)
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func RefreshFileInfo(opts FileOptions) bool {
|
||||
func RefreshFileInfo(opts FileOptions) error {
|
||||
if !opts.Checker.Check(opts.Path) {
|
||||
return false
|
||||
return fmt.Errorf("permission denied: %s", opts.Path)
|
||||
}
|
||||
index := GetIndex(rootPath)
|
||||
trimmed := strings.TrimPrefix(opts.Path, "/")
|
||||
if trimmed == "" {
|
||||
trimmed = "/"
|
||||
}
|
||||
adjustedPath := makeIndexPath(trimmed, index.Root)
|
||||
file, err := stat(opts.Path, opts) // Pass opts.Path here
|
||||
adjustedPath := index.makeIndexPath(opts.Path, opts.IsDir)
|
||||
file, err := stat(opts)
|
||||
if err != nil {
|
||||
return false
|
||||
return fmt.Errorf("File/folder does not exist to refresh data: %s", opts.Path)
|
||||
}
|
||||
_ = file.detectType(adjustedPath, true, opts.Content, opts.ReadHeader)
|
||||
_ = file.detectType(opts.Path, true, opts.Content, opts.ReadHeader)
|
||||
if file.IsDir {
|
||||
err := file.readListing(opts.Path, opts.Checker, opts.ReadHeader)
|
||||
if err != nil {
|
||||
return false
|
||||
return fmt.Errorf("Dir info could not be read: %s", opts.Path)
|
||||
}
|
||||
return index.UpdateFileMetadata(adjustedPath, *file)
|
||||
} else {
|
||||
return index.UpdateFileMetadata(adjustedPath, *file)
|
||||
}
|
||||
result := index.UpdateFileMetadata(adjustedPath, *file)
|
||||
if !result {
|
||||
return fmt.Errorf("File/folder does not exist in metadata: %s", adjustedPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stat(path string, opts FileOptions) (*FileInfo, error) {
|
||||
info, err := os.Lstat(path)
|
||||
func stat(opts FileOptions) (*FileInfo, error) {
|
||||
info, err := os.Lstat(opts.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file := &FileInfo{
|
||||
Path: opts.Path,
|
||||
Name: info.Name(),
|
||||
@@ -185,13 +194,12 @@ func stat(path string, opts FileOptions) (*FileInfo, error) {
|
||||
Extension: filepath.Ext(info.Name()),
|
||||
Token: opts.Token,
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
file.IsDir = true
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
file.IsSymlink = true
|
||||
targetInfo, err := os.Stat(path)
|
||||
targetInfo, err := os.Stat(opts.Path)
|
||||
if err == nil {
|
||||
file.Size = targetInfo.Size()
|
||||
file.IsDir = targetInfo.IsDir()
|
||||
@@ -248,20 +256,19 @@ func (i *FileInfo) RealPath() string {
|
||||
return i.Path
|
||||
}
|
||||
|
||||
func GetRealPath(relativePath ...string) (string, error) {
|
||||
func GetRealPath(relativePath ...string) (string, bool, error) {
|
||||
combined := []string{settings.Config.Server.Root}
|
||||
for _, path := range relativePath {
|
||||
combined = append(combined, strings.TrimPrefix(path, settings.Config.Server.Root))
|
||||
}
|
||||
joinedPath := filepath.Join(combined...)
|
||||
|
||||
// Convert relative path to absolute path
|
||||
absolutePath, err := filepath.Abs(joinedPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", false, err
|
||||
}
|
||||
if !Exists(absolutePath) {
|
||||
return absolutePath, nil // return without error
|
||||
return absolutePath, false, nil // return without error
|
||||
}
|
||||
// Resolve symlinks and get the real path
|
||||
return resolveSymlinks(absolutePath)
|
||||
@@ -272,10 +279,9 @@ func DeleteFiles(absPath string, opts FileOptions) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parentDir := filepath.Dir(absPath)
|
||||
opts.Path = parentDir
|
||||
updated := RefreshFileInfo(opts)
|
||||
if !updated {
|
||||
opts.Path = filepath.Dir(absPath)
|
||||
err = RefreshFileInfo(opts)
|
||||
if err != nil {
|
||||
return errors.ErrEmptyKey
|
||||
}
|
||||
return nil
|
||||
@@ -288,16 +294,14 @@ func WriteDirectory(opts FileOptions) error {
|
||||
return err
|
||||
}
|
||||
opts.Path = filepath.Dir(opts.Path)
|
||||
updated := RefreshFileInfo(opts)
|
||||
if !updated {
|
||||
err = RefreshFileInfo(opts)
|
||||
if err != nil {
|
||||
return errors.ErrEmptyKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteFile(opts FileOptions, in io.Reader) error {
|
||||
fmt.Println("writing file", opts.Path)
|
||||
dst := opts.Path
|
||||
parentDir := filepath.Dir(dst)
|
||||
// Split the directory from the destination path
|
||||
@@ -321,23 +325,21 @@ func WriteFile(opts FileOptions, in io.Reader) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("refreshing info for ", parentDir)
|
||||
opts.Path = parentDir
|
||||
updated := RefreshFileInfo(opts)
|
||||
if !updated {
|
||||
err = RefreshFileInfo(opts)
|
||||
if err != nil {
|
||||
return errors.ErrEmptyKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveSymlinks resolves symlinks in the given path
|
||||
func resolveSymlinks(path string) (string, error) {
|
||||
func resolveSymlinks(path string) (string, bool, error) {
|
||||
for {
|
||||
// Get the file info
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
// Check if it's a symlink
|
||||
@@ -345,14 +347,14 @@ func resolveSymlinks(path string) (string, error) {
|
||||
// Read the symlink target
|
||||
target, err := os.Readlink(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
// Resolve the target relative to the symlink's directory
|
||||
path = filepath.Join(filepath.Dir(path), target)
|
||||
} else {
|
||||
// Not a symlink, so we are done
|
||||
return path, nil
|
||||
// Not a symlink, so return the resolved path and check if it's a directory
|
||||
return path, info.IsDir(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -504,9 +506,8 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
|
||||
return err
|
||||
}
|
||||
|
||||
listing := &Listing{
|
||||
listing := &FileInfo{
|
||||
Items: []*FileInfo{},
|
||||
Path: i.Path,
|
||||
NumDirs: 0,
|
||||
NumFiles: 0,
|
||||
}
|
||||
@@ -561,7 +562,7 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
|
||||
listing.Items = append(listing.Items, file)
|
||||
}
|
||||
|
||||
i.Listing = listing
|
||||
i.Items = listing.Items
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_GetRealPath(t *testing.T) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
trimPrefix := filepath.Dir(filepath.Dir(cwd)) + "/"
|
||||
tests := []struct {
|
||||
name string
|
||||
paths []string
|
||||
want struct {
|
||||
path string
|
||||
isDir bool
|
||||
}
|
||||
}{
|
||||
{
|
||||
name: "current directory",
|
||||
paths: []string{
|
||||
"./",
|
||||
},
|
||||
want: struct {
|
||||
path string
|
||||
isDir bool
|
||||
}{
|
||||
path: "backend/files",
|
||||
isDir: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "current directory",
|
||||
paths: []string{
|
||||
"./file.go",
|
||||
},
|
||||
want: struct {
|
||||
path string
|
||||
isDir bool
|
||||
}{
|
||||
path: "backend/files/file.go",
|
||||
isDir: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "other test case",
|
||||
paths: []string{
|
||||
"/mnt/doesnt/exist",
|
||||
},
|
||||
want: struct {
|
||||
path string
|
||||
isDir bool
|
||||
}{
|
||||
path: "/mnt/doesnt/exist",
|
||||
isDir: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
realPath, isDir, err := GetRealPath(tt.paths...)
|
||||
adjustedRealPath := strings.TrimPrefix(realPath, trimPrefix)
|
||||
if tt.want.path != adjustedRealPath || tt.want.isDir != isDir {
|
||||
t.Errorf("expected %v:%v but got: %v:%v", tt.want.path, tt.want.isDir, adjustedRealPath, isDir)
|
||||
}
|
||||
if err != nil {
|
||||
t.Error("got error", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+101
-70
@@ -1,9 +1,9 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -11,22 +11,12 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
)
|
||||
|
||||
type Directory struct {
|
||||
Metadata map[string]FileInfo
|
||||
Files string
|
||||
}
|
||||
type File struct {
|
||||
Name string
|
||||
IsDir bool
|
||||
}
|
||||
|
||||
type Index struct {
|
||||
Root string
|
||||
Directories map[string]Directory
|
||||
Directories map[string]FileInfo
|
||||
NumDirs int
|
||||
NumFiles int
|
||||
inProgress bool
|
||||
quickList []File
|
||||
LastIndexed time.Time
|
||||
mu sync.RWMutex
|
||||
}
|
||||
@@ -48,16 +38,12 @@ func indexingScheduler(intervalMinutes uint32) {
|
||||
rootPath = settings.Config.Server.Root
|
||||
}
|
||||
si := GetIndex(rootPath)
|
||||
log.Printf("Indexing Files...")
|
||||
log.Printf("Configured to run every %v minutes", intervalMinutes)
|
||||
log.Printf("Indexing from root: %s", si.Root)
|
||||
for {
|
||||
startTime := time.Now()
|
||||
// Set the indexing flag to indicate that indexing is in progress
|
||||
si.resetCount()
|
||||
// Perform the indexing operation
|
||||
err := si.indexFiles(si.Root)
|
||||
si.quickList = []File{}
|
||||
// Reset the indexing flag to indicate that indexing has finished
|
||||
si.inProgress = false
|
||||
// Update the LastIndexed time
|
||||
@@ -79,89 +65,134 @@ func indexingScheduler(intervalMinutes uint32) {
|
||||
|
||||
// Define a function to recursively index files and directories
|
||||
func (si *Index) indexFiles(path string) error {
|
||||
// Check if the current directory has been modified since the last indexing
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
adjustedPath := makeIndexPath(path, si.Root)
|
||||
// Ensure path is cleaned and normalized
|
||||
adjustedPath := si.makeIndexPath(path, true)
|
||||
|
||||
// Open the directory
|
||||
dir, err := os.Open(path)
|
||||
if err != nil {
|
||||
// Directory must have been deleted, remove it from the index
|
||||
// If the directory can't be opened (e.g., deleted), remove it from the index
|
||||
si.RemoveDirectory(adjustedPath)
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
dirInfo, err := dir.Stat()
|
||||
if err != nil {
|
||||
dir.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
// Compare the last modified time of the directory with the last indexed time
|
||||
lastIndexed := si.LastIndexed
|
||||
if dirInfo.ModTime().Before(lastIndexed) {
|
||||
dir.Close()
|
||||
// Check if the directory is already up-to-date
|
||||
if dirInfo.ModTime().Before(si.LastIndexed) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read the directory contents
|
||||
// Read directory contents
|
||||
files, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir.Close()
|
||||
si.UpdateQuickList(files)
|
||||
si.InsertFiles(path)
|
||||
// done separately for memory efficiency on recursion
|
||||
si.InsertDirs(path)
|
||||
|
||||
// Recursively process files and directories
|
||||
fileInfos := []*FileInfo{}
|
||||
var totalSize int64
|
||||
var numDirs, numFiles int
|
||||
|
||||
for _, file := range files {
|
||||
parentInfo := &FileInfo{
|
||||
Name: file.Name(),
|
||||
Size: file.Size(),
|
||||
ModTime: file.ModTime(),
|
||||
IsDir: file.IsDir(),
|
||||
}
|
||||
childInfo, err := si.InsertInfo(path, parentInfo)
|
||||
if err != nil {
|
||||
// Log error, but continue processing other files
|
||||
continue
|
||||
}
|
||||
|
||||
// Accumulate directory size and items
|
||||
totalSize += childInfo.Size
|
||||
if childInfo.IsDir {
|
||||
numDirs++
|
||||
} else {
|
||||
numFiles++
|
||||
}
|
||||
_ = childInfo.detectType(path, true, false, false)
|
||||
fileInfos = append(fileInfos, childInfo)
|
||||
}
|
||||
|
||||
// Create FileInfo for the current directory
|
||||
dirFileInfo := &FileInfo{
|
||||
Items: fileInfos,
|
||||
Name: filepath.Base(path),
|
||||
Size: totalSize,
|
||||
ModTime: dirInfo.ModTime(),
|
||||
CacheTime: time.Now(),
|
||||
IsDir: true,
|
||||
NumDirs: numDirs,
|
||||
NumFiles: numFiles,
|
||||
}
|
||||
|
||||
// Add directory to index
|
||||
si.mu.Lock()
|
||||
si.Directories[adjustedPath] = *dirFileInfo
|
||||
si.NumDirs += numDirs
|
||||
si.NumFiles += numFiles
|
||||
si.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (si *Index) InsertFiles(path string) {
|
||||
adjustedPath := makeIndexPath(path, si.Root)
|
||||
subDirectory := Directory{}
|
||||
buffer := bytes.Buffer{}
|
||||
// InsertInfo function to handle adding a file or directory into the index
|
||||
func (si *Index) InsertInfo(parentPath string, file *FileInfo) (*FileInfo, error) {
|
||||
filePath := filepath.Join(parentPath, file.Name)
|
||||
|
||||
for _, f := range si.GetQuickList() {
|
||||
if !f.IsDir {
|
||||
buffer.WriteString(f.Name + ";")
|
||||
si.UpdateCount("files")
|
||||
// Check if it's a directory and recursively index it
|
||||
if file.IsDir {
|
||||
// Recursively index directory
|
||||
err := si.indexFiles(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return directory info from the index
|
||||
adjustedPath := si.makeIndexPath(filePath, true)
|
||||
si.mu.RLock()
|
||||
dirInfo := si.Directories[adjustedPath]
|
||||
si.mu.RUnlock()
|
||||
return &dirInfo, nil
|
||||
}
|
||||
// Use GetMetadataInfo and SetFileMetadata for safer read and write operations
|
||||
subDirectory.Files = buffer.String()
|
||||
si.SetDirectoryInfo(adjustedPath, subDirectory)
|
||||
|
||||
// Create FileInfo for regular files
|
||||
fileInfo := &FileInfo{
|
||||
Path: filePath,
|
||||
Name: file.Name,
|
||||
Size: file.Size,
|
||||
ModTime: file.ModTime,
|
||||
IsDir: false,
|
||||
}
|
||||
|
||||
return fileInfo, nil
|
||||
}
|
||||
|
||||
func (si *Index) InsertDirs(path string) {
|
||||
adjustedPath := makeIndexPath(path, si.Root)
|
||||
for _, f := range si.GetQuickList() {
|
||||
if f.IsDir {
|
||||
if _, exists := si.Directories[adjustedPath]; exists {
|
||||
si.UpdateCount("dirs")
|
||||
// Add or update the directory in the map
|
||||
if adjustedPath == "/" {
|
||||
si.SetDirectoryInfo("/"+f.Name, Directory{})
|
||||
} else {
|
||||
si.SetDirectoryInfo(adjustedPath+"/"+f.Name, Directory{})
|
||||
}
|
||||
}
|
||||
err := si.indexFiles(path + "/" + f.Name)
|
||||
if err != nil {
|
||||
if err.Error() == "invalid argument" {
|
||||
log.Printf("Could not index \"%v\": %v \n", path, "Permission Denied")
|
||||
} else {
|
||||
log.Printf("Could not index \"%v\": %v \n", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeIndexPath(path string, root string) string {
|
||||
if path == root {
|
||||
func (si *Index) makeIndexPath(subPath string, isDir bool) string {
|
||||
if si.Root == subPath {
|
||||
return "/"
|
||||
}
|
||||
adjustedPath := strings.TrimPrefix(path, root+"/")
|
||||
// clean path
|
||||
subPath = strings.TrimSuffix(subPath, "/")
|
||||
// remove index prefix
|
||||
adjustedPath := strings.TrimPrefix(subPath, si.Root)
|
||||
// remove trailing slash
|
||||
adjustedPath = strings.TrimSuffix(adjustedPath, "/")
|
||||
// add leading slash for root of index
|
||||
if adjustedPath == "" {
|
||||
adjustedPath = "/"
|
||||
} else if !isDir {
|
||||
adjustedPath = filepath.Dir(adjustedPath)
|
||||
}
|
||||
if !strings.HasPrefix(adjustedPath, "/") {
|
||||
adjustedPath = "/" + adjustedPath
|
||||
}
|
||||
return adjustedPath
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package files
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
@@ -23,18 +24,26 @@ func BenchmarkFillIndex(b *testing.B) {
|
||||
func (si *Index) createMockData(numDirs, numFilesPerDir int) {
|
||||
for i := 0; i < numDirs; i++ {
|
||||
dirName := generateRandomPath(rand.Intn(3) + 1)
|
||||
files := []File{}
|
||||
// Append a new Directory to the slice
|
||||
files := []*FileInfo{} // Slice of FileInfo
|
||||
|
||||
// Simulating files and directories with FileInfo
|
||||
for j := 0; j < numFilesPerDir; j++ {
|
||||
newFile := File{
|
||||
Name: "file-" + getRandomTerm() + getRandomExtension(),
|
||||
IsDir: false,
|
||||
newFile := &FileInfo{
|
||||
Name: "file-" + getRandomTerm() + getRandomExtension(),
|
||||
IsDir: false,
|
||||
Size: rand.Int63n(1000), // Random size
|
||||
ModTime: time.Now().Add(-time.Duration(rand.Intn(100)) * time.Hour), // Random mod time
|
||||
}
|
||||
files = append(files, newFile)
|
||||
}
|
||||
si.UpdateQuickListForTests(files)
|
||||
si.InsertFiles(dirName)
|
||||
si.InsertDirs(dirName)
|
||||
|
||||
// Simulate inserting files into index
|
||||
for _, file := range files {
|
||||
_, err := si.InsertInfo(dirName, file)
|
||||
if err != nil {
|
||||
fmt.Println("Error inserting file:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+88
-57
@@ -2,7 +2,6 @@ package files
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -30,12 +29,17 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
|
||||
continue
|
||||
}
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
for dirName, dir := range si.Directories {
|
||||
isDir := true
|
||||
files := strings.Split(dir.Files, ";")
|
||||
files := []string{}
|
||||
for _, item := range dir.Items {
|
||||
if !item.IsDir {
|
||||
files = append(files, item.Name)
|
||||
}
|
||||
}
|
||||
value, found := sessionInProgress.Load(sourceSession)
|
||||
if !found || value != runningHash {
|
||||
si.mu.Unlock()
|
||||
return []string{}, map[string]map[string]bool{}
|
||||
}
|
||||
if count > maxSearchResults {
|
||||
@@ -46,7 +50,9 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
|
||||
continue // path not matched
|
||||
}
|
||||
fileTypes := map[string]bool{}
|
||||
matches, fileType := containsSearchTerm(dirName, searchTerm, *searchOptions, isDir, fileTypes)
|
||||
si.mu.Unlock()
|
||||
matches, fileType := si.containsSearchTerm(dirName, searchTerm, *searchOptions, isDir, fileTypes)
|
||||
si.mu.Lock()
|
||||
if matches {
|
||||
fileListTypes[pathName] = fileType
|
||||
matching = append(matching, pathName)
|
||||
@@ -67,8 +73,9 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
|
||||
}
|
||||
fullName := strings.TrimLeft(pathName+file, "/")
|
||||
fileTypes := map[string]bool{}
|
||||
|
||||
matches, fileType := containsSearchTerm(fullName, searchTerm, *searchOptions, isDir, fileTypes)
|
||||
si.mu.Unlock()
|
||||
matches, fileType := si.containsSearchTerm(fullName, searchTerm, *searchOptions, isDir, fileTypes)
|
||||
si.mu.Lock()
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
@@ -77,6 +84,7 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
|
||||
count++
|
||||
}
|
||||
}
|
||||
si.mu.Unlock()
|
||||
}
|
||||
// Sort the strings based on the number of elements after splitting by "/"
|
||||
sort.Slice(matching, func(i, j int) bool {
|
||||
@@ -102,65 +110,88 @@ func scopedPathNameFilter(pathName string, scope string, isDir bool) string {
|
||||
return pathName
|
||||
}
|
||||
|
||||
func containsSearchTerm(pathName string, searchTerm string, options SearchOptions, isDir bool, fileTypes map[string]bool) (bool, map[string]bool) {
|
||||
func (si *Index) containsSearchTerm(pathName string, searchTerm string, options SearchOptions, isDir bool, fileTypes map[string]bool) (bool, map[string]bool) {
|
||||
largerThan := int64(options.LargerThan) * 1024 * 1024
|
||||
smallerThan := int64(options.SmallerThan) * 1024 * 1024
|
||||
conditions := options.Conditions
|
||||
path := getLastPathComponent(pathName)
|
||||
// Convert to lowercase once
|
||||
fileName := filepath.Base(pathName)
|
||||
adjustedPath := si.makeIndexPath(pathName, isDir)
|
||||
|
||||
// Convert to lowercase if not exact match
|
||||
if !conditions["exact"] {
|
||||
path = strings.ToLower(path)
|
||||
fileName = strings.ToLower(fileName)
|
||||
searchTerm = strings.ToLower(searchTerm)
|
||||
}
|
||||
if strings.Contains(path, searchTerm) {
|
||||
// Calculate fileSize only if needed
|
||||
var fileSize int64
|
||||
matchesAllConditions := true
|
||||
extension := filepath.Ext(path)
|
||||
for _, k := range AllFiletypeOptions {
|
||||
if IsMatchingType(extension, k) {
|
||||
fileTypes[k] = true
|
||||
|
||||
// Check if the file name contains the search term
|
||||
if !strings.Contains(fileName, searchTerm) {
|
||||
return false, map[string]bool{}
|
||||
}
|
||||
|
||||
// Initialize file size and fileTypes map
|
||||
var fileSize int64
|
||||
extension := filepath.Ext(fileName)
|
||||
|
||||
// Collect file types
|
||||
for _, k := range AllFiletypeOptions {
|
||||
if IsMatchingType(extension, k) {
|
||||
fileTypes[k] = true
|
||||
}
|
||||
}
|
||||
fileTypes["dir"] = isDir
|
||||
// Get file info if needed for size-related conditions
|
||||
if largerThan > 0 || smallerThan > 0 {
|
||||
fileInfo, exists := si.GetMetadataInfo(adjustedPath)
|
||||
if !exists {
|
||||
return false, fileTypes
|
||||
} else if !isDir {
|
||||
// Look for specific file in ReducedItems
|
||||
for _, item := range fileInfo.ReducedItems {
|
||||
lower := strings.ToLower(item.Name)
|
||||
if strings.Contains(lower, searchTerm) {
|
||||
if item.Size == 0 {
|
||||
return false, fileTypes
|
||||
}
|
||||
fileSize = item.Size
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fileSize = fileInfo.Size
|
||||
}
|
||||
if fileSize == 0 {
|
||||
return false, fileTypes
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate all conditions
|
||||
for t, v := range conditions {
|
||||
if t == "exact" {
|
||||
continue
|
||||
}
|
||||
switch t {
|
||||
case "larger":
|
||||
if largerThan > 0 {
|
||||
if fileSize <= largerThan {
|
||||
return false, fileTypes
|
||||
}
|
||||
}
|
||||
case "smaller":
|
||||
if smallerThan > 0 {
|
||||
if fileSize >= smallerThan {
|
||||
return false, fileTypes
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Handle other file type conditions
|
||||
notMatchType := v != fileTypes[t]
|
||||
if notMatchType {
|
||||
return false, fileTypes
|
||||
}
|
||||
}
|
||||
fileTypes["dir"] = isDir
|
||||
for t, v := range conditions {
|
||||
if t == "exact" {
|
||||
continue
|
||||
}
|
||||
var matchesCondition bool
|
||||
switch t {
|
||||
case "larger":
|
||||
if fileSize == 0 {
|
||||
fileSize = getFileSize(pathName)
|
||||
}
|
||||
matchesCondition = fileSize > int64(options.LargerThan)*bytesInMegabyte
|
||||
case "smaller":
|
||||
if fileSize == 0 {
|
||||
fileSize = getFileSize(pathName)
|
||||
}
|
||||
matchesCondition = fileSize < int64(options.SmallerThan)*bytesInMegabyte
|
||||
default:
|
||||
matchesCondition = v == fileTypes[t]
|
||||
}
|
||||
if !matchesCondition {
|
||||
matchesAllConditions = false
|
||||
}
|
||||
}
|
||||
return matchesAllConditions, fileTypes
|
||||
}
|
||||
// Clear variables and return
|
||||
return false, map[string]bool{}
|
||||
}
|
||||
|
||||
func getFileSize(filepath string) int64 {
|
||||
fileInfo, err := os.Stat(rootPath + "/" + filepath)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return fileInfo.Size()
|
||||
}
|
||||
|
||||
func getLastPathComponent(path string) string {
|
||||
// Use filepath.Base to extract the last component of the path
|
||||
return filepath.Base(path)
|
||||
return true, fileTypes
|
||||
}
|
||||
|
||||
func generateRandomHash(length int) string {
|
||||
|
||||
+96
-168
@@ -11,7 +11,7 @@ func BenchmarkSearchAllIndexes(b *testing.B) {
|
||||
InitializeIndex(5, false)
|
||||
si := GetIndex(rootPath)
|
||||
|
||||
si.createMockData(50, 3) // 1000 dirs, 3 files per dir
|
||||
si.createMockData(50, 3) // 50 dirs, 3 files per dir
|
||||
|
||||
// Generate 100 random search terms
|
||||
searchTerms := generateRandomSearchTerms(100)
|
||||
@@ -26,87 +26,90 @@ func BenchmarkSearchAllIndexes(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
// loop over test files and compare output
|
||||
func TestParseSearch(t *testing.T) {
|
||||
value := ParseSearch("my test search")
|
||||
want := &SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
"exact": false,
|
||||
tests := []struct {
|
||||
input string
|
||||
want *SearchOptions
|
||||
}{
|
||||
{
|
||||
input: "my test search",
|
||||
want: &SearchOptions{
|
||||
Conditions: map[string]bool{"exact": false},
|
||||
Terms: []string{"my test search"},
|
||||
},
|
||||
},
|
||||
Terms: []string{"my test search"},
|
||||
}
|
||||
if !reflect.DeepEqual(value, want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||
}
|
||||
value = ParseSearch("case:exact my|test|search")
|
||||
want = &SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
"exact": true,
|
||||
{
|
||||
input: "case:exact my|test|search",
|
||||
want: &SearchOptions{
|
||||
Conditions: map[string]bool{"exact": true},
|
||||
Terms: []string{"my", "test", "search"},
|
||||
},
|
||||
},
|
||||
Terms: []string{"my", "test", "search"},
|
||||
}
|
||||
if !reflect.DeepEqual(value, want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||
}
|
||||
value = ParseSearch("type:largerThan=100 type:smallerThan=1000 test")
|
||||
want = &SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
"exact": false,
|
||||
"larger": true,
|
||||
{
|
||||
input: "type:largerThan=100 type:smallerThan=1000 test",
|
||||
want: &SearchOptions{
|
||||
Conditions: map[string]bool{"exact": false, "larger": true, "smaller": true},
|
||||
Terms: []string{"test"},
|
||||
LargerThan: 100,
|
||||
SmallerThan: 1000,
|
||||
},
|
||||
},
|
||||
Terms: []string{"test"},
|
||||
LargerThan: 100,
|
||||
SmallerThan: 1000,
|
||||
}
|
||||
if !reflect.DeepEqual(value, want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||
}
|
||||
value = ParseSearch("type:audio thisfile")
|
||||
want = &SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
"exact": false,
|
||||
"audio": true,
|
||||
{
|
||||
input: "type:audio thisfile",
|
||||
want: &SearchOptions{
|
||||
Conditions: map[string]bool{"exact": false, "audio": true},
|
||||
Terms: []string{"thisfile"},
|
||||
},
|
||||
},
|
||||
Terms: []string{"thisfile"},
|
||||
}
|
||||
if !reflect.DeepEqual(value, want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
value := ParseSearch(tt.input)
|
||||
if !reflect.DeepEqual(value, tt.want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchWhileIndexing(t *testing.T) {
|
||||
InitializeIndex(5, false)
|
||||
si := GetIndex(rootPath)
|
||||
// Generate 100 random search terms
|
||||
// Generate 100 random search terms
|
||||
|
||||
searchTerms := generateRandomSearchTerms(10)
|
||||
for i := 0; i < 5; i++ {
|
||||
// Execute the SearchAllIndexes function
|
||||
go si.createMockData(100, 100) // 1000 dirs, 3 files per dir
|
||||
go si.createMockData(100, 100) // Creating mock data concurrently
|
||||
for _, term := range searchTerms {
|
||||
go si.Search(term, "/", "test")
|
||||
go si.Search(term, "/", "test") // Search concurrently
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchIndexes(t *testing.T) {
|
||||
index := Index{
|
||||
Directories: map[string]Directory{
|
||||
"test": {
|
||||
Files: "audio1.wav;",
|
||||
},
|
||||
"test/path": {
|
||||
Files: "file.txt;",
|
||||
},
|
||||
"new": {},
|
||||
"new/test": {
|
||||
Files: "audio.wav;video.mp4;video.MP4;",
|
||||
},
|
||||
"new/test/path": {
|
||||
Files: "archive.zip;",
|
||||
Directories: map[string]FileInfo{
|
||||
"test": {Items: []*FileInfo{{Name: "audio1.wav"}}},
|
||||
"test/path": {Items: []*FileInfo{{Name: "file.txt"}}},
|
||||
"new/test": {Items: []*FileInfo{
|
||||
{Name: "audio.wav"},
|
||||
{Name: "video.mp4"},
|
||||
{Name: "video.MP4"},
|
||||
}},
|
||||
"new/test/path": {Items: []*FileInfo{{Name: "archive.zip"}}},
|
||||
"/firstDir": {Items: []*FileInfo{
|
||||
{Name: "archive.zip", Size: 100},
|
||||
{Name: "thisIsDir", IsDir: true, Size: 2 * 1024 * 1024},
|
||||
}},
|
||||
"/firstDir/thisIsDir": {
|
||||
Items: []*FileInfo{
|
||||
{Name: "hi.txt"},
|
||||
},
|
||||
Size: 2 * 1024 * 1024,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
search string
|
||||
scope string
|
||||
@@ -118,7 +121,7 @@ func TestSearchIndexes(t *testing.T) {
|
||||
scope: "/new/",
|
||||
expectedResult: []string{"test/audio.wav"},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"test/audio.wav": map[string]bool{"audio": true, "dir": false},
|
||||
"test/audio.wav": {"audio": true, "dir": false},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -126,16 +129,41 @@ func TestSearchIndexes(t *testing.T) {
|
||||
scope: "/",
|
||||
expectedResult: []string{"test/", "new/test/"},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"test/": map[string]bool{"dir": true},
|
||||
"new/test/": map[string]bool{"dir": true},
|
||||
"test/": {"dir": true},
|
||||
"new/test/": {"dir": true},
|
||||
},
|
||||
},
|
||||
{
|
||||
search: "archive",
|
||||
scope: "/",
|
||||
expectedResult: []string{"new/test/path/archive.zip"},
|
||||
expectedResult: []string{"firstDir/archive.zip", "new/test/path/archive.zip"},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"new/test/path/archive.zip": map[string]bool{"archive": true, "dir": false},
|
||||
"new/test/path/archive.zip": {"archive": true, "dir": false},
|
||||
"firstDir/archive.zip": {"archive": true, "dir": false},
|
||||
},
|
||||
},
|
||||
{
|
||||
search: "arch",
|
||||
scope: "/firstDir",
|
||||
expectedResult: []string{"archive.zip"},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"archive.zip": {"archive": true, "dir": false},
|
||||
},
|
||||
},
|
||||
{
|
||||
search: "isdir",
|
||||
scope: "/",
|
||||
expectedResult: []string{"firstDir/thisIsDir/"},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"firstDir/thisIsDir/": {"dir": true},
|
||||
},
|
||||
},
|
||||
{
|
||||
search: "dir type:largerThan=1",
|
||||
scope: "/",
|
||||
expectedResult: []string{"firstDir/thisIsDir/"},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"firstDir/thisIsDir/": {"dir": true},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -146,18 +174,17 @@ func TestSearchIndexes(t *testing.T) {
|
||||
"new/test/video.MP4",
|
||||
},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"new/test/video.MP4": map[string]bool{"video": true, "dir": false},
|
||||
"new/test/video.mp4": map[string]bool{"video": true, "dir": false},
|
||||
"new/test/video.MP4": {"video": true, "dir": false},
|
||||
"new/test/video.mp4": {"video": true, "dir": false},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.search, func(t *testing.T) {
|
||||
actualResult, actualTypes := index.Search(tt.search, tt.scope, "")
|
||||
assert.Equal(t, tt.expectedResult, actualResult)
|
||||
if !reflect.DeepEqual(tt.expectedTypes, actualTypes) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", actualTypes, tt.expectedTypes)
|
||||
}
|
||||
assert.Equal(t, tt.expectedTypes, actualTypes)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -186,6 +213,7 @@ func Test_scopedPathNameFilter(t *testing.T) {
|
||||
want: "", // Update this with the expected result
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := scopedPathNameFilter(tt.args.pathName, tt.args.scope, tt.args.isDir); got != tt.want {
|
||||
@@ -194,103 +222,3 @@ func Test_scopedPathNameFilter(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isDoc(t *testing.T) {
|
||||
type args struct {
|
||||
extension string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := isDoc(tt.args.extension); got != tt.want {
|
||||
t.Errorf("isDoc() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getFileSize(t *testing.T) {
|
||||
type args struct {
|
||||
filepath string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int64
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := getFileSize(tt.args.filepath); got != tt.want {
|
||||
t.Errorf("getFileSize() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isArchive(t *testing.T) {
|
||||
type args struct {
|
||||
extension string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := isArchive(tt.args.extension); got != tt.want {
|
||||
t.Errorf("isArchive() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getLastPathComponent(t *testing.T) {
|
||||
type args struct {
|
||||
path string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := getLastPathComponent(tt.args.path); got != tt.want {
|
||||
t.Errorf("getLastPathComponent() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_generateRandomHash(t *testing.T) {
|
||||
type args struct {
|
||||
length int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := generateRandomHash(tt.args.length); got != tt.want {
|
||||
t.Errorf("generateRandomHash() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+25
-77
@@ -1,57 +1,32 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
)
|
||||
|
||||
// GetFileMetadata retrieves the FileInfo from the specified directory in the index.
|
||||
func (si *Index) GetFileMetadata(adjustedPath string) (FileInfo, bool) {
|
||||
si.mu.RLock()
|
||||
dir, exists := si.Directories[adjustedPath]
|
||||
si.mu.RUnlock()
|
||||
if exists {
|
||||
// Initialize the Metadata map if it is nil
|
||||
if dir.Metadata == nil {
|
||||
dir.Metadata = make(map[string]FileInfo)
|
||||
si.SetDirectoryInfo(adjustedPath, dir)
|
||||
return FileInfo{}, false
|
||||
} else {
|
||||
return dir.Metadata[adjustedPath], true
|
||||
}
|
||||
}
|
||||
return FileInfo{}, false
|
||||
}
|
||||
|
||||
// UpdateFileMetadata updates the FileInfo for the specified directory in the index.
|
||||
func (si *Index) UpdateFileMetadata(adjustedPath string, info FileInfo) bool {
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
dir, exists := si.Directories[adjustedPath]
|
||||
if !exists {
|
||||
// Initialize the Metadata map if it is nil
|
||||
if dir.Metadata == nil {
|
||||
dir.Metadata = make(map[string]FileInfo)
|
||||
}
|
||||
si.Directories[adjustedPath] = dir
|
||||
// Release the read lock before calling SetFileMetadata
|
||||
si.Directories[adjustedPath] = FileInfo{}
|
||||
}
|
||||
return si.SetFileMetadata(adjustedPath, info)
|
||||
return si.SetFileMetadata(adjustedPath, dir)
|
||||
}
|
||||
|
||||
// SetFileMetadata sets the FileInfo for the specified directory in the index.
|
||||
// internal use only
|
||||
func (si *Index) SetFileMetadata(adjustedPath string, info FileInfo) bool {
|
||||
|
||||
_, exists := si.Directories[adjustedPath]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
info.CacheTime = time.Now()
|
||||
si.Directories[adjustedPath].Metadata[adjustedPath] = info
|
||||
si.Directories[adjustedPath] = info
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -60,34 +35,40 @@ func (si *Index) GetMetadataInfo(adjustedPath string) (FileInfo, bool) {
|
||||
si.mu.RLock()
|
||||
dir, exists := si.Directories[adjustedPath]
|
||||
si.mu.RUnlock()
|
||||
if exists {
|
||||
// Initialize the Metadata map if it is nil
|
||||
if dir.Metadata == nil {
|
||||
dir.Metadata = make(map[string]FileInfo)
|
||||
si.SetDirectoryInfo(adjustedPath, dir)
|
||||
}
|
||||
info, metadataExists := dir.Metadata[adjustedPath]
|
||||
return info, metadataExists
|
||||
if !exists {
|
||||
return dir, exists
|
||||
}
|
||||
return FileInfo{}, false
|
||||
// remove recursive items, we only want this directories direct files
|
||||
cleanedItems := []ReducedItem{}
|
||||
for _, item := range dir.Items {
|
||||
cleanedItems = append(cleanedItems, ReducedItem{
|
||||
Name: item.Name,
|
||||
Size: item.Size,
|
||||
IsDir: item.IsDir,
|
||||
ModTime: item.ModTime,
|
||||
Type: item.Type,
|
||||
})
|
||||
}
|
||||
dir.Items = nil
|
||||
dir.ReducedItems = cleanedItems
|
||||
realPath, _, _ := GetRealPath(adjustedPath)
|
||||
dir.Path = realPath
|
||||
return dir, exists
|
||||
}
|
||||
|
||||
// SetDirectoryInfo sets the directory information in the index.
|
||||
func (si *Index) SetDirectoryInfo(adjustedPath string, dir Directory) {
|
||||
func (si *Index) SetDirectoryInfo(adjustedPath string, dir FileInfo) {
|
||||
si.mu.Lock()
|
||||
si.Directories[adjustedPath] = dir
|
||||
si.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetDirectoryInfo sets the directory information in the index.
|
||||
func (si *Index) GetDirectoryInfo(adjustedPath string) (Directory, bool) {
|
||||
func (si *Index) GetDirectoryInfo(adjustedPath string) (FileInfo, bool) {
|
||||
si.mu.RLock()
|
||||
dir, exists := si.Directories[adjustedPath]
|
||||
si.mu.RUnlock()
|
||||
if exists {
|
||||
return dir, true
|
||||
}
|
||||
return Directory{}, false
|
||||
return dir, exists
|
||||
}
|
||||
|
||||
func (si *Index) RemoveDirectory(path string) {
|
||||
@@ -127,7 +108,7 @@ func GetIndex(root string) *Index {
|
||||
}
|
||||
newIndex := &Index{
|
||||
Root: rootPath,
|
||||
Directories: make(map[string]Directory), // Initialize the map
|
||||
Directories: map[string]FileInfo{},
|
||||
NumDirs: 0,
|
||||
NumFiles: 0,
|
||||
inProgress: false,
|
||||
@@ -137,36 +118,3 @@ func GetIndex(root string) *Index {
|
||||
indexesMutex.Unlock()
|
||||
return newIndex
|
||||
}
|
||||
|
||||
func (si *Index) UpdateQuickList(files []fs.FileInfo) {
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
si.quickList = []File{}
|
||||
for _, file := range files {
|
||||
newFile := File{
|
||||
Name: file.Name(),
|
||||
IsDir: file.IsDir(),
|
||||
}
|
||||
si.quickList = append(si.quickList, newFile)
|
||||
}
|
||||
}
|
||||
|
||||
func (si *Index) UpdateQuickListForTests(files []File) {
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
si.quickList = []File{}
|
||||
for _, file := range files {
|
||||
newFile := File{
|
||||
Name: file.Name,
|
||||
IsDir: file.IsDir,
|
||||
}
|
||||
si.quickList = append(si.quickList, newFile)
|
||||
}
|
||||
}
|
||||
|
||||
func (si *Index) GetQuickList() []File {
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
newQuickList := si.quickList
|
||||
return newQuickList
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var testIndex Index
|
||||
|
||||
// Test for GetFileMetadata// Test for GetFileMetadata
|
||||
func TestGetFileMetadataSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
adjustedPath string
|
||||
expectedName string
|
||||
expectedSize int64
|
||||
}{
|
||||
{
|
||||
name: "testpath exists",
|
||||
adjustedPath: "/testpath",
|
||||
expectedName: "testfile.txt",
|
||||
expectedSize: 100,
|
||||
},
|
||||
{
|
||||
name: "testpath exists",
|
||||
adjustedPath: "/testpath",
|
||||
expectedName: "directory",
|
||||
expectedSize: 100,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fileInfo, _ := testIndex.GetMetadataInfo(tt.adjustedPath)
|
||||
// Iterate over fileInfo.Items to look for expectedName
|
||||
for _, item := range fileInfo.ReducedItems {
|
||||
// Assert the existence and the name
|
||||
if item.Name == tt.expectedName {
|
||||
assert.Equal(t, tt.expectedSize, item.Size)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test for GetFileMetadata// Test for GetFileMetadata
|
||||
func TestGetFileMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
adjustedPath string
|
||||
expectedName string
|
||||
expectedExists bool
|
||||
}{
|
||||
{
|
||||
name: "testpath exists",
|
||||
adjustedPath: "/testpath",
|
||||
expectedName: "testfile.txt",
|
||||
expectedExists: true,
|
||||
},
|
||||
{
|
||||
name: "testpath not exists",
|
||||
adjustedPath: "/testpath",
|
||||
expectedName: "nonexistent.txt",
|
||||
expectedExists: false,
|
||||
},
|
||||
{
|
||||
name: "File exists in /anotherpath",
|
||||
adjustedPath: "/anotherpath",
|
||||
expectedName: "afile.txt",
|
||||
expectedExists: true,
|
||||
},
|
||||
{
|
||||
name: "File does not exist in /anotherpath",
|
||||
adjustedPath: "/anotherpath",
|
||||
expectedName: "nonexistentfile.txt",
|
||||
expectedExists: false,
|
||||
},
|
||||
{
|
||||
name: "Directory does not exist",
|
||||
adjustedPath: "/nonexistentpath",
|
||||
expectedName: "",
|
||||
expectedExists: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fileInfo, _ := testIndex.GetMetadataInfo(tt.adjustedPath)
|
||||
found := false
|
||||
// Iterate over fileInfo.Items to look for expectedName
|
||||
for _, item := range fileInfo.ReducedItems {
|
||||
// Assert the existence and the name
|
||||
if item.Name == tt.expectedName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Equal(t, tt.expectedExists, found)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test for UpdateFileMetadata
|
||||
func TestUpdateFileMetadata(t *testing.T) {
|
||||
index := &Index{
|
||||
Directories: map[string]FileInfo{
|
||||
"/testpath": {
|
||||
Path: "/testpath",
|
||||
Name: "testpath",
|
||||
IsDir: true,
|
||||
ReducedItems: []ReducedItem{
|
||||
{Name: "testfile.txt"},
|
||||
{Name: "anotherfile.txt"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
info := FileInfo{Name: "testfile.txt"}
|
||||
|
||||
success := index.UpdateFileMetadata("/testpath", info)
|
||||
if !success {
|
||||
t.Fatalf("expected UpdateFileMetadata to succeed")
|
||||
}
|
||||
|
||||
dir, exists := index.Directories["/testpath"]
|
||||
if !exists || dir.ReducedItems[0].Name != "testfile.txt" {
|
||||
t.Fatalf("expected testfile.txt to be updated in the directory metadata")
|
||||
}
|
||||
}
|
||||
|
||||
// Test for GetDirMetadata
|
||||
func TestGetDirMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, exists := testIndex.GetMetadataInfo("/testpath")
|
||||
if !exists {
|
||||
t.Fatalf("expected GetDirMetadata to return initialized metadata map")
|
||||
}
|
||||
|
||||
_, exists = testIndex.GetMetadataInfo("/nonexistent")
|
||||
if exists {
|
||||
t.Fatalf("expected GetDirMetadata to return false for nonexistent directory")
|
||||
}
|
||||
}
|
||||
|
||||
// Test for SetDirectoryInfo
|
||||
func TestSetDirectoryInfo(t *testing.T) {
|
||||
index := &Index{
|
||||
Directories: map[string]FileInfo{
|
||||
"/testpath": {
|
||||
Path: "/testpath",
|
||||
Name: "testpath",
|
||||
IsDir: true,
|
||||
Items: []*FileInfo{
|
||||
{Name: "testfile.txt"},
|
||||
{Name: "anotherfile.txt"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
dir := FileInfo{
|
||||
Path: "/newPath",
|
||||
Name: "newPath",
|
||||
IsDir: true,
|
||||
Items: []*FileInfo{
|
||||
{Name: "testfile.txt"},
|
||||
},
|
||||
}
|
||||
index.SetDirectoryInfo("/newPath", dir)
|
||||
storedDir, exists := index.Directories["/newPath"]
|
||||
if !exists || storedDir.Items[0].Name != "testfile.txt" {
|
||||
t.Fatalf("expected SetDirectoryInfo to store directory info correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// Test for GetDirectoryInfo
|
||||
func TestGetDirectoryInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir, exists := testIndex.GetDirectoryInfo("/testpath")
|
||||
if !exists || dir.Items[0].Name != "testfile.txt" {
|
||||
t.Fatalf("expected GetDirectoryInfo to return correct directory info")
|
||||
}
|
||||
|
||||
_, exists = testIndex.GetDirectoryInfo("/nonexistent")
|
||||
if exists {
|
||||
t.Fatalf("expected GetDirectoryInfo to return false for nonexistent directory")
|
||||
}
|
||||
}
|
||||
|
||||
// Test for RemoveDirectory
|
||||
func TestRemoveDirectory(t *testing.T) {
|
||||
index := &Index{
|
||||
Directories: map[string]FileInfo{
|
||||
"/testpath": {},
|
||||
},
|
||||
}
|
||||
index.RemoveDirectory("/testpath")
|
||||
_, exists := index.Directories["/testpath"]
|
||||
if exists {
|
||||
t.Fatalf("expected directory to be removed")
|
||||
}
|
||||
}
|
||||
|
||||
// Test for UpdateCount
|
||||
func TestUpdateCount(t *testing.T) {
|
||||
index := &Index{}
|
||||
index.UpdateCount("files")
|
||||
if index.NumFiles != 1 {
|
||||
t.Fatalf("expected NumFiles to be 1 after UpdateCount('files')")
|
||||
}
|
||||
if index.NumFiles != 1 {
|
||||
t.Fatalf("expected NumFiles to be 1 after UpdateCount('files')")
|
||||
}
|
||||
index.UpdateCount("dirs")
|
||||
if index.NumDirs != 1 {
|
||||
t.Fatalf("expected NumDirs to be 1 after UpdateCount('dirs')")
|
||||
}
|
||||
index.UpdateCount("unknown")
|
||||
// Just ensure it does not panic or update any counters
|
||||
if index.NumFiles != 1 || index.NumDirs != 1 {
|
||||
t.Fatalf("expected counts to remain unchanged for unknown type")
|
||||
}
|
||||
index.resetCount()
|
||||
if index.NumFiles != 0 || index.NumDirs != 0 || !index.inProgress {
|
||||
t.Fatalf("expected resetCount to reset counts and set inProgress to true")
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
testIndex = Index{
|
||||
Root: "/",
|
||||
NumFiles: 10,
|
||||
NumDirs: 5,
|
||||
inProgress: false,
|
||||
Directories: map[string]FileInfo{
|
||||
"/testpath": {
|
||||
Path: "/testpath",
|
||||
Name: "testpath",
|
||||
IsDir: true,
|
||||
NumDirs: 1,
|
||||
NumFiles: 2,
|
||||
Items: []*FileInfo{
|
||||
{Name: "testfile.txt", Size: 100},
|
||||
{Name: "anotherfile.txt", Size: 100},
|
||||
},
|
||||
},
|
||||
"/anotherpath": {
|
||||
Path: "/anotherpath",
|
||||
Name: "anotherpath",
|
||||
IsDir: true,
|
||||
NumDirs: 1,
|
||||
NumFiles: 1,
|
||||
Items: []*FileInfo{
|
||||
{Name: "directory", IsDir: true, Size: 100},
|
||||
{Name: "afile.txt", Size: 100},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -47,7 +47,7 @@ require (
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.3.10 // indirect
|
||||
go.etcd.io/bbolt v1.3.11 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
||||
|
||||
+4
-35
@@ -2,7 +2,6 @@ github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
|
||||
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
|
||||
github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc=
|
||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
@@ -33,8 +32,6 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003142440-7a1927d49d9d/go.mod h1:LV
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU=
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw=
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
|
||||
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
@@ -42,11 +39,9 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
@@ -62,7 +57,6 @@ github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOW
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
|
||||
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
|
||||
@@ -71,7 +65,6 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
|
||||
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
@@ -85,12 +78,10 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU=
|
||||
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
@@ -103,29 +94,21 @@ github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/marusama/semaphore/v2 v2.5.0 h1:o/1QJD9DBYOWRnDhPwDVAXQn6mQYD0gZaS1Tpx6DJGM=
|
||||
github.com/marusama/semaphore/v2 v2.5.0/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
|
||||
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
||||
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
|
||||
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
|
||||
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM=
|
||||
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
@@ -143,7 +126,6 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
|
||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
@@ -153,18 +135,13 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofm
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
|
||||
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
|
||||
golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
@@ -174,15 +151,13 @@ golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -192,22 +167,16 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk=
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
|
||||
@@ -2,7 +2,6 @@ package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -127,11 +126,10 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
|
||||
return http.StatusBadRequest, nil
|
||||
}
|
||||
|
||||
user := &users.User{
|
||||
Username: info.Username,
|
||||
Password: info.Password,
|
||||
}
|
||||
settings.Config.UserDefaults.Apply(user)
|
||||
user := users.ApplyDefaults(users.User{})
|
||||
user.Username = info.Username
|
||||
user.Password = info.Password
|
||||
|
||||
userHome, err := d.settings.MakeUserDir(user.Username, user.Scope, d.server.Root)
|
||||
if err != nil {
|
||||
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
||||
@@ -139,8 +137,7 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
|
||||
}
|
||||
user.Scope = userHome
|
||||
log.Printf("new user: %s, home dir: [%s].", user.Username, userHome)
|
||||
settings.Config.UserDefaults.Apply(user)
|
||||
err = d.store.Users.Save(user)
|
||||
err = d.store.Users.Save(&user)
|
||||
if err == errors.ErrExist {
|
||||
return http.StatusConflict, err
|
||||
} else if err != nil {
|
||||
@@ -157,7 +154,6 @@ var renewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data
|
||||
func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (int, error) {
|
||||
duration, err := time.ParseDuration(settings.Config.Auth.TokenExpirationTime)
|
||||
if err != nil {
|
||||
fmt.Println("Error parsing duration:", err)
|
||||
duration = time.Hour * 2
|
||||
}
|
||||
claims := &authToken{
|
||||
|
||||
+12
-3
@@ -15,11 +15,20 @@ type modifyRequest struct {
|
||||
Which []string `json:"which"` // Answer to: which fields?
|
||||
}
|
||||
|
||||
var (
|
||||
store *storage.Storage
|
||||
server *settings.Server
|
||||
fileCache FileCache
|
||||
)
|
||||
|
||||
func SetupEnv(storage *storage.Storage, s *settings.Server, cache FileCache) {
|
||||
store = storage
|
||||
server = s
|
||||
fileCache = cache
|
||||
}
|
||||
|
||||
func NewHandler(
|
||||
imgSvc ImgService,
|
||||
fileCache FileCache,
|
||||
store *storage.Storage,
|
||||
server *settings.Server,
|
||||
assetsFs fs.FS,
|
||||
) (http.Handler, error) {
|
||||
server.Clean()
|
||||
|
||||
@@ -19,25 +19,25 @@ import (
|
||||
var withHashFile = func(fn handleFunc) handleFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
id, path := ifPathWithName(r)
|
||||
fmt.Println(id, path)
|
||||
link, err := d.store.Share.GetByHash(id)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
if link.Hash != "" {
|
||||
var status int
|
||||
status, err = authenticateShareRequest(r, link) // Assign to the existing `err` variable
|
||||
status, err = authenticateShareRequest(r, link)
|
||||
if err != nil || status != 0 {
|
||||
return status, err
|
||||
}
|
||||
}
|
||||
d.user = &users.PublicUser
|
||||
realPath, err := files.GetRealPath(d.user.Scope, link.Path, path)
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, link.Path, path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: true,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/share"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/storage/bolt"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
@@ -73,8 +74,13 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
|
||||
t.Errorf("failed to close db: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
storage, err := bolt.NewStorage(db)
|
||||
authStore, userStore, shareStore, settingsStore, err := bolt.NewStorage(db)
|
||||
storage := &storage.Storage{
|
||||
Auth: authStore,
|
||||
Users: userStore,
|
||||
Share: shareStore,
|
||||
Settings: settingsStore,
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get storage: %v", err)
|
||||
}
|
||||
|
||||
+2
-1
@@ -81,12 +81,13 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
|
||||
if !d.user.Perm.Download {
|
||||
return http.StatusAccepted, nil
|
||||
}
|
||||
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
|
||||
+21
-21
@@ -18,13 +18,13 @@ import (
|
||||
)
|
||||
|
||||
var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
if err != nil {
|
||||
fmt.Println("unable to get real path", d.user.Scope, r.URL.Path)
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: true,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
@@ -34,19 +34,16 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
if file.IsDir {
|
||||
file.Listing.Sorting = d.user.Sorting
|
||||
return renderJSON(w, r, file)
|
||||
}
|
||||
if checksum := r.URL.Query().Get("checksum"); checksum != "" {
|
||||
err := file.Checksum(checksum)
|
||||
if err == errors.ErrInvalidOption {
|
||||
return http.StatusBadRequest, nil
|
||||
} else if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
if !file.IsDir {
|
||||
if checksum := r.URL.Query().Get("checksum"); checksum != "" {
|
||||
err := file.Checksum(checksum)
|
||||
if err == errors.ErrInvalidOption {
|
||||
return http.StatusBadRequest, nil
|
||||
} else if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return renderJSON(w, r, file)
|
||||
})
|
||||
|
||||
@@ -55,12 +52,13 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
|
||||
if r.URL.Path == "/" || !d.user.Perm.Delete {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
fileOpts := files.FileOptions{
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
@@ -90,12 +88,13 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
|
||||
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
fileOpts := files.FileOptions{
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
@@ -109,7 +108,6 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
|
||||
}
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
file, err := files.FileInfoFaster(fileOpts)
|
||||
if err == nil {
|
||||
if r.URL.Query().Get("override") != "true" {
|
||||
@@ -141,12 +139,13 @@ var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
fileOpts := files.FileOptions{
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
@@ -187,7 +186,6 @@ func resourcePatchHandler(fileCache FileCache) handleFunc {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
err = d.RunHook(func() error {
|
||||
fmt.Println("hook", src, dst)
|
||||
return patchAction(r.Context(), action, src, dst, d, fileCache)
|
||||
}, action, src, dst, d.user)
|
||||
|
||||
@@ -237,16 +235,17 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
|
||||
}
|
||||
src = path.Clean("/" + src)
|
||||
dst = path.Clean("/" + dst)
|
||||
realDest, err := files.GetRealPath(d.user.Scope, dst)
|
||||
realDest, _, err := files.GetRealPath(d.user.Scope, dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
realSrc, err := files.GetRealPath(d.user.Scope, src)
|
||||
realSrc, isDir, err := files.GetRealPath(d.user.Scope, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: realSrc,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: false,
|
||||
@@ -274,12 +273,13 @@ type DiskUsageResponse struct {
|
||||
}
|
||||
|
||||
var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: false,
|
||||
|
||||
+6
-17
@@ -2,8 +2,6 @@ package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sort"
|
||||
@@ -15,6 +13,7 @@ import (
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
@@ -131,19 +130,7 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
|
||||
return http.StatusBadRequest, errors.ErrEmptyPassword
|
||||
}
|
||||
|
||||
userHome, err := d.settings.MakeUserDir(req.Data.Username, req.Data.Scope, d.server.Root)
|
||||
if err != nil {
|
||||
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
req.Data.Scope = userHome
|
||||
log.Printf("user: %s, home dir: [%s].", req.Data.Username, userHome)
|
||||
_, err = files.GetRealPath(d.server.Root, req.Data.Scope)
|
||||
if err != nil {
|
||||
fmt.Println("user path is not valid", req.Data.Scope)
|
||||
return http.StatusBadRequest, nil
|
||||
}
|
||||
err = d.store.Users.Save(req.Data)
|
||||
err = storage.CreateUser(*req.Data, req.Data.Perm.Admin)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -161,7 +148,7 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
|
||||
if req.Data.ID != d.raw.(uint) {
|
||||
return http.StatusBadRequest, nil
|
||||
}
|
||||
_, err = files.GetRealPath(d.server.Root, req.Data.Scope)
|
||||
_, _, err = files.GetRealPath(d.server.Root, req.Data.Scope)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, nil
|
||||
}
|
||||
@@ -175,7 +162,9 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
|
||||
t := v.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if field.Name != "Password" && field.Name != "Fs" {
|
||||
if field.Name == "Password" && req.Data.Password != "" {
|
||||
req.Which = append(req.Which, field.Name)
|
||||
} else if field.Name != "Password" && field.Name != "Fs" {
|
||||
req.Which = append(req.Which, field.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ checkExit() {
|
||||
if command -v go &> /dev/null
|
||||
then
|
||||
printf "\n == Running tests == \n"
|
||||
go test -race -v ./...
|
||||
go test -race -parallel -v ./...
|
||||
checkExit
|
||||
else
|
||||
echo "ERROR: unable to perform tests"
|
||||
|
||||
@@ -20,8 +20,8 @@ type Runner struct {
|
||||
|
||||
// RunHook runs the hooks for the before and after event.
|
||||
func (r *Runner) RunHook(fn func() error, evt, path, dst string, user *users.User) error {
|
||||
path, _ = files.GetRealPath(user.Scope, path)
|
||||
dst, _ = files.GetRealPath(user.Scope, dst)
|
||||
path, _, _ = files.GetRealPath(user.Scope, path)
|
||||
dst, _, _ = files.GetRealPath(user.Scope, dst)
|
||||
|
||||
if r.Enabled {
|
||||
if val, ok := r.Commands["before_"+evt]; ok {
|
||||
|
||||
+16
-24
@@ -3,10 +3,9 @@ package settings
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
var Config Settings
|
||||
@@ -19,22 +18,30 @@ func Initialize(configFile string) {
|
||||
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
||||
}
|
||||
Config.UserDefaults.Perm = Config.UserDefaults.Permissions
|
||||
Config.Server.Root = strings.TrimSuffix(Config.Server.Root, "/")
|
||||
// Convert relative path to absolute path
|
||||
realRoot, err := filepath.Abs(Config.Server.Root)
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting root path: %v", err)
|
||||
}
|
||||
_, err = os.Stat(realRoot)
|
||||
if err != nil {
|
||||
log.Fatalf("ERROR: Configured Root Path does not exist! %v", err)
|
||||
}
|
||||
Config.Server.Root = realRoot
|
||||
}
|
||||
|
||||
func loadConfigFile(configFile string) []byte {
|
||||
// Open and read the YAML file
|
||||
yamlFile, err := os.Open(configFile)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: opening config file\n %v\n WARNING: Using default config only\n If this was a mistake, please make sure the file exists and is accessible by the filebrowser binary.\n\n", err)
|
||||
Config = setDefaults()
|
||||
return []byte{}
|
||||
log.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer yamlFile.Close()
|
||||
|
||||
stat, err := yamlFile.Stat()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting file information: %s", err.Error())
|
||||
log.Fatalf("error getting file information: %s", err.Error())
|
||||
}
|
||||
|
||||
yamlData := make([]byte, stat.Size())
|
||||
@@ -77,8 +84,9 @@ func setDefaults() Settings {
|
||||
HideDotfiles: true,
|
||||
DarkMode: false,
|
||||
DisableSettings: false,
|
||||
ViewMode: "normal",
|
||||
Locale: "en",
|
||||
Permissions: users.Permissions{
|
||||
Permissions: Permissions{
|
||||
Create: false,
|
||||
Rename: false,
|
||||
Modify: false,
|
||||
@@ -90,19 +98,3 @@ func setDefaults() Settings {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Apply applies the default options to a user.
|
||||
func (d *UserDefaults) Apply(u *users.User) {
|
||||
u.StickySidebar = d.StickySidebar
|
||||
u.DisableSettings = d.DisableSettings
|
||||
u.DarkMode = d.DarkMode
|
||||
u.Scope = d.Scope
|
||||
u.Locale = d.Locale
|
||||
u.ViewMode = d.ViewMode
|
||||
u.SingleClick = d.SingleClick
|
||||
u.Perm = d.Perm
|
||||
u.Sorting = d.Sorting
|
||||
u.Commands = d.Commands
|
||||
u.HideDotfiles = d.HideDotfiles
|
||||
u.DateFormat = d.DateFormat
|
||||
}
|
||||
@@ -39,3 +39,15 @@ func GenerateKey() ([]byte, error) {
|
||||
func GetSettingsConfig(nameType string, Value string) string {
|
||||
return nameType + Value
|
||||
}
|
||||
|
||||
func AdminPerms() Permissions {
|
||||
return Permissions{
|
||||
Create: true,
|
||||
Rename: true,
|
||||
Modify: true,
|
||||
Delete: true,
|
||||
Share: true,
|
||||
Download: true,
|
||||
Admin: true,
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package settings
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/rules"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
@@ -82,9 +81,20 @@ type UserDefaults struct {
|
||||
By string `json:"by"`
|
||||
Asc bool `json:"asc"`
|
||||
} `json:"sorting"`
|
||||
Perm users.Permissions `json:"perm"`
|
||||
Permissions users.Permissions `json:"permissions"`
|
||||
Commands []string `json:"commands,omitempty"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
Perm Permissions `json:"perm"`
|
||||
Permissions Permissions `json:"permissions"`
|
||||
Commands []string `json:"commands,omitempty"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
}
|
||||
|
||||
type Permissions struct {
|
||||
Admin bool `json:"admin"`
|
||||
Execute bool `json:"execute"`
|
||||
Create bool `json:"create"`
|
||||
Rename bool `json:"rename"`
|
||||
Modify bool `json:"modify"`
|
||||
Delete bool `json:"delete"`
|
||||
Share bool `json:"share"`
|
||||
Download bool `json:"download"`
|
||||
}
|
||||
@@ -28,5 +28,5 @@ func (s authBackend) Get(t string) (auth.Auther, error) {
|
||||
}
|
||||
|
||||
func (s authBackend) Save(a auth.Auther) error {
|
||||
return save(s.db, "auther", a)
|
||||
return Save(s.db, "auther", a)
|
||||
}
|
||||
@@ -6,26 +6,14 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/share"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
// NewStorage creates a storage.Storage based on Bolt DB.
|
||||
func NewStorage(db *storm.DB) (*storage.Storage, error) {
|
||||
func NewStorage(db *storm.DB) (*auth.Storage, *users.Storage, *share.Storage, *settings.Storage, error) {
|
||||
userStore := users.NewStorage(usersBackend{db: db})
|
||||
shareStore := share.NewStorage(shareBackend{db: db})
|
||||
settingsStore := settings.NewStorage(settingsBackend{db: db})
|
||||
authStore := auth.NewStorage(authBackend{db: db}, userStore)
|
||||
|
||||
err := save(db, "version", 2) //nolint:gomnd
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &storage.Storage{
|
||||
Auth: authStore,
|
||||
Users: userStore,
|
||||
Share: shareStore,
|
||||
Settings: settingsStore,
|
||||
}, nil
|
||||
return authStore, userStore, shareStore, settingsStore, nil
|
||||
}
|
||||
@@ -15,7 +15,7 @@ func (s settingsBackend) Get() (*settings.Settings, error) {
|
||||
}
|
||||
|
||||
func (s settingsBackend) Save(set *settings.Settings) error {
|
||||
return save(s.db, "settings", set)
|
||||
return Save(s.db, "settings", set)
|
||||
}
|
||||
|
||||
func (s settingsBackend) GetServer() (*settings.Server, error) {
|
||||
@@ -27,5 +27,5 @@ func (s settingsBackend) GetServer() (*settings.Server, error) {
|
||||
}
|
||||
|
||||
func (s settingsBackend) SaveServer(server *settings.Server) error {
|
||||
return save(s.db, "server", server)
|
||||
return Save(s.db, "server", server)
|
||||
}
|
||||
@@ -15,6 +15,6 @@ func get(db *storm.DB, name string, to interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func save(db *storm.DB, name string, from interface{}) error {
|
||||
func Save(db *storm.DB, name string, from interface{}) error {
|
||||
return db.Set("config", name, from)
|
||||
}
|
||||
@@ -1,10 +1,20 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/gtsteffaniak/filebrowser/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/share"
|
||||
"github.com/gtsteffaniak/filebrowser/storage/bolt"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
// Storage is a storage powered by a Backend which makes the necessary
|
||||
@@ -15,3 +25,112 @@ type Storage struct {
|
||||
Auth *auth.Storage
|
||||
Settings *settings.Storage
|
||||
}
|
||||
|
||||
var store *Storage
|
||||
|
||||
func InitializeDb(path string) (*Storage, bool, error) {
|
||||
exists, err := dbExists(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
db, err := storm.Open(path)
|
||||
|
||||
utils.CheckErr(fmt.Sprintf("storm.Open path %v", path), err)
|
||||
authStore, userStore, shareStore, settingsStore, err := bolt.NewStorage(db)
|
||||
if err != nil {
|
||||
return nil, exists, err
|
||||
}
|
||||
|
||||
err = bolt.Save(db, "version", 2) //nolint:gomnd
|
||||
if err != nil {
|
||||
return nil, exists, err
|
||||
}
|
||||
store = &Storage{
|
||||
Auth: authStore,
|
||||
Users: userStore,
|
||||
Share: shareStore,
|
||||
Settings: settingsStore,
|
||||
}
|
||||
if !exists {
|
||||
quickSetup(store)
|
||||
}
|
||||
|
||||
return store, exists, err
|
||||
}
|
||||
|
||||
func dbExists(path string) (bool, error) {
|
||||
stat, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return stat.Size() != 0, nil
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
d := filepath.Dir(path)
|
||||
_, err = os.Stat(d)
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet,gomnd
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
func quickSetup(store *Storage) {
|
||||
settings.Config.Auth.Key = utils.GenerateKey()
|
||||
if settings.Config.Auth.Method == "noauth" {
|
||||
err := store.Auth.Save(&auth.NoAuth{})
|
||||
utils.CheckErr("store.Auth.Save", err)
|
||||
} else {
|
||||
settings.Config.Auth.Method = "password"
|
||||
err := store.Auth.Save(&auth.JSONAuth{})
|
||||
utils.CheckErr("store.Auth.Save", err)
|
||||
}
|
||||
err := store.Settings.Save(&settings.Config)
|
||||
utils.CheckErr("store.Settings.Save", err)
|
||||
err = store.Settings.SaveServer(&settings.Config.Server)
|
||||
utils.CheckErr("store.Settings.SaveServer", err)
|
||||
user := users.ApplyDefaults(users.User{})
|
||||
user.Username = settings.Config.Auth.AdminUsername
|
||||
user.Password = settings.Config.Auth.AdminPassword
|
||||
user.Perm.Admin = true
|
||||
user.Scope = "./"
|
||||
user.DarkMode = true
|
||||
user.ViewMode = "normal"
|
||||
user.LockPassword = false
|
||||
user.Perm = settings.AdminPerms()
|
||||
err = store.Users.Save(&user)
|
||||
utils.CheckErr("store.Users.Save", err)
|
||||
}
|
||||
|
||||
// create new user
|
||||
func CreateUser(userInfo users.User, asAdmin bool) error {
|
||||
// must have username or password to create
|
||||
if userInfo.Username == "" || userInfo.Password == "" {
|
||||
return errors.ErrInvalidRequestParams
|
||||
}
|
||||
newUser := users.ApplyDefaults(userInfo)
|
||||
if asAdmin {
|
||||
newUser.Perm = settings.AdminPerms()
|
||||
}
|
||||
// create new home directory
|
||||
userHome, err := settings.Config.MakeUserDir(newUser.Username, newUser.Scope, settings.Config.Server.Root)
|
||||
if err != nil {
|
||||
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
||||
return err
|
||||
}
|
||||
newUser.Scope = userHome
|
||||
log.Printf("user: %s, home dir: [%s].", newUser.Username, userHome)
|
||||
_, _, err = files.GetRealPath(settings.Config.Server.Root, newUser.Scope)
|
||||
if err != nil {
|
||||
log.Println("user path is not valid", newUser.Scope)
|
||||
return nil
|
||||
}
|
||||
err = store.Users.Save(&newUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -123,7 +122,6 @@ func (s *Storage) DeleteRule(userID string, ruleID string) error {
|
||||
|
||||
// Save saves the user in a storage.
|
||||
func (s *Storage) Save(user *User) error {
|
||||
log.Println("Saving new user:", user.Username)
|
||||
return s.back.Save(user)
|
||||
}
|
||||
|
||||
|
||||
+37
-30
@@ -4,19 +4,9 @@ import (
|
||||
"regexp"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/rules"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
)
|
||||
|
||||
type Permissions struct {
|
||||
Admin bool `json:"admin"`
|
||||
Execute bool `json:"execute"`
|
||||
Create bool `json:"create"`
|
||||
Rename bool `json:"rename"`
|
||||
Modify bool `json:"modify"`
|
||||
Delete bool `json:"delete"`
|
||||
Share bool `json:"share"`
|
||||
Download bool `json:"download"`
|
||||
}
|
||||
|
||||
// SortingSettings represents the sorting settings.
|
||||
type Sorting struct {
|
||||
By string `json:"by"`
|
||||
@@ -25,24 +15,24 @@ type Sorting struct {
|
||||
|
||||
// User describes a user.
|
||||
type User struct {
|
||||
StickySidebar bool `json:"stickySidebar"`
|
||||
DarkMode bool `json:"darkMode"`
|
||||
DisableSettings bool `json:"disableSettings"`
|
||||
ID uint `storm:"id,increment" json:"id"`
|
||||
Username string `storm:"unique" json:"username"`
|
||||
Password string `json:"password"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Perm Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
Sorting Sorting `json:"sorting"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
GallerySize int `json:"gallerySize"`
|
||||
StickySidebar bool `json:"stickySidebar"`
|
||||
DarkMode bool `json:"darkMode"`
|
||||
DisableSettings bool `json:"disableSettings"`
|
||||
ID uint `storm:"id,increment" json:"id"`
|
||||
Username string `storm:"unique" json:"username"`
|
||||
Password string `json:"password"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Perm settings.Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
Sorting Sorting `json:"sorting"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
GallerySize int `json:"gallerySize"`
|
||||
}
|
||||
|
||||
var PublicUser = User{
|
||||
@@ -51,7 +41,7 @@ var PublicUser = User{
|
||||
Scope: "./",
|
||||
ViewMode: "normal",
|
||||
LockPassword: true,
|
||||
Perm: Permissions{
|
||||
Perm: settings.Permissions{
|
||||
Create: false,
|
||||
Rename: false,
|
||||
Modify: false,
|
||||
@@ -81,3 +71,20 @@ func (u *User) CanExecute(command string) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Apply applies the default options to a user.
|
||||
func ApplyDefaults(u User) User {
|
||||
u.StickySidebar = settings.Config.UserDefaults.StickySidebar
|
||||
u.DisableSettings = settings.Config.UserDefaults.DisableSettings
|
||||
u.DarkMode = settings.Config.UserDefaults.DarkMode
|
||||
u.Scope = settings.Config.UserDefaults.Scope
|
||||
u.Locale = settings.Config.UserDefaults.Locale
|
||||
u.ViewMode = settings.Config.UserDefaults.ViewMode
|
||||
u.SingleClick = settings.Config.UserDefaults.SingleClick
|
||||
u.Perm = settings.Config.UserDefaults.Perm
|
||||
u.Sorting = settings.Config.UserDefaults.Sorting
|
||||
u.Commands = settings.Config.UserDefaults.Commands
|
||||
u.HideDotfiles = settings.Config.UserDefaults.HideDotfiles
|
||||
u.DateFormat = settings.Config.UserDefaults.DateFormat
|
||||
return u
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
)
|
||||
|
||||
func CheckErr(source string, err error) {
|
||||
if err != nil {
|
||||
log.Fatalf("%s: %v", source, err)
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateKey() []byte {
|
||||
k, err := settings.GenerateKey()
|
||||
CheckErr("generateKey", err)
|
||||
return k
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,2 @@
|
||||
# Contributing Guide
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Getting Started using FileBrowser Quantum
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Migration help
|
||||
|
||||
It is possible to use the same database as used by filebrowser/filebrowser,
|
||||
but you will need to follow the following process:
|
||||
|
||||
1. Create a configuration file as mentioned above.
|
||||
2. Copy your database file from the original filebrowser to the path of
|
||||
the new one.
|
||||
3. Update the configuration file to use the database (under server in
|
||||
filebrowser.yml)
|
||||
4. If you are using docker, update the docker-compose file or docker run
|
||||
command to use the config file as described in the install section
|
||||
above.
|
||||
5. If you are not using docker, just make sure you run filebrowser -c
|
||||
filebrowser.yml and have a valid filebrowser config.
|
||||
|
||||
|
||||
Note: share links will not work and will need to be re-created after migration.
|
||||
|
||||
The filebrowser Quantum application should run with the same user and rules that
|
||||
you have from the original. But keep in mind the differences that may not work
|
||||
the same way, but all user configuration should be available.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Planned Roadmap
|
||||
|
||||
upcoming 0.2.x releases:
|
||||
|
||||
- Replace http routes for gorilla/mux with stdlib
|
||||
- Theme configuration from settings
|
||||
- File syncronization improvements
|
||||
- more filetype previews
|
||||
|
||||
next major 0.3.0 release :
|
||||
|
||||
- multiple sources https://github.com/filebrowser/filebrowser/issues/2514
|
||||
- introduce jobs as replacement to runners.
|
||||
- Add Job status to the sidebar
|
||||
- index status.
|
||||
- Job status from users
|
||||
- upload status
|
||||
|
||||
Unplanned Future releases:
|
||||
- Add tools to sidebar
|
||||
- duplicate file detector.
|
||||
- bulk rename https://github.com/filebrowser/filebrowser/issues/2473
|
||||
- metrics tracker - user access, file access, download count, last login, etc
|
||||
- support minio, s3, and backblaze sources https://github.com/filebrowser/filebrowser/issues/2544
|
||||
@@ -9,7 +9,7 @@
|
||||
<script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit" data-vite-ignore></script>
|
||||
[{[ end ]}]
|
||||
|
||||
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]</title>
|
||||
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]FileBrowser Quantum[{[ end ]}]</title>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="256x256" href="[{[ .StaticURL ]}]/img/icons/favicon-256x256.png">
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
|
||||
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
|
||||
var dynamicManifest = {
|
||||
"name": window.FileBrowser.Name || 'File Browser',
|
||||
"short_name": window.FileBrowser.Name || 'File Browser',
|
||||
"name": window.FileBrowser.Name || 'FileBrowser Quantum',
|
||||
"short_name": window.FileBrowser.Name || 'FileBrowser',
|
||||
"icons": [
|
||||
{
|
||||
"src": fullStaticURL + "/img/icons/android-chrome-256x256.png",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "File Browser",
|
||||
"short_name": "File Browser",
|
||||
"name": "FileBrowser",
|
||||
"short_name": "FileBrowser",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./img/icons/android-chrome-192x192.png",
|
||||
|
||||
+158
-102
@@ -1,122 +1,151 @@
|
||||
import { createURL, fetchURL, removePrefix } from "./utils";
|
||||
import { baseURL } from "@/utils/constants";
|
||||
import { state } from "@/store";
|
||||
import { notify } from "@/notify";
|
||||
|
||||
export async function fetch(url,content=false) {
|
||||
url = removePrefix(url);
|
||||
// Notify if errors occur
|
||||
export async function fetch(url, content = false) {
|
||||
try {
|
||||
url = removePrefix(url);
|
||||
|
||||
const res = await fetchURL(`/api/resources${url}?content=${content}`, {});
|
||||
const res = await fetchURL(`/api/resources${url}?content=${content}`, {});
|
||||
const data = await res.json();
|
||||
data.url = `/files${url}`;
|
||||
|
||||
let data = await res.json();
|
||||
data.url = `/files${url}`;
|
||||
if (data.isDir) {
|
||||
if (!data.url.endsWith("/")) data.url += "/";
|
||||
data.items = data.items.map((item, index) => {
|
||||
item.index = index;
|
||||
item.url = `${data.url}${encodeURIComponent(item.name)}`;
|
||||
|
||||
if (data.isDir) {
|
||||
if (!data.url.endsWith("/")) data.url += "/";
|
||||
data.items = data.items.map((item, index) => {
|
||||
item.index = index;
|
||||
item.url = `${data.url}${encodeURIComponent(item.name)}`;
|
||||
if (item.isDir) {
|
||||
item.url += "/";
|
||||
}
|
||||
|
||||
if (item.isDir) {
|
||||
item.url += "/";
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
return data;
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error fetching data");
|
||||
throw err;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function resourceAction(url, method, content) {
|
||||
url = removePrefix(url);
|
||||
try {
|
||||
url = removePrefix(url);
|
||||
|
||||
let opts = { method };
|
||||
let opts = { method };
|
||||
|
||||
if (content) {
|
||||
opts.body = content;
|
||||
if (content) {
|
||||
opts.body = content;
|
||||
}
|
||||
|
||||
const res = await fetchURL(`/api/resources${url}`, opts);
|
||||
return res;
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error performing resource action");
|
||||
throw err;
|
||||
}
|
||||
|
||||
const res = await fetchURL(`/api/resources${url}`, opts);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function remove(url) {
|
||||
return resourceAction(url, "DELETE");
|
||||
try {
|
||||
return await resourceAction(url, "DELETE");
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error deleting resource");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function put(url, content = "") {
|
||||
return resourceAction(url, "PUT", content);
|
||||
try {
|
||||
return await resourceAction(url, "PUT", content);
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error putting resource");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function download(format, ...files) {
|
||||
let url = `${baseURL}/api/raw`;
|
||||
try {
|
||||
let url = `${baseURL}/api/raw`;
|
||||
|
||||
if (files.length === 1) {
|
||||
url += removePrefix(files[0]) + "?";
|
||||
} else {
|
||||
let arg = "";
|
||||
if (files.length === 1) {
|
||||
url += removePrefix(files[0]) + "?";
|
||||
} else {
|
||||
let arg = "";
|
||||
|
||||
for (let file of files) {
|
||||
arg += removePrefix(file) + ",";
|
||||
for (let file of files) {
|
||||
arg += removePrefix(file) + ",";
|
||||
}
|
||||
|
||||
arg = arg.substring(0, arg.length - 1);
|
||||
arg = encodeURIComponent(arg);
|
||||
url += `/?files=${arg}&`;
|
||||
}
|
||||
|
||||
arg = arg.substring(0, arg.length - 1);
|
||||
arg = encodeURIComponent(arg);
|
||||
url += `/?files=${arg}&`;
|
||||
}
|
||||
if (format) {
|
||||
url += `algo=${format}&`;
|
||||
}
|
||||
|
||||
if (format) {
|
||||
url += `algo=${format}&`;
|
||||
}
|
||||
if (state.jwt) {
|
||||
url += `auth=${state.jwt}&`;
|
||||
}
|
||||
|
||||
if (state.jwt) {
|
||||
url += `auth=${state.jwt}&`;
|
||||
window.open(url);
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error downloading files");
|
||||
}
|
||||
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
export async function post(url, content = "", overwrite = false, onupload) {
|
||||
url = removePrefix(url);
|
||||
try {
|
||||
url = removePrefix(url);
|
||||
|
||||
let bufferContent;
|
||||
if (
|
||||
content instanceof Blob &&
|
||||
!["http:", "https:"].includes(window.location.protocol)
|
||||
) {
|
||||
bufferContent = await new Response(content).arrayBuffer();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new XMLHttpRequest();
|
||||
request.open(
|
||||
"POST",
|
||||
`${baseURL}/api/resources${url}?override=${overwrite}`,
|
||||
true
|
||||
);
|
||||
request.setRequestHeader("X-Auth", state.jwt);
|
||||
|
||||
if (typeof onupload === "function") {
|
||||
request.upload.onprogress = onupload;
|
||||
let bufferContent;
|
||||
if (
|
||||
content instanceof Blob &&
|
||||
!["http:", "https:"].includes(window.location.protocol)
|
||||
) {
|
||||
bufferContent = await new Response(content).arrayBuffer();
|
||||
}
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
resolve(request.responseText);
|
||||
} else if (request.status === 409) {
|
||||
reject(request.status);
|
||||
} else {
|
||||
reject(request.responseText);
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new XMLHttpRequest();
|
||||
request.open(
|
||||
"POST",
|
||||
`${baseURL}/api/resources${url}?override=${overwrite}`,
|
||||
true
|
||||
);
|
||||
request.setRequestHeader("X-Auth", state.jwt);
|
||||
|
||||
if (typeof onupload === "function") {
|
||||
request.upload.onprogress = onupload;
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error("001 Connection aborted"));
|
||||
};
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
resolve(request.responseText);
|
||||
} else if (request.status === 409) {
|
||||
reject(request.status);
|
||||
} else {
|
||||
reject(request.responseText);
|
||||
}
|
||||
};
|
||||
|
||||
request.send(bufferContent || content);
|
||||
});
|
||||
request.onerror = () => {
|
||||
reject(new Error("001 Connection aborted"));
|
||||
};
|
||||
|
||||
request.send(bufferContent || content);
|
||||
});
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error posting resource");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function moveCopy(items, copy = false, overwrite = false, rename = false) {
|
||||
@@ -131,7 +160,10 @@ function moveCopy(items, copy = false, overwrite = false, rename = false) {
|
||||
promises.push(resourceAction(url, "PATCH"));
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
return Promise.all(promises).catch((err) => {
|
||||
notify.showError(err.message || "Error moving/copying resources");
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
export function move(items, overwrite = false, rename = false) {
|
||||
@@ -143,44 +175,68 @@ export function copy(items, overwrite = false, rename = false) {
|
||||
}
|
||||
|
||||
export async function checksum(url, algo) {
|
||||
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
|
||||
return (await data.json()).checksums[algo];
|
||||
try {
|
||||
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
|
||||
return (await data.json()).checksums[algo];
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error fetching checksum");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function getDownloadURL(file, inline) {
|
||||
const params = {
|
||||
...(inline && { inline: "true" }),
|
||||
};
|
||||
try {
|
||||
const params = {
|
||||
...(inline && { inline: "true" }),
|
||||
};
|
||||
|
||||
return createURL("api/raw" + file.path, params);
|
||||
return createURL("api/raw" + file.path, params);
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error getting download URL");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPreviewURL(file, size) {
|
||||
const params = {
|
||||
inline: "true",
|
||||
key: Date.parse(file.modified),
|
||||
};
|
||||
try {
|
||||
const params = {
|
||||
inline: "true",
|
||||
key: Date.parse(file.modified),
|
||||
};
|
||||
|
||||
return createURL("api/preview/" + size + file.path, params);
|
||||
return createURL("api/preview/" + size + file.path, params);
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error getting preview URL");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function getSubtitlesURL(file) {
|
||||
const params = {
|
||||
inline: "true",
|
||||
};
|
||||
try {
|
||||
const params = {
|
||||
inline: "true",
|
||||
};
|
||||
|
||||
const subtitles = [];
|
||||
for (const sub of file.subtitles) {
|
||||
subtitles.push(createURL("api/raw" + sub, params));
|
||||
const subtitles = [];
|
||||
for (const sub of file.subtitles) {
|
||||
subtitles.push(createURL("api/raw" + sub, params));
|
||||
}
|
||||
|
||||
return subtitles;
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error fetching subtitles URL");
|
||||
throw err;
|
||||
}
|
||||
|
||||
return subtitles;
|
||||
}
|
||||
|
||||
export async function usage(url) {
|
||||
url = removePrefix(url);
|
||||
try {
|
||||
url = removePrefix(url);
|
||||
|
||||
const res = await fetchURL(`/api/usage${url}`, {});
|
||||
|
||||
return await res.json();
|
||||
const res = await fetchURL(`/api/usage${url}`, {});
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error fetching usage data");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -84,5 +84,6 @@ export function getDownloadURL(share, inline = false) {
|
||||
if (share.path == undefined) {
|
||||
share.path = ""
|
||||
}
|
||||
return createURL("api/public/dl/" + share.hash + "/"+share.path, params, false);
|
||||
const path = share.path.replace("/share/"+share.hash +"/","")
|
||||
return createURL("api/public/dl/" + share.hash + "/"+path, params, false);
|
||||
}
|
||||
+21
-15
@@ -1,22 +1,28 @@
|
||||
import { fetchURL, removePrefix } from "./utils";
|
||||
import url from "../utils/url";
|
||||
import { notify } from "@/notify"; // Import notify for error handling
|
||||
|
||||
export default async function search(base, query) {
|
||||
base = removePrefix(base);
|
||||
query = encodeURIComponent(query);
|
||||
try {
|
||||
base = removePrefix(base);
|
||||
query = encodeURIComponent(query);
|
||||
|
||||
if (!base.endsWith("/")) {
|
||||
base += "/";
|
||||
if (!base.endsWith("/")) {
|
||||
base += "/";
|
||||
}
|
||||
|
||||
const res = await fetchURL(`/api/search${base}?query=${query}`, {});
|
||||
|
||||
let data = await res.json();
|
||||
|
||||
data = data.map((item) => {
|
||||
item.url = `/files${base}` + url.encodePath(item.path);
|
||||
return item;
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error occurred during search");
|
||||
throw err;
|
||||
}
|
||||
|
||||
let res = await fetchURL(`/api/search${base}?query=${query}`, {});
|
||||
|
||||
let data = await res.json();
|
||||
|
||||
data = data.map((item) => {
|
||||
item.url = `/files${base}` + url.encodePath(item.path);
|
||||
return item;
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
+57
-28
@@ -1,47 +1,76 @@
|
||||
import { fetchURL, fetchJSON } from "@/api/utils";
|
||||
import { notify } from "@/notify"; // Import notify for error handling
|
||||
|
||||
export async function getAllUsers() {
|
||||
return await fetchJSON(`/api/users`, {});
|
||||
try {
|
||||
return await fetchJSON(`/api/users`, {});
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Failed to fetch users");
|
||||
throw err; // Re-throw to handle further if needed
|
||||
}
|
||||
}
|
||||
|
||||
export async function get(id) {
|
||||
return fetchJSON(`/api/users/${id}`, {});
|
||||
try {
|
||||
return await fetchJSON(`/api/users/${id}`, {});
|
||||
} catch (err) {
|
||||
notify.showError(err.message || `Failed to fetch user with ID: ${id}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(user) {
|
||||
const res = await fetchURL(`/api/users`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
what: "user",
|
||||
which: [],
|
||||
data: user,
|
||||
}),
|
||||
});
|
||||
try {
|
||||
const res = await fetchURL(`/api/users`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
what: "user",
|
||||
which: [],
|
||||
data: user,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.status === 201) {
|
||||
return res.headers.get("Location");
|
||||
if (res.status === 201) {
|
||||
return res.headers.get("Location");
|
||||
} else {
|
||||
throw new Error("Failed to create user");
|
||||
}
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error creating user");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(user, which = ["all"]) {
|
||||
if (which[0] != "password") {
|
||||
user.password = "";
|
||||
try {
|
||||
// List of keys to exclude from the "which" array
|
||||
const excludeKeys = ["id", "name"];
|
||||
// Filter out the keys from "which"
|
||||
which = which.filter(item => !excludeKeys.includes(item));
|
||||
if (user.username === "publicUser") {
|
||||
return;
|
||||
}
|
||||
await fetchURL(`/api/users/${user.id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
what: "user",
|
||||
which: which,
|
||||
data: user,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
notify.showError(err.message || `Failed to update user with ID: ${user.id}`);
|
||||
throw err;
|
||||
}
|
||||
if (user.username == "publicUser") {
|
||||
return
|
||||
}
|
||||
await fetchURL(`/api/users/${user.id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
what: "user",
|
||||
which: which,
|
||||
data: user,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function remove(id) {
|
||||
await fetchURL(`/api/users/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
try {
|
||||
await fetchURL(`/api/users/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
} catch (err) {
|
||||
notify.showError(err.message || `Failed to delete user with ID: ${id}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { state } from "@/store";
|
||||
import { renew, logout } from "@/utils/auth";
|
||||
import { baseURL } from "@/utils/constants";
|
||||
import { encodePath } from "@/utils/url";
|
||||
import { showError } from "@/notify";
|
||||
import { notify } from "@/notify";
|
||||
|
||||
export async function fetchURL(url, opts, auth = true) {
|
||||
opts = opts || {};
|
||||
@@ -51,7 +51,7 @@ export async function fetchJSON(url, opts) {
|
||||
if (res.status === 200) {
|
||||
return res.json();
|
||||
} else {
|
||||
showError("unable to fetch : " + url + "status" + res.status);
|
||||
notify.showError("unable to fetch : " + url + "status" + res.status);
|
||||
throw new Error(res.status);
|
||||
}
|
||||
}
|
||||
|
||||
File renamed without changes.
@@ -14,24 +14,26 @@
|
||||
<component :is="element" :to="link.url">{{ link.name }}</component>
|
||||
</span>
|
||||
<action style="display: contents" v-if="showShare" icon="share" show="share" />
|
||||
<div v-if="isResizableView">
|
||||
<div v-if="isCardView">
|
||||
Size:
|
||||
<input
|
||||
v-model="gallerySize"
|
||||
type="range"
|
||||
id="gallary-size"
|
||||
name="gallary-size"
|
||||
id="gallery-size"
|
||||
name="gallery-size"
|
||||
:value="gallerySize"
|
||||
min="0"
|
||||
max="10"
|
||||
@input="updateGallerySize"
|
||||
@change="commitGallerySize"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { state, mutations, getters } from "@/store"; // Import mutations as well
|
||||
import Action from "@/components/header/Action.vue";
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import Action from "@/components/Action.vue";
|
||||
|
||||
export default {
|
||||
name: "breadcrumbs",
|
||||
@@ -43,16 +45,10 @@ export default {
|
||||
gallerySize: state.user.gallerySize,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
gallerySize(newValue) {
|
||||
this.gallerySize = parseInt(newValue, 0); // Update the user object
|
||||
mutations.setGallerySize(this.gallerySize);
|
||||
},
|
||||
},
|
||||
props: ["base", "noLink"],
|
||||
computed: {
|
||||
isResizableView() {
|
||||
return getters.isResizableView();
|
||||
isCardView() {
|
||||
return getters.isCardView();
|
||||
},
|
||||
items() {
|
||||
const relativePath = state.route.path.replace(this.base, "");
|
||||
@@ -100,17 +96,15 @@ export default {
|
||||
return "router-link";
|
||||
},
|
||||
showShare() {
|
||||
// Ensure user properties are accessed safely
|
||||
if (state.route.path.startsWith("/share")) {
|
||||
return false;
|
||||
}
|
||||
return state.user?.perm && state.user?.perm.share; // Access from state directly
|
||||
return state.user?.perm && state.user?.perm.share;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// Example of a method using mutations
|
||||
updateUserPermissions(newPerms) {
|
||||
mutations.updateUser({ perm: newPerms });
|
||||
updateGallerySize(event) {
|
||||
this.gallerySize = parseInt(event.target.value, 10);
|
||||
},
|
||||
commitGallerySize() {
|
||||
mutations.setGallerySize(this.gallerySize);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<div
|
||||
id="context-menu"
|
||||
ref="contextMenu"
|
||||
v-show="showContext"
|
||||
:style="{
|
||||
top: `${top}px`,
|
||||
left: `${left}px`,
|
||||
}"
|
||||
class="button"
|
||||
:class="{ 'dark-mode': isDarkMode, mobile: isMobile }"
|
||||
>
|
||||
<div v-if="selectedCount > 0" class="button selected-count-header">
|
||||
<span>{{ selectedCount }} selected</span>
|
||||
</div>
|
||||
|
||||
<action
|
||||
v-if="!headerButtons.select"
|
||||
icon="create_new_folder"
|
||||
:label="$t('sidebar.newFolder')"
|
||||
@action="showHover('newDir')"
|
||||
/>
|
||||
<action
|
||||
v-if="!headerButtons.select"
|
||||
icon="note_add"
|
||||
:label="$t('sidebar.newFile')"
|
||||
@action="showHover('newFile')"
|
||||
/>
|
||||
<action
|
||||
v-if="!headerButtons.select"
|
||||
icon="file_upload"
|
||||
:label="$t('buttons.upload')"
|
||||
@action="uploadFunc"
|
||||
/>
|
||||
|
||||
<action
|
||||
v-if="headerButtons.select"
|
||||
icon="info"
|
||||
:label="$t('buttons.info')"
|
||||
show="info"
|
||||
/>
|
||||
<action
|
||||
v-if="!isMultiple"
|
||||
icon="check_circle"
|
||||
:label="$t('buttons.selectMultiple')"
|
||||
@action="toggleMultipleSelection"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.download"
|
||||
icon="file_download"
|
||||
:label="$t('buttons.download')"
|
||||
@action="startDownload"
|
||||
:counter="selectedCount"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.share"
|
||||
icon="share"
|
||||
:label="$t('buttons.share')"
|
||||
show="share"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.rename"
|
||||
icon="mode_edit"
|
||||
:label="$t('buttons.rename')"
|
||||
show="rename"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.copy"
|
||||
icon="content_copy"
|
||||
:label="$t('buttons.copyFile')"
|
||||
show="copy"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.move"
|
||||
icon="forward"
|
||||
:label="$t('buttons.moveFile')"
|
||||
show="move"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.delete"
|
||||
icon="delete"
|
||||
:label="$t('buttons.delete')"
|
||||
show="delete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import downloadFiles from "@/utils/download";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
import Action from "@/components/Action.vue";
|
||||
|
||||
export default {
|
||||
name: "ContextMenu",
|
||||
components: {
|
||||
Action,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
posX: 0,
|
||||
posY: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isMultiple() {
|
||||
return state.multiple;
|
||||
},
|
||||
user() {
|
||||
return state.user;
|
||||
},
|
||||
isMobile() {
|
||||
return getters.isMobile();
|
||||
},
|
||||
showContext() {
|
||||
if (getters.currentPromptName() == "ContextMenu" && state.prompts != []) {
|
||||
this.setPositions();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
top() {
|
||||
// Ensure the context menu stays within the viewport
|
||||
return Math.min(
|
||||
this.posY,
|
||||
|
||||
window.innerHeight - (this.$refs.contextMenu?.clientHeight ?? 0)
|
||||
);
|
||||
},
|
||||
left() {
|
||||
return Math.min(
|
||||
this.posX,
|
||||
|
||||
window.innerWidth - (this.$refs.contextMenu?.clientWidth ?? 0)
|
||||
);
|
||||
},
|
||||
isDarkMode() {
|
||||
return getters.isDarkMode();
|
||||
},
|
||||
headerButtons() {
|
||||
return {
|
||||
select: state.selected.length > 0,
|
||||
upload: state.user.perm?.create && state.selected.length > 0,
|
||||
download: state.user.perm.download && state.selected.length > 0,
|
||||
delete: state.selected.length > 0 && state.user.perm.delete,
|
||||
rename: state.selected.length === 1 && state.user.perm.rename,
|
||||
share: state.selected.length === 1 && state.user.perm.share,
|
||||
move: state.selected.length > 0 && state.user.perm.rename,
|
||||
copy: state.selected.length > 0 && state.user.perm?.create,
|
||||
};
|
||||
},
|
||||
selectedCount() {
|
||||
return getters.selectedCount();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
uploadFunc() {
|
||||
mutations.showHover("upload");
|
||||
},
|
||||
showHover(value) {
|
||||
return mutations.showHover(value);
|
||||
},
|
||||
setPositions() {
|
||||
const contextProps = getters.currentPrompt().props;
|
||||
this.posX = contextProps.posX;
|
||||
this.posY = contextProps.posY;
|
||||
},
|
||||
toggleMultipleSelection() {
|
||||
mutations.setMultiple(!state.multiple);
|
||||
mutations.closeHovers();
|
||||
},
|
||||
startDownload() {
|
||||
downloadFiles();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#context-menu {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
background-color: white;
|
||||
max-width: 20em;
|
||||
min-width: 15em;
|
||||
min-height: 4em;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#context-menu.mobile {
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.selected-count-header {
|
||||
border-radius: 0.5em;
|
||||
cursor: unset;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
#context-menu .action {
|
||||
width: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#context-menu > span {
|
||||
display: inline-block;
|
||||
margin-left: 1em;
|
||||
color: #6f6f6f;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
#context-menu .action span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* File selection */
|
||||
#context-menu.dark-mode {
|
||||
background: var(--surfaceSecondary) !important;
|
||||
}
|
||||
|
||||
#context-menu.dark-mode span {
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,168 +0,0 @@
|
||||
<template>
|
||||
<div v-if="selectedCount > 0" id="file-selection" :class="{ 'dark-mode': isDarkMode }">
|
||||
<span>{{ selectedCount }} selected</span>
|
||||
<div>
|
||||
<action
|
||||
v-if="headerButtons.select"
|
||||
icon="info"
|
||||
:label="$t('buttons.info')"
|
||||
show="info"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.select"
|
||||
icon="check_circle"
|
||||
:label="$t('buttons.selectMultiple')"
|
||||
@action="toggleMultipleSelection"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.download"
|
||||
icon="file_download"
|
||||
:label="$t('buttons.download')"
|
||||
@action="download"
|
||||
:counter="selectedCount"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.share"
|
||||
icon="share"
|
||||
:label="$t('buttons.share')"
|
||||
show="share"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.rename"
|
||||
icon="mode_edit"
|
||||
:label="$t('buttons.rename')"
|
||||
show="rename"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.copy"
|
||||
icon="content_copy"
|
||||
:label="$t('buttons.copyFile')"
|
||||
show="copy"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.move"
|
||||
icon="forward"
|
||||
:label="$t('buttons.moveFile')"
|
||||
show="move"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.delete"
|
||||
icon="delete"
|
||||
:label="$t('buttons.delete')"
|
||||
show="delete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
import { files as api } from "@/api";
|
||||
import Action from "@/components/header/Action.vue";
|
||||
|
||||
export default {
|
||||
name: "fileSelection",
|
||||
components: {
|
||||
Action,
|
||||
},
|
||||
computed: {
|
||||
isDarkMode() {
|
||||
return getters.isDarkMode();
|
||||
},
|
||||
headerButtons() {
|
||||
return {
|
||||
select: state.selected.length > 0,
|
||||
upload: state.user.perm?.create && state.selected.length > 0,
|
||||
download: state.user.perm.download && state.selected.length > 0,
|
||||
delete: state.selected.length > 0 && state.user.perm.delete,
|
||||
rename: state.selected.length === 1 && state.user.perm.rename,
|
||||
share: state.selected.length === 1 && state.user.perm.share,
|
||||
move: state.selected.length > 0 && state.user.perm.rename,
|
||||
copy: state.selected.length > 0 && state.user.perm?.create,
|
||||
};
|
||||
},
|
||||
selectedCount() {
|
||||
return getters.selectedCount();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleMultipleSelection() {
|
||||
mutations.setMultiple(!state.multiple);
|
||||
mutations.closeHovers();
|
||||
},
|
||||
download() {
|
||||
if (getters.isSingleFileSelected()) {
|
||||
api.download(null, getters.selectedDownloadUrl());
|
||||
return;
|
||||
}
|
||||
mutations.showHover({
|
||||
name: "download",
|
||||
confirm: (format) => {
|
||||
mutations.closeHovers();
|
||||
let files = [];
|
||||
if (state.selected.length > 0) {
|
||||
for (let i of state.selected) {
|
||||
files.push(state.req.items[i].url);
|
||||
}
|
||||
} else {
|
||||
files.push(state.route.path);
|
||||
}
|
||||
try {
|
||||
api.download(format, ...files);
|
||||
showSuccess("download started");
|
||||
} catch (e) {
|
||||
showError("error downloading", e);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
@media (min-width: 800px) {
|
||||
#file-selection {
|
||||
bottom: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
#file-selection .action {
|
||||
border-radius: 50%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#file-selection > span {
|
||||
display: inline-block;
|
||||
margin-left: 1em;
|
||||
color: #6f6f6f;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
#file-selection .action span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* File Selection */
|
||||
#file-selection {
|
||||
box-shadow: rgba(0, 0, 0, 0.3) 0px 2em 50px 10px;
|
||||
position: fixed;
|
||||
bottom: 4em;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
max-width: 30em;
|
||||
z-index: 3;
|
||||
border-radius: 1em;
|
||||
display: flex;
|
||||
width: 90%;
|
||||
}
|
||||
/* File selection */
|
||||
#file-selection.dark-mode {
|
||||
background: var(--surfaceSecondary) !important;
|
||||
}
|
||||
|
||||
#file-selection.dark-mode span {
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -16,6 +16,7 @@
|
||||
<i v-else class="material-icons">search</i>
|
||||
<!-- Input field for search -->
|
||||
<input
|
||||
id="main-input"
|
||||
class="main-input"
|
||||
type="text"
|
||||
@keyup.exact="keyup"
|
||||
@@ -165,10 +166,6 @@
|
||||
<b>Multiple Search terms:</b> Additional terms separated by <code>|</code>,
|
||||
for example <code>"test|not"</code> searches for both terms independently.
|
||||
</p>
|
||||
<p>
|
||||
<b>File size:</b> Searching files by size may have significantly longer search
|
||||
times.
|
||||
</p>
|
||||
</div>
|
||||
<!-- List of search results -->
|
||||
<ul v-show="results.length > 0">
|
||||
@@ -194,7 +191,6 @@
|
||||
import ButtonGroup from "./ButtonGroup.vue";
|
||||
import { search } from "@/api";
|
||||
import { getters, mutations, state } from "@/store";
|
||||
import { showError } from "@/notify";
|
||||
|
||||
var boxes = {
|
||||
folder: { label: "folders", icon: "folder" },
|
||||
@@ -248,13 +244,18 @@ export default {
|
||||
this.submit();
|
||||
},
|
||||
active(active) {
|
||||
// this is hear to allow for animation
|
||||
const resultList = document.getElementById("result-list");
|
||||
if (!active) {
|
||||
resultList.classList.remove("active");
|
||||
this.value = "";
|
||||
event.stopPropagation();
|
||||
mutations.closeHovers();
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
resultList.classList.add("active");
|
||||
document.getElementById("main-input").focus();
|
||||
}, 100);
|
||||
},
|
||||
value() {
|
||||
@@ -306,6 +307,9 @@ export default {
|
||||
path = path.slice(1);
|
||||
path = "./" + path.substring(path.indexOf("/") + 1);
|
||||
path = path.replace(/\/+$/, "") + "/";
|
||||
if (path == "./files/") {
|
||||
path = "./";
|
||||
}
|
||||
return path;
|
||||
},
|
||||
},
|
||||
@@ -386,19 +390,17 @@ export default {
|
||||
return;
|
||||
}
|
||||
let searchTypesFull = this.searchTypes;
|
||||
if (this.largerThan != "") {
|
||||
if (this.largerThan != "" && !this.isTypeSelectDisabled) {
|
||||
searchTypesFull = searchTypesFull + "type:largerThan=" + this.largerThan + " ";
|
||||
}
|
||||
if (this.smallerThan != "") {
|
||||
if (this.smallerThan != "" && !this.isTypeSelectDisabled) {
|
||||
searchTypesFull = searchTypesFull + "type:smallerThan=" + this.smallerThan + " ";
|
||||
}
|
||||
let path = state.route.path;
|
||||
this.ongoing = true;
|
||||
try {
|
||||
this.results = await search(path, searchTypesFull + this.value);
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
}
|
||||
|
||||
this.results = await search(path, searchTypesFull + this.value);
|
||||
|
||||
this.ongoing = false;
|
||||
if (this.results.length == 0) {
|
||||
this.noneMessage = "No results found in indexed search.";
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import { state, mutations } from "@/store";
|
||||
import throttle from "@/utils/throttle";
|
||||
import { showError } from "@/notify";
|
||||
import { notify } from "@/notify";
|
||||
export default {
|
||||
props: {
|
||||
src: String,
|
||||
@@ -131,8 +131,7 @@ export default {
|
||||
imgex.onload = () => URL.revokeObjectURL(imgex.src); // Clean up URL object after loading
|
||||
}
|
||||
} catch (error) {
|
||||
showError("Error decoding TIFF");
|
||||
console.error("Error decoding TIFF:", error);
|
||||
notify.showError("Error decoding TIFF");
|
||||
}
|
||||
},
|
||||
onMouseUp() {
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{ activebutton: this.isMaximized && this.isSelected }"
|
||||
class="item"
|
||||
<component
|
||||
:is="quickNav ? 'a' : 'div'"
|
||||
:href="quickNav ? url : undefined"
|
||||
:class="{
|
||||
item: true,
|
||||
activebutton: isMaximized && isSelected,
|
||||
}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:draggable="isDraggable"
|
||||
@dragstart="dragStart"
|
||||
@dragover="dragOver"
|
||||
@drop="drop"
|
||||
@click="itemClick"
|
||||
:data-dir="isDir"
|
||||
:data-type="type"
|
||||
:aria-label="name"
|
||||
:aria-selected="isSelected"
|
||||
@click="quickNav ? toggleClick() : itemClick($event)"
|
||||
>
|
||||
<div
|
||||
@click="toggleClick"
|
||||
:class="{ activetitle: this.isMaximized && this.isSelected }"
|
||||
>
|
||||
<div @click="toggleClick" :class="{ activetitle: isMaximized && isSelected }">
|
||||
<img
|
||||
v-if="readOnly === undefined && type === 'image' && isThumbsEnabled && isInView"
|
||||
v-lazy="thumbnailUrl"
|
||||
:class="{ activeimg: this.isMaximized && this.isSelected }"
|
||||
:class="{ activeimg: isMaximized && isSelected }"
|
||||
ref="thumbnail"
|
||||
/>
|
||||
<i
|
||||
:class="{ iconActive: this.isMaximized && this.isSelected }"
|
||||
:class="{ iconActive: isMaximized && isSelected }"
|
||||
v-else
|
||||
class="material-icons"
|
||||
></i>
|
||||
</div>
|
||||
|
||||
<div class="text" :class="{ activecontent: this.isMaximized && this.isSelected }">
|
||||
<div class="text" :class="{ activecontent: isMaximized && isSelected }">
|
||||
<p class="name">{{ name }}</p>
|
||||
<p v-if="isDir" class="size" data-order="-1">—</p>
|
||||
<p v-else class="size" :data-order="humanSize()">{{ humanSize() }}</p>
|
||||
<p class="size" :data-order="humanSize()">{{ humanSize() }}</p>
|
||||
<p class="modified">
|
||||
<time :datetime="modified">{{ humanTime() }}</time>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@@ -73,7 +73,7 @@ import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
|
||||
export default {
|
||||
name: "item",
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
isThumbnailInView: false,
|
||||
isMaximized: false,
|
||||
@@ -92,33 +92,21 @@ export default {
|
||||
"path",
|
||||
],
|
||||
computed: {
|
||||
quickNav() {
|
||||
return state.user.singleClick && !state.multiple;
|
||||
},
|
||||
user() {
|
||||
return state.user;
|
||||
},
|
||||
selected() {
|
||||
return state.selected;
|
||||
},
|
||||
req() {
|
||||
return state.req;
|
||||
},
|
||||
jwt() {
|
||||
return state.jwt;
|
||||
},
|
||||
selectedCount() {
|
||||
return getters.selectedCount();
|
||||
},
|
||||
isClicked() {
|
||||
if (state.user.singleClick || !this.allowedView) {
|
||||
return false;
|
||||
}
|
||||
return !this.isMaximized;
|
||||
},
|
||||
allowedView() {
|
||||
return state.user.viewMode != "gallery" && state.user.viewMode != "normal";
|
||||
},
|
||||
singleClick() {
|
||||
return this.readOnly == undefined && state.user.singleClick;
|
||||
},
|
||||
isSelected() {
|
||||
return this.selected.indexOf(this.index) !== -1;
|
||||
},
|
||||
@@ -181,18 +169,18 @@ export default {
|
||||
toggleClick() {
|
||||
this.isMaximized = this.isClicked;
|
||||
},
|
||||
humanSize: function () {
|
||||
humanSize() {
|
||||
return this.type == "invalid_link"
|
||||
? "invalid link"
|
||||
: getHumanReadableFilesize(this.size);
|
||||
},
|
||||
humanTime: function () {
|
||||
humanTime() {
|
||||
if (this.readOnly == undefined && state.user.dateFormat) {
|
||||
return fromNow(this.modified, state.user.locale).format("L LT");
|
||||
}
|
||||
return fromNow(this.modified, state.user.locale);
|
||||
},
|
||||
dragStart: function () {
|
||||
dragStart() {
|
||||
if (getters.selectedCount() === 0) {
|
||||
mutations.addSelected(this.index);
|
||||
return;
|
||||
@@ -203,7 +191,7 @@ export default {
|
||||
mutations.addSelected(this.index);
|
||||
}
|
||||
},
|
||||
dragOver: function (event) {
|
||||
dragOver(event) {
|
||||
if (!this.canDrop) return;
|
||||
|
||||
event.preventDefault();
|
||||
@@ -217,7 +205,7 @@ export default {
|
||||
|
||||
el.style.opacity = 1;
|
||||
},
|
||||
drop: async function (event) {
|
||||
async drop(event) {
|
||||
if (!this.canDrop) return;
|
||||
event.preventDefault();
|
||||
|
||||
@@ -276,16 +264,17 @@ export default {
|
||||
|
||||
action(overwrite, rename);
|
||||
},
|
||||
itemClick: function (event) {
|
||||
itemClick(event) {
|
||||
console.log("should say something");
|
||||
if (this.singleClick && !state.multiple) this.open();
|
||||
else this.click(event);
|
||||
},
|
||||
click: function (event) {
|
||||
click(event) {
|
||||
if (!this.singleClick && getters.selectedCount() !== 0) event.preventDefault();
|
||||
|
||||
setTimeout(() => {
|
||||
this.touches = 0;
|
||||
}, 300);
|
||||
}, 500);
|
||||
|
||||
this.touches++;
|
||||
if (this.touches > 1) {
|
||||
@@ -321,7 +310,7 @@ export default {
|
||||
mutations.resetSelected();
|
||||
mutations.addSelected(this.index);
|
||||
},
|
||||
open: function () {
|
||||
open() {
|
||||
this.$router.push({ path: this.url });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -52,7 +52,7 @@ import FileList from "./FileList.vue";
|
||||
import { files as api } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
import { showError } from "@/notify";
|
||||
import { notify } from "@/notify";
|
||||
|
||||
export default {
|
||||
name: "copy",
|
||||
@@ -102,7 +102,7 @@ export default {
|
||||
})
|
||||
.catch((e) => {
|
||||
buttons.done("copy");
|
||||
showError(e);
|
||||
notify.showError(e);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
import { files as api } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import { state, getters, mutations } from "@/store";
|
||||
import { showError,showSuccess } from "@/notify";
|
||||
import { notify } from "@/notify";
|
||||
|
||||
export default {
|
||||
name: "delete",
|
||||
@@ -59,7 +59,7 @@ export default {
|
||||
if (!this.isListing) {
|
||||
await api.remove(state.route.path);
|
||||
buttons.success("delete");
|
||||
showSuccess("Deleted item successfully")
|
||||
showSuccess("Deleted item successfully");
|
||||
|
||||
this.currentPrompt?.confirm();
|
||||
this.closeHovers();
|
||||
@@ -79,11 +79,11 @@ export default {
|
||||
|
||||
await Promise.all(promises);
|
||||
buttons.success("delete");
|
||||
showSuccess("Deleted item successfully")
|
||||
showSuccess("Deleted item successfully");
|
||||
mutations.setReload(true); // Handle reload as needed
|
||||
} catch (e) {
|
||||
buttons.done("delete");
|
||||
showError(e);
|
||||
notify.showError(e);
|
||||
if (this.isListing) mutations.setReload(true); // Handle reload as needed
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import { users as api } from "@/api";
|
||||
import { showSuccess,showError } from "@/notify";
|
||||
import { notify } from "@/notify";
|
||||
import buttons from "@/utils/buttons";
|
||||
import { state, mutations, getters } from "@/store";
|
||||
|
||||
@@ -40,12 +40,12 @@ export default {
|
||||
event.preventDefault();
|
||||
try {
|
||||
await api.remove(this.user.id);
|
||||
this.$router.push({ path: "/settings/users" });
|
||||
showSuccess(this.$t("settings.userDeleted"));
|
||||
this.$router.push({ path: "/settings",hash:"#users-main" });
|
||||
notify.showSuccess(this.$t("settings.userDeleted"));
|
||||
} catch (e) {
|
||||
e.message === "403"
|
||||
? showError(this.$t("errors.forbidden"), false)
|
||||
: showError(e);
|
||||
? notify.showError(this.$t("errors.forbidden"), false)
|
||||
: notify.showError(e);
|
||||
}
|
||||
},
|
||||
closeHovers() {
|
||||
@@ -80,7 +80,7 @@ export default {
|
||||
mutations.setReload(true); // Handle reload as needed
|
||||
} catch (e) {
|
||||
buttons.done("delete");
|
||||
showError(e);
|
||||
notify.showError(e);
|
||||
if (this.isListing) mutations.setReload(true); // Handle reload as needed
|
||||
}
|
||||
},
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
import { state, mutations } from "@/store";
|
||||
import url from "@/utils/url";
|
||||
import { files } from "@/api";
|
||||
import { showError } from "@/notify";
|
||||
|
||||
export default {
|
||||
name: "file-list",
|
||||
@@ -86,7 +85,7 @@ export default {
|
||||
// content.
|
||||
let uri = event.currentTarget.dataset.url;
|
||||
|
||||
files.fetch(uri).then(this.fillOptions).catch(showError);
|
||||
files.fetch(uri).then(this.fillOptions);
|
||||
},
|
||||
touchstart(event) {
|
||||
let url = event.currentTarget.dataset.url;
|
||||
|
||||
@@ -75,7 +75,6 @@ import { getHumanReadableFilesize } from "@/utils/filesizes";
|
||||
import { formatTimestamp } from "@/utils/moment";
|
||||
import { files as api } from "@/api";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
import { showError } from "@/notify";
|
||||
|
||||
export default {
|
||||
name: "info",
|
||||
@@ -146,12 +145,8 @@ export default {
|
||||
link = state.route.path;
|
||||
}
|
||||
|
||||
try {
|
||||
const hash = await api.checksum(link, algo);
|
||||
event.target.innerHTML = hash;
|
||||
} catch (e) {
|
||||
showError(e);
|
||||
}
|
||||
const hash = await api.checksum(link, algo);
|
||||
event.target.innerHTML = hash;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -52,7 +52,7 @@ import FileList from "./FileList.vue";
|
||||
import { files as api } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
import { showError } from "@/notify";
|
||||
import { notify } from "@/notify";
|
||||
|
||||
export default {
|
||||
name: "move",
|
||||
@@ -95,7 +95,7 @@ export default {
|
||||
})
|
||||
.catch((e) => {
|
||||
buttons.done("move");
|
||||
showError(e);
|
||||
notify.showError(e);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -121,7 +121,6 @@ export default {
|
||||
}
|
||||
|
||||
action(overwrite, rename);
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
import { files as api } from "@/api";
|
||||
import url from "@/utils/url";
|
||||
import { getters, mutations, state } from "@/store"; // Import your custom store
|
||||
import { showError } from "@/notify";
|
||||
|
||||
export default {
|
||||
name: "new-dir",
|
||||
@@ -87,16 +86,12 @@ export default {
|
||||
uri += encodeURIComponent(this.name) + "/";
|
||||
uri = uri.replace("//", "/");
|
||||
|
||||
try {
|
||||
await api.post(uri);
|
||||
if (this.redirect) {
|
||||
this.$router.push({ path: uri });
|
||||
} else if (!this.base) {
|
||||
const res = await api.fetch(url.removeLastDir(uri) + "/");
|
||||
mutations.updateRequest(res);
|
||||
}
|
||||
} catch (e) {
|
||||
showError(e);
|
||||
await api.post(uri);
|
||||
if (this.redirect) {
|
||||
this.$router.push({ path: uri });
|
||||
} else if (!this.base) {
|
||||
const res = await api.fetch(url.removeLastDir(uri) + "/");
|
||||
mutations.updateRequest(res);
|
||||
}
|
||||
|
||||
mutations.closeHovers();
|
||||
|
||||
@@ -73,12 +73,8 @@ export default {
|
||||
uri += encodeURIComponent(this.name);
|
||||
uri = uri.replace("//", "/");
|
||||
|
||||
try {
|
||||
await api.post(uri);
|
||||
this.$router.push({ path: uri });
|
||||
} catch (e) {
|
||||
showError(e);
|
||||
}
|
||||
await api.post(uri);
|
||||
this.$router.push({ path: uri });
|
||||
|
||||
mutations.closeHovers();
|
||||
},
|
||||
|
||||
@@ -98,18 +98,14 @@ export default {
|
||||
|
||||
newLink = url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
|
||||
|
||||
try {
|
||||
await api.move([{ from: oldLink, to: newLink }]);
|
||||
if (!this.isListing) {
|
||||
this.$router.push({ path: newLink });
|
||||
return;
|
||||
}
|
||||
|
||||
mutations.setReload(true);
|
||||
} catch (e) {
|
||||
showError(e);
|
||||
await api.move([{ from: oldLink, to: newLink }]);
|
||||
if (!this.isListing) {
|
||||
this.$router.push({ path: newLink });
|
||||
return;
|
||||
}
|
||||
|
||||
mutations.setReload(true);
|
||||
|
||||
mutations.closeHovers();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { showSuccess, showError } from "@/notify";
|
||||
import { notify } from "@/notify";
|
||||
import { state, getters, mutations } from "@/store";
|
||||
import { share as api, pub as pub_api } from "@/api";
|
||||
import { fromNow } from "@/utils/moment";
|
||||
@@ -173,22 +173,18 @@ export default {
|
||||
},
|
||||
},
|
||||
async beforeMount() {
|
||||
try {
|
||||
const links = await api.get(this.url);
|
||||
this.links = links;
|
||||
this.sort();
|
||||
const links = await api.get(this.url);
|
||||
this.links = links;
|
||||
this.sort();
|
||||
|
||||
if (this.links.length === 0) {
|
||||
this.listing = false;
|
||||
}
|
||||
} catch (e) {
|
||||
showError(e);
|
||||
if (this.links.length === 0) {
|
||||
this.listing = false;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.clip = new Clipboard(".copy-clipboard");
|
||||
this.clip.on("success", () => {
|
||||
showSuccess(this.$t("success.linkCopied"));
|
||||
notify.showSuccess(this.$t("success.linkCopied"));
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
@@ -198,38 +194,30 @@ export default {
|
||||
async submit() {
|
||||
let isPermanent = !this.time || this.time === 0;
|
||||
|
||||
try {
|
||||
let res = null;
|
||||
let res = null;
|
||||
|
||||
if (isPermanent) {
|
||||
res = await api.create(this.url, this.password);
|
||||
} else {
|
||||
res = await api.create(this.url, this.password, this.time, this.unit);
|
||||
}
|
||||
|
||||
this.links.push(res);
|
||||
this.sort();
|
||||
|
||||
this.time = "";
|
||||
this.unit = "hours";
|
||||
this.password = "";
|
||||
|
||||
this.listing = true;
|
||||
} catch (e) {
|
||||
showError(e);
|
||||
if (isPermanent) {
|
||||
res = await api.create(this.url, this.password);
|
||||
} else {
|
||||
res = await api.create(this.url, this.password, this.time, this.unit);
|
||||
}
|
||||
|
||||
this.links.push(res);
|
||||
this.sort();
|
||||
|
||||
this.time = "";
|
||||
this.unit = "hours";
|
||||
this.password = "";
|
||||
|
||||
this.listing = true;
|
||||
},
|
||||
async deleteLink(event, link) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
await api.remove(link.hash);
|
||||
this.links = this.links.filter((item) => item.hash !== link.hash);
|
||||
await api.remove(link.hash);
|
||||
this.links = this.links.filter((item) => item.hash !== link.hash);
|
||||
|
||||
if (this.links.length === 0) {
|
||||
this.listing = false;
|
||||
}
|
||||
} catch (e) {
|
||||
showError(e);
|
||||
if (this.links.length === 0) {
|
||||
this.listing = false;
|
||||
}
|
||||
},
|
||||
humanTime(time) {
|
||||
|
||||
@@ -68,7 +68,7 @@ export default {
|
||||
handleFiles(event);
|
||||
};
|
||||
|
||||
const handleFiles = (event) => {
|
||||
const handleFiles = async (event) => {
|
||||
mutations.closeHovers();
|
||||
const files = event.target.files;
|
||||
if (!files) return;
|
||||
@@ -94,21 +94,20 @@ export default {
|
||||
if (conflict) {
|
||||
mutations.showHover({
|
||||
name: "replace",
|
||||
action: (event) => {
|
||||
action: async (event) => {
|
||||
event.preventDefault();
|
||||
mutations.closeHovers();
|
||||
upload.handleFiles(uploadFiles, path, false);
|
||||
await upload.handleFiles(uploadFiles, path, false);
|
||||
},
|
||||
confirm: (event) => {
|
||||
confirm: async (event) => {
|
||||
event.preventDefault();
|
||||
mutations.closeHovers();
|
||||
upload.handleFiles(uploadFiles, path, true);
|
||||
await upload.handleFiles(uploadFiles, path, true);
|
||||
},
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
await upload.handleFiles(uploadFiles, path, true);
|
||||
}
|
||||
|
||||
upload.handleFiles(uploadFiles, path, true);
|
||||
mutations.setReload(true);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
type="text"
|
||||
v-model="user.username"
|
||||
id="username"
|
||||
@input="emitUpdate"
|
||||
/>
|
||||
</p>
|
||||
|
||||
@@ -18,22 +19,24 @@
|
||||
:placeholder="passwordPlaceholder"
|
||||
v-model="user.password"
|
||||
id="password"
|
||||
@input="emitUpdate"
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="scope">{{ $t("settings.scope") }}</label>
|
||||
<input
|
||||
:disabled="createUserDirData"
|
||||
:disabled="createUserDir"
|
||||
:placeholder="scopePlaceholder"
|
||||
class="input input--block"
|
||||
type="text"
|
||||
v-model="user.scope"
|
||||
id="scope"
|
||||
@input="emitUpdate"
|
||||
/>
|
||||
</p>
|
||||
<p class="small" v-if="displayHomeDirectoryCheckbox">
|
||||
<input type="checkbox" v-model="createUserDirData" />
|
||||
<input type="checkbox" v-model="createUserDir" />
|
||||
{{ $t("settings.createUserHomeDirectory") }}
|
||||
</p>
|
||||
|
||||
@@ -43,31 +46,32 @@
|
||||
class="input input--block"
|
||||
id="locale"
|
||||
v-model:locale="user.locale"
|
||||
@input="emitUpdate"
|
||||
></languages>
|
||||
</p>
|
||||
|
||||
<p v-if="!isDefault">
|
||||
<input
|
||||
type="checkbox"
|
||||
:disabled="user.perm.admin"
|
||||
:disabled="user.perm?.admin"
|
||||
v-model="user.lockPassword"
|
||||
@input="emitUpdate"
|
||||
/>
|
||||
{{ $t("settings.lockPassword") }}
|
||||
</p>
|
||||
|
||||
<permissions :perm="user.perm" />
|
||||
<permissions :perm="localUser.perm" />
|
||||
<commands v-if="isExecEnabled" v-model:commands="user.commands" />
|
||||
|
||||
<div v-if="!isDefault">
|
||||
<h3>{{ $t("settings.rules") }}</h3>
|
||||
<p class="small">{{ $t("settings.rulesHelp") }}</p>
|
||||
<rules v-model:rules="user.rules" />
|
||||
<rules v-model:rules="user.rules" @input="emitUpdate" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { state } from "@/store"
|
||||
import Languages from "./Languages.vue";
|
||||
import Rules from "./Rules.vue";
|
||||
import Permissions from "./Permissions.vue";
|
||||
@@ -75,35 +79,51 @@ import Commands from "./Commands.vue";
|
||||
import { enableExec } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "user",
|
||||
data() {
|
||||
return {
|
||||
createUserDirData: false,
|
||||
originalUserScope: "/",
|
||||
};
|
||||
},
|
||||
name: "UserForm",
|
||||
components: {
|
||||
Permissions,
|
||||
Languages,
|
||||
Rules,
|
||||
Commands,
|
||||
},
|
||||
props: [ "createUserDir", "isNew", "isDefault"],
|
||||
created() {
|
||||
this.originalUserScope = state.user.scope;
|
||||
this.createUserDirData = this.createUserDir;
|
||||
data() {
|
||||
return {
|
||||
createUserDir: false,
|
||||
originalUserScope: ".",
|
||||
localUser: { ...this.user },
|
||||
};
|
||||
},
|
||||
props: {
|
||||
user: Object, // Define user as a prop
|
||||
isDefault: Boolean,
|
||||
isNew: Boolean,
|
||||
},
|
||||
watch: {
|
||||
user: {
|
||||
handler(newUser) {
|
||||
console.log("UserForm: user changed", newUser);
|
||||
this.localUser = { ...newUser }; // Watch for changes in the parent and update the local copy
|
||||
},
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
"user.perm.admin": function (newValue) {
|
||||
if (newValue) {
|
||||
this.user.lockPassword = false;
|
||||
}
|
||||
},
|
||||
createUserDir(newVal) {
|
||||
this.user.scope = newVal ? "" : this.originalUserScope;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
user() {
|
||||
return state.user;
|
||||
},
|
||||
passwordPlaceholder() {
|
||||
return this.isNew ? "" : this.$t("settings.avoidChanges");
|
||||
},
|
||||
scopePlaceholder() {
|
||||
return this.createUserDir
|
||||
? this.$t("settings.userScopeGenerationPlaceholder")
|
||||
: "";
|
||||
: "./";
|
||||
},
|
||||
displayHomeDirectoryCheckbox() {
|
||||
return this.isNew && this.createUserDir;
|
||||
@@ -112,10 +132,5 @@ export default {
|
||||
return enableExec; // Removed arrow function
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
createUserDirData(newVal) {
|
||||
state.user.scope = newVal ? "" : this.originalUserScope;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
@@ -44,35 +44,6 @@
|
||||
|
||||
<!-- Section for logged-in users -->
|
||||
<div v-if="isLoggedIn" class="sidebar-scroll-list">
|
||||
<!-- Buttons visible if user has create permission -->
|
||||
<div v-if="user.perm?.create">
|
||||
<!-- New Folder button -->
|
||||
<button
|
||||
@click="showHover('newDir')"
|
||||
class="action"
|
||||
:aria-label="$t('sidebar.newFolder')"
|
||||
:title="$t('sidebar.newFolder')"
|
||||
>
|
||||
<i class="material-icons">create_new_folder</i>
|
||||
<span>{{ $t("sidebar.newFolder") }}</span>
|
||||
</button>
|
||||
<!-- New File button -->
|
||||
<button
|
||||
@click="showHover('newFile')"
|
||||
class="action"
|
||||
:aria-label="$t('sidebar.newFile')"
|
||||
:title="$t('sidebar.newFile')"
|
||||
>
|
||||
<i class="material-icons">note_add</i>
|
||||
<span>{{ $t("sidebar.newFile") }}</span>
|
||||
</button>
|
||||
<!-- Upload button -->
|
||||
<button id="upload-button" @click="uploadFunc" class="action">
|
||||
<i class="material-icons">file_upload</i>
|
||||
<span>Upload file</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoggedIn" class="sources card">
|
||||
<span>Sources</span>
|
||||
<div class="inner-card">
|
||||
@@ -138,7 +109,6 @@ import { files } from "@/api";
|
||||
import ProgressBar from "@/components/ProgressBar.vue";
|
||||
import { getHumanReadableFilesize } from "@/utils/filesizes";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
import { showError } from "@/notify";
|
||||
|
||||
export default {
|
||||
name: "SidebarGeneral",
|
||||
@@ -192,13 +162,13 @@ export default {
|
||||
this.hoverText = "Quick Toggles"; // Reset to default hover text
|
||||
},
|
||||
toggleClick() {
|
||||
mutations.updateUser({ singleClick: !state.user.singleClick });
|
||||
mutations.updateCurrentUser({ singleClick: !state.user.singleClick });
|
||||
},
|
||||
toggleDarkMode() {
|
||||
mutations.toggleDarkMode();
|
||||
},
|
||||
toggleSticky() {
|
||||
mutations.updateUser({ stickySidebar: !state.user.stickySidebar });
|
||||
mutations.updateCurrentUser({ stickySidebar: !state.user.stickySidebar });
|
||||
},
|
||||
async updateUsage() {
|
||||
if (!getters.isLoggedIn()) {
|
||||
@@ -209,21 +179,16 @@ export default {
|
||||
if (this.disableUsedPercentage) {
|
||||
return usageStats;
|
||||
}
|
||||
try {
|
||||
let usage = await files.usage(path);
|
||||
usageStats = {
|
||||
used: getHumanReadableFilesize(usage.used / 1024),
|
||||
total: getHumanReadableFilesize(usage.total / 1024),
|
||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
||||
};
|
||||
} catch (error) {
|
||||
showError("Error fetching usage", error);
|
||||
}
|
||||
let usage = await files.usage(path);
|
||||
usageStats = {
|
||||
used: getHumanReadableFilesize(usage.used / 1024),
|
||||
total: getHumanReadableFilesize(usage.total / 1024),
|
||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
||||
};
|
||||
|
||||
mutations.setUsage(usageStats);
|
||||
},
|
||||
showHover(value) {
|
||||
return mutations.showHover(value);
|
||||
},
|
||||
|
||||
navigateTo(path) {
|
||||
const hashIndex = path.indexOf("#");
|
||||
if (hashIndex !== -1) {
|
||||
@@ -241,9 +206,7 @@ export default {
|
||||
help() {
|
||||
mutations.showHover("help");
|
||||
},
|
||||
uploadFunc() {
|
||||
mutations.showHover("upload");
|
||||
},
|
||||
|
||||
// Logout the user
|
||||
logout: auth.logout,
|
||||
},
|
||||
|
||||
@@ -7,28 +7,38 @@
|
||||
@click="setView(setting.id + '-main')"
|
||||
:class="{ 'active-settings': active(setting.id + '-main') }"
|
||||
>
|
||||
<div class="card-wrapper">{{ setting.label }}</div>
|
||||
<div v-if="shouldShow(setting)" class="card-wrapper">{{ setting.label }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { state, getters, mutations } from "@/store";
|
||||
import { settings } from "@/utils/constants";
|
||||
import { router } from "@/router";
|
||||
|
||||
export default {
|
||||
name: "SidebarSettings",
|
||||
data() {
|
||||
return {
|
||||
settings // Initialize the settings array in data
|
||||
settings, // Initialize the settings array in data
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentHash: () => getters.currentHash(),
|
||||
},
|
||||
methods: {
|
||||
shouldShow(setting) {
|
||||
const perm = setting?.perm || {};
|
||||
// Check if all keys in setting.perm exist in state.user.perm and have truthy values
|
||||
return Object.keys(perm).every((key) => state.user.perm[key]);
|
||||
},
|
||||
active: (view) => state.activeSettingsView === view,
|
||||
setView(view) {
|
||||
mutations.setActiveSettingsView(view);
|
||||
if (state.route.path != "/settings") {
|
||||
router.push({ path: "/settings", hash: "#" + view }, () => {});
|
||||
} else {
|
||||
mutations.setActiveSettingsView(view);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<script>
|
||||
import { version, commitSHA } from "@/utils/constants";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
import { getters, mutations } from "@/store"; // Import your custom store
|
||||
import SidebarGeneral from "./General.vue";
|
||||
import SidebarSettings from "./Settings.vue";
|
||||
|
||||
@@ -43,6 +43,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
version: () => version,
|
||||
commitSHA: () => commitSHA,
|
||||
isDarkMode: () => getters.isDarkMode(),
|
||||
isLoggedIn: () => getters.isLoggedIn(),
|
||||
isSettings: () => getters.isSettings(),
|
||||
|
||||
@@ -92,7 +92,9 @@ main > div {
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
height: 3em;
|
||||
overflow-x: auto;
|
||||
height: auto;
|
||||
min-height: 3em;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -162,23 +164,25 @@ button:disabled {
|
||||
}
|
||||
|
||||
#popup-notification {
|
||||
color: white;
|
||||
border-radius: 1em;
|
||||
color: #fff;
|
||||
position: fixed;
|
||||
max-width: 90vw;
|
||||
height: 4em;
|
||||
bottom: 0;
|
||||
right: -20em; /* Start off-screen */
|
||||
right: -20em;
|
||||
display: flex;
|
||||
padding: 1em;
|
||||
padding: 0.5em;
|
||||
align-items: center;
|
||||
transition: right 1s ease; /* Animate the 'right' property */
|
||||
transition: right 1s ease;
|
||||
z-index: 5;
|
||||
margin: 1em;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#popup-notification-content {
|
||||
color: white;
|
||||
padding: 0;
|
||||
padding-left: .5em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#popup-notification.success {
|
||||
|
||||
@@ -103,6 +103,10 @@
|
||||
border-color: var(--divider) !important;
|
||||
}
|
||||
|
||||
.dark-mode #listingView.gallery .item .text {
|
||||
text-shadow: 0 0 2px black;
|
||||
}
|
||||
|
||||
/* Listing item modified text */
|
||||
.dark-mode #listingView .item .modified {
|
||||
color: var(--textSecondary);
|
||||
|
||||
@@ -36,6 +36,7 @@ body.rtl #listingView {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
#listingView .item div:last-of-type {
|
||||
@@ -140,7 +141,7 @@ body.rtl #listingView {
|
||||
display:flex;
|
||||
min-width: 12em;
|
||||
min-height: 12em;
|
||||
text-shadow: 0 0 2px black;
|
||||
text-shadow: 0 0 2px white;
|
||||
}
|
||||
|
||||
#listingView.gallery .item div:last-of-type {
|
||||
@@ -407,25 +408,3 @@ body.rtl #listingView {
|
||||
#listingView.list .header .active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#listingView #multiple-selection {
|
||||
position: fixed;
|
||||
bottom: -4em;
|
||||
left: 0;
|
||||
z-index: 99999;
|
||||
width: 100%;
|
||||
background-color: var(--blue);
|
||||
height: 4em;
|
||||
padding: 0.5em 0.5em 0.5em 1em;
|
||||
justify-content: space-between;
|
||||
transition: .2s ease bottom;
|
||||
}
|
||||
|
||||
#listingView #multiple-selection.active {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#listingView #multiple-selection p,
|
||||
#listingView #multiple-selection i {
|
||||
color: var(--item-selected);
|
||||
}
|
||||
@@ -43,6 +43,8 @@
|
||||
padding: .5em;
|
||||
text-align: center;
|
||||
animation: .2s opac forwards;
|
||||
margin-bottom: 0.5em;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
@keyframes opac {
|
||||
|
||||
@@ -174,6 +174,7 @@
|
||||
"video": "Video"
|
||||
},
|
||||
"settings": {
|
||||
"UserManagement": "User Management",
|
||||
"admin": "Admin",
|
||||
"administrator": "Administrator",
|
||||
"allowCommands": "Execute commands",
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
"avoidChanges": "(değişiklikleri önlemek için boş bırakın)",
|
||||
"branding": "Marka",
|
||||
"brandingDirectoryPath": "Marka dizin yolu",
|
||||
"brandingHelp": "Adını değiştirerek, logoyu değiştirerek, özel stiller ekleyerek ve hatta GitHub'a harici bağlantıları devre dışı bırakarak Filebrowser örneğinizin görünüşünü ve hissini özelleştirebilirsiniz.\nÖzel marka bilinci oluşturma hakkında daha fazla bilgi için lütfen {0} sayfasına göz atın.",
|
||||
"brandingHelp": "Adını değiştirerek, logoyu değiştirerek, özel stiller ekleyerek ve hatta GitHub'a harici bağlantıları devre dışı bırakarak FileBrowser örneğinizin görünüşünü ve hissini özelleştirebilirsiniz.\nÖzel marka bilinci oluşturma hakkında daha fazla bilgi için lütfen {0} sayfasına göz atın.",
|
||||
"changePassword": "Şifre Değiştir",
|
||||
"commandRunner": "Komut satırı",
|
||||
"commandRunnerHelp": "Burada, adlandırılmış olaylarda yürütülen komutları ayarlayabilirsiniz. Her satıra bir tane yazmalısınız. {0} ve {1} ortam değişkenleri, {1}'ye göre {0} olacak şekilde kullanılabilir olacaktır. Bu özellik ve mevcut ortam değişkenleri hakkında daha fazla bilgi için lütfen {2}'yi okuyun.",
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
|
||||
import { showSuccess, showError, closePopUp } from "./message.js";
|
||||
import * as notify from "./message.js";
|
||||
export {
|
||||
showSuccess,
|
||||
showError,
|
||||
closePopUp,
|
||||
notify,
|
||||
};
|
||||
@@ -1,20 +1,35 @@
|
||||
import { mutations, state } from "@/store";
|
||||
|
||||
export function showPopup(type, message) {
|
||||
const [popup, popupContent] = getElements();
|
||||
if (popup == undefined) {
|
||||
return
|
||||
}
|
||||
popup.classList.remove('success', 'error'); // Clear previous types
|
||||
popup.classList.add(type);
|
||||
popupContent.textContent = message;
|
||||
|
||||
// Start animation: bring the popup into view
|
||||
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);
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
export function closePopUp() {
|
||||
const [popup, popupContent] = getElements();
|
||||
if (popupContent == undefined) {
|
||||
return
|
||||
}
|
||||
if (popupContent.textContent == "Multiple Selection Enabled" && state.multiple) {
|
||||
mutations.setMultiple(false)
|
||||
}
|
||||
popup.style.right = '-50em'; // Slide out
|
||||
popupContent.textContent = "no content";
|
||||
}
|
||||
@@ -22,14 +37,12 @@ export function closePopUp() {
|
||||
function getElements() {
|
||||
const popup = document.getElementById('popup-notification');
|
||||
if (!popup) {
|
||||
console.error('Popup notification element not found');
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
const popupContent = popup.querySelector('#popup-notification-content');
|
||||
if (!popupContent) {
|
||||
console.error('Popup notification content element not found');
|
||||
return [null, null];
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
return [popup, popupContent];
|
||||
@@ -42,4 +55,8 @@ export function showSuccess(message) {
|
||||
export function showError(message) {
|
||||
showPopup('error', message);
|
||||
console.error(message)
|
||||
}
|
||||
|
||||
export function showMultipleSelection() {
|
||||
showPopup("action","Multiple Selection Enabled");
|
||||
}
|
||||
@@ -70,6 +70,14 @@ const routes = [
|
||||
name: "Settings",
|
||||
component: Settings,
|
||||
},
|
||||
{
|
||||
path: "users/:id",
|
||||
name: "User",
|
||||
component: Settings,
|
||||
meta: {
|
||||
requiresAdmin: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { state } from "./state.js";
|
||||
|
||||
export const getters = {
|
||||
isResizableView: () => (state.user.viewMode == "gallery" || state.user.viewMode == "normal" ) && getters.currentView() == "listingView" ,
|
||||
isCardView: () => (state.user.viewMode == "gallery" || state.user.viewMode == "normal" ) && getters.currentView() == "listingView" ,
|
||||
currentHash: () => state.route.hash.replace("#", ""),
|
||||
isMobile: () => state.isMobile,
|
||||
isLoading: () => Object.keys(state.loading).length > 0,
|
||||
@@ -19,6 +19,7 @@ export const getters = {
|
||||
isFiles: () => state.route.name === "Files",
|
||||
isListing: () => getters.isFiles() && state.req.isDir,
|
||||
selectedCount: () => Array.isArray(state.selected) ? state.selected.length : 0,
|
||||
getFirstSelected: () => state.req.items[state.selected[0]],
|
||||
isSingleFileSelected: () => getters.selectedCount() === 1 && !state.req.items[state.selected[0]]?.isDir,
|
||||
selectedDownloadUrl() {
|
||||
let selectedItem = state.selected[0]
|
||||
@@ -77,7 +78,7 @@ export const getters = {
|
||||
return { dirs, files };
|
||||
},
|
||||
isSidebarVisible: () => {
|
||||
let visible = state.showSidebar || getters.isStickySidebar()
|
||||
let visible = (state.showSidebar || getters.isStickySidebar()) && state.user.username != "publicUser"
|
||||
if (getters.currentView() == "settings") {
|
||||
visible = !getters.isMobile();
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ import { state } from "./state.js";
|
||||
import router from "@/router";
|
||||
import { emitStateChanged } from './eventBus'; // Import the function from eventBus.js
|
||||
import { users } from "@/api";
|
||||
import { notify } from "@/notify";
|
||||
|
||||
export const mutations = {
|
||||
setGallerySize: (value) => {
|
||||
state.user.gallerySize = value
|
||||
emitStateChanged();
|
||||
users.update(state.user,['gallerySize']);
|
||||
},
|
||||
setActiveSettingsView: (value) => {
|
||||
state.activeSettingsView = value;
|
||||
@@ -31,13 +33,13 @@ export const mutations = {
|
||||
emitStateChanged();
|
||||
},
|
||||
toggleDarkMode() {
|
||||
mutations.updateUser({ "darkMode": !state.user.darkMode });
|
||||
mutations.updateCurrentUser({ "darkMode": !state.user.darkMode });
|
||||
emitStateChanged();
|
||||
},
|
||||
toggleSidebar() {
|
||||
if (state.user.stickySidebar) {
|
||||
localStorage.setItem("stickySidebar", "false");
|
||||
mutations.updateUser({ "stickySidebar": false }); // turn off sticky when closed
|
||||
mutations.updateCurrentUser({ "stickySidebar": false }); // turn off sticky when closed
|
||||
state.showSidebar = false;
|
||||
} else {
|
||||
state.showSidebar = !state.showSidebar;
|
||||
@@ -94,27 +96,23 @@ export const mutations = {
|
||||
state.loading = { ...state.loading, [loadType]: true };
|
||||
}
|
||||
emitStateChanged();
|
||||
},
|
||||
},
|
||||
setReload: (value) => {
|
||||
state.reload = value;
|
||||
emitStateChanged();
|
||||
},
|
||||
setUser: (value) => {
|
||||
if (value === null) {
|
||||
state.user = null;
|
||||
setCurrentUser: (value) => {
|
||||
state.user = value;
|
||||
// If value is null or undefined, emit state change and exit early
|
||||
if (!value) {
|
||||
emitStateChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
let locale = value.locale;
|
||||
if (locale === "") {
|
||||
value.locale = i18n.detectLocale();
|
||||
}
|
||||
let previousUser = state.user
|
||||
state.user = value;
|
||||
if (state.user != previousUser && state.user.username != "publicUser") {
|
||||
users.update(state.user);
|
||||
// Ensure locale exists and is valid
|
||||
if (!value.locale) {
|
||||
value.locale = i18n.detectLocale(); // Default to detected locale if missing
|
||||
}
|
||||
// Emit state change after setting the user and locale
|
||||
emitStateChanged();
|
||||
},
|
||||
setJWT: (value) => {
|
||||
@@ -127,6 +125,11 @@ export const mutations = {
|
||||
},
|
||||
setMultiple: (value) => {
|
||||
state.multiple = value;
|
||||
if (value == true) {
|
||||
notify.showMultipleSelection()
|
||||
} else {
|
||||
notify.closePopUp()
|
||||
}
|
||||
emitStateChanged();
|
||||
},
|
||||
addSelected: (value) => {
|
||||
@@ -144,22 +147,38 @@ export const mutations = {
|
||||
mutations.setMultiple(false);
|
||||
emitStateChanged();
|
||||
},
|
||||
updateUser: (value) => {
|
||||
if (typeof value !== "object") return;
|
||||
if (state.user === null) {
|
||||
updateCurrentUser: (value) => {
|
||||
// Ensure the input is a valid object
|
||||
if (typeof value !== "object" || value === null) return;
|
||||
|
||||
// Initialize state.user if it's null
|
||||
if (!state.user) {
|
||||
state.user = {};
|
||||
}
|
||||
let previousUser = state.user;
|
||||
|
||||
// Store previous state for comparison
|
||||
const previousUser = { ...state.user };
|
||||
|
||||
// Merge the new values into the current user state
|
||||
state.user = { ...state.user, ...value };
|
||||
|
||||
// Handle locale change
|
||||
if (state.user.locale !== previousUser.locale) {
|
||||
state.user.locale = i18n.detectLocale();
|
||||
i18n.setLocale(state.user.locale);
|
||||
i18n.default.locale = state.user.locale;
|
||||
}
|
||||
localStorage.setItem("stickySidebar", state.user.stickySidebar);
|
||||
if (state.user != previousUser) {
|
||||
users.update(state.user);
|
||||
|
||||
// Update localStorage if stickySidebar exists
|
||||
if ('stickySidebar' in state.user) {
|
||||
localStorage.setItem("stickySidebar", state.user.stickySidebar);
|
||||
}
|
||||
// Update users if there's any change in state.user
|
||||
if (JSON.stringify(state.user) !== JSON.stringify(previousUser)) {
|
||||
users.update(state.user,Object.keys(value));
|
||||
}
|
||||
|
||||
// Emit state change event
|
||||
emitStateChanged();
|
||||
},
|
||||
updateRequest: (value) => {
|
||||
@@ -177,19 +196,20 @@ export const mutations = {
|
||||
emitStateChanged();
|
||||
},
|
||||
setRoute: (value) => {
|
||||
console.log("going...",value)
|
||||
state.route = value;
|
||||
emitStateChanged();
|
||||
},
|
||||
updateListingSortConfig: ({ field, asc }) => {
|
||||
state.req.sorting.by = field;
|
||||
state.req.sorting.asc = asc;
|
||||
state.user.sorting.by = field;
|
||||
state.user.sorting.asc = asc;
|
||||
emitStateChanged();
|
||||
},
|
||||
updateListingItems: () => {
|
||||
state.req.items.sort((a, b) => {
|
||||
const valueA = a[state.req.sorting.by];
|
||||
const valueB = b[state.req.sorting.by];
|
||||
if (state.req.sorting.asc) {
|
||||
const valueA = a[state.user.sorting.by];
|
||||
const valueB = b[state.user.sorting.by];
|
||||
if (state.user.sorting.asc) {
|
||||
return valueA > valueB ? 1 : -1;
|
||||
} else {
|
||||
return valueA < valueB ? 1 : -1;
|
||||
|
||||
@@ -13,7 +13,7 @@ export function parseToken(token) {
|
||||
localStorage.setItem("jwt", token);
|
||||
mutations.setJWT(token);
|
||||
mutations.setSession(generateRandomCode(8));
|
||||
mutations.setUser(data.user);
|
||||
mutations.setCurrentUser(data.user);
|
||||
}
|
||||
|
||||
export async function validateLogin() {
|
||||
@@ -89,7 +89,7 @@ export async function signupLogin(username, password) {
|
||||
export function logout() {
|
||||
document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
|
||||
mutations.setJWT("");
|
||||
mutations.setUser(null);
|
||||
mutations.setCurrentUser(null);
|
||||
localStorage.setItem("jwt", null);
|
||||
router.push({ path: "/login" });
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
const name = window.FileBrowser.Name || "File Browser";
|
||||
const name = window.FileBrowser.Name || "FileBrowser Quantum";
|
||||
const disableExternal = window.FileBrowser.DisableExternal;
|
||||
const disableUsedPercentage = window.FileBrowser.DisableUsedPercentage;
|
||||
const baseURL = window.FileBrowser.BaseURL;
|
||||
@@ -20,9 +20,13 @@ const origin = window.location.origin;
|
||||
|
||||
const settings = [
|
||||
{ id: 'profile', label: 'Profile Management', component: 'ProfileSettings' },
|
||||
{ id: 'shares', label: 'Share Management', component: 'SharesSettings' },
|
||||
{ id: 'global', label: 'Global', component: 'GlobalSettings' },
|
||||
{ id: 'user-defaults', label: 'User Defaults', component: 'UserDefaultSettings' },
|
||||
{
|
||||
id: 'shares', label: 'Share Management', component: 'SharesSettings', perm: {
|
||||
share: true
|
||||
}
|
||||
},
|
||||
{ id: 'global', label: 'Global', component: 'GlobalSettings', perm: { admin: true } },
|
||||
{ id: 'users', label: 'User Management', component: 'UserManagement', perm: { admin: true } },
|
||||
]
|
||||
|
||||
export {
|
||||
|
||||
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user