v0.2.2 (#86)
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Graham Steffaniak <graham.steffaniak@autodesk.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
parent
8e4629a0c4
commit
e8091dcb24
|
@ -0,0 +1,76 @@
|
||||||
|
name: dev
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'dev_v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: 1.21.1
|
||||||
|
- run: cd backend && go test -race -v ./...
|
||||||
|
lint-backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: 1.21.1
|
||||||
|
- run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2
|
||||||
|
- run: cd backend && golangci-lint run
|
||||||
|
format-backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: 1.21.1
|
||||||
|
- run: cd backend && go fmt ./...
|
||||||
|
lint-frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
- run: cd frontend && npm i eslint
|
||||||
|
- run: cd frontend && npm run lint
|
||||||
|
push_dev_to_registry:
|
||||||
|
name: Push dev image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
|
# Workaround to fix error:
|
||||||
|
# failed to push: failed to copy: io: read/write on closed pipe
|
||||||
|
# See https://github.com/docker/build-push-action/issues/761
|
||||||
|
with:
|
||||||
|
driver-opts: |
|
||||||
|
image=moby/buildkit:v0.10.6
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||||
|
with:
|
||||||
|
images: gtstef/filebrowser
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
|
@ -4,6 +4,7 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- 'main'
|
- 'main'
|
||||||
|
- 'dev_v*'
|
||||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
@ -43,7 +44,6 @@ jobs:
|
||||||
- run: cd frontend && npm run lint
|
- run: cd frontend && npm run lint
|
||||||
|
|
||||||
push_pr_to_registry:
|
push_pr_to_registry:
|
||||||
needs: [lint-frontend, lint-backend, test-backend, format-backend]
|
|
||||||
name: Push PR
|
name: Push PR
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
|
@ -4,6 +4,7 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-backend:
|
test-backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
121
CHANGELOG.md
121
CHANGELOG.md
|
@ -1,69 +1,88 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
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.1
|
## v0.2.2
|
||||||
- issue #29 - Rules can now be configured and read from configuration yaml
|
|
||||||
- issue #28 - Allow disable settings per user.
|
|
||||||
- issue #27 - shorten download link for password protected files
|
|
||||||
- issue #26 - enable dark mode per user and improve switching performance.
|
|
||||||
- More rounded corners and improved listing styling
|
|
||||||
- improve search performance
|
|
||||||
- fixes authentication issues
|
|
||||||
- adds compact view mode
|
|
||||||
- improves view mode configuration and behavior
|
|
||||||
- updates configuration file to accept new settings
|
|
||||||
|
|
||||||
# v0.2.0
|
- **Major Indexing Changes:**
|
||||||
- improved UI
|
- **Speed:** (0m57s) - Decreased by 78% compared to the previous release.
|
||||||
- more unified coehisive look
|
- **Memory Usage:** (41MB) - Reduced by 45% compared to the previous release.
|
||||||
- Adjusted header bar look and icon behavior
|
- Now utilizes the index for file browser listings!
|
||||||
- The shell is dead.
|
- **[Work in Progress]** Hidden files are still directly accessible.
|
||||||
- If you need to use custom commands, exec into the docker container.
|
- **[Work in Progress]** Editor issues fixed on save and themes.
|
||||||
- The json config file is dead.
|
- **[Work in Progress]** `running-config.yaml` gets updated when settings change, ensuring that running settings are up to date.
|
||||||
- All configuration is done via advanced `filebrowser.yaml`
|
|
||||||
- The only flag that is allowed is flag to specify config file.
|
|
||||||
- Removed old code to migrate database versions
|
|
||||||
- Removed all unused cmd code
|
|
||||||
|
|
||||||
# v0.1.4
|
## v0.2.1
|
||||||
- various UI fixes
|
|
||||||
- Added download button back to toolbar
|
- Addressed issue #29 - Rules can now be configured and read from the configuration YAML.
|
||||||
- Added upload button to side menu
|
- Addressed issue #28 - Allows disabling settings per user.
|
||||||
- breadcrumb spacing fix
|
- Addressed issue #27 - Shortened download link for password-protected files.
|
||||||
- Added "compact" view option
|
- Addressed issue #26 - Enables dark mode per user and improves switching performance.
|
||||||
- fixed slash issue with css rtl logic
|
- Improved styling with more rounded corners and enhanced listing design.
|
||||||
- various backend fixes
|
- Enhanced search performance.
|
||||||
- search has a sessionId attached so searches don't collide
|
- Fixed authentication issues.
|
||||||
- search no longer searches by word with spaces, includes space in searches
|
- Added compact view mode.
|
||||||
- prepared for full json configuration
|
- Improved view mode configuration and behavior.
|
||||||
- made size search work for smaller and larger
|
- Updated the configuration file to accept new settings.
|
||||||
- made search types not show up in search bar when used
|
|
||||||
|
## v0.2.0
|
||||||
|
|
||||||
|
- **Improved UI:**
|
||||||
|
- Enhanced the cohesive and unified look.
|
||||||
|
- Adjusted the header bar appearance and icon behavior.
|
||||||
|
- The shell feature has been deprecated.
|
||||||
|
- Custom commands can be executed within the Docker container if needed.
|
||||||
|
- The JSON config file is no longer used.
|
||||||
|
- All configurations are now performed via the advanced `filebrowser.yaml`.
|
||||||
|
- The only allowed flag is specifying the config file.
|
||||||
|
- Removed old code for migrating database versions.
|
||||||
|
- Eliminated all unused `cmd` code.
|
||||||
|
|
||||||
|
## v0.1.4
|
||||||
|
|
||||||
|
- **Various UI fixes:**
|
||||||
|
- Reintroduced the download button to the toolbar.
|
||||||
|
- Added the upload button to the side menu.
|
||||||
|
- Adjusted breadcrumb spacing.
|
||||||
|
- Introduced a "compact" view option.
|
||||||
|
- Fixed a slash issue with CSS right-to-left (RTL) logic.
|
||||||
|
- **Various backend improvements:**
|
||||||
|
- Added session IDs to searches to prevent collisions.
|
||||||
|
- Modified search behavior to include spaces in searches.
|
||||||
|
- Prepared for full JSON configuration support.
|
||||||
|
- Made size-based searches work for both smaller and larger files.
|
||||||
|
- Modified search types not to appear in the search bar when used.
|
||||||
|
|
||||||
## v0.1.3
|
## v0.1.3
|
||||||
|
|
||||||
- improved styling, colors, transparency, blur
|
- Enhanced styling with improved colors, transparency, and blur effects.
|
||||||
- Made sidebar hidden on desktop as well
|
- Hid the sidebar on desktop views.
|
||||||
- simplified navbar to be three buttons
|
- Simplified the navbar to include three buttons:
|
||||||
- open menu
|
- Open menu
|
||||||
- search
|
- Search
|
||||||
- toggle view
|
- Toggle view
|
||||||
- Changed desktop search style and included additional search options.
|
- Revised desktop search style and included additional search options.
|
||||||
|
|
||||||
## v0.1.2
|
## v0.1.2
|
||||||
|
|
||||||
- Updated UI to use search features better
|
- Updated the UI to better utilize search features:
|
||||||
- More filter options
|
- Added more filter options.
|
||||||
- Better icons with colors
|
- Enhanced icons with colors.
|
||||||
- GUI styling
|
- Improved GUI styling.
|
||||||
- Improved search performance
|
- Improved search performance.
|
||||||
|
- **Index Changes:**
|
||||||
|
- **Speed:** (0m32s) - Increased by 6% compared to the previous release.
|
||||||
|
- **Memory Usage:** (93MB) - Increased by 3% compared to the previous release.
|
||||||
|
|
||||||
## v0.1.1
|
## v0.1.1
|
||||||
|
|
||||||
- Improved search with indexing
|
- Improved search functionality with indexing.
|
||||||
|
- **Index Changes (Baseline Results):**
|
||||||
|
- **Speed:** (0m30s)
|
||||||
|
- **Memory Usage:** (90MB)
|
||||||
|
|
||||||
## v0.1.0
|
## v0.1.0
|
||||||
|
|
||||||
- nothing changed from origin.
|
- No changes from the original.
|
||||||
|
|
||||||
Forked from https://github.com/filebrowser/filebrowser
|
Forked from [filebrowser/filebrowser](https://github.com/filebrowser/filebrowser).
|
||||||
|
|
42
README.md
42
README.md
|
@ -9,20 +9,24 @@
|
||||||
<img width="500" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/b45683b0-bd55-4430-9831-650fe0d21eb8" title="Main Screenshot">
|
<img width="500" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/b45683b0-bd55-4430-9831-650fe0d21eb8" title="Main Screenshot">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> **NOTE**
|
> [!NOTE]
|
||||||
Intended for docker use only
|
> Only intended to be used with docker.
|
||||||
|
|
||||||
> **Warning**
|
> [!WARNING]
|
||||||
Starting with v0.2.0, *ALL* configuration is done via `filebrowser.yaml` configuration file. `.filebrowser.json` and any flags other than `-c` and `-config` during execution WILL NO LONGER WORK. This is by design, in order to use the v0.2.0 You can mount your directory and initialize a new DB with a new default `filebrowser.yaml` which you can tweak and use in the future. Or you can copy and paste the default startup `filebrowser.yaml` below.
|
> Starting with v0.2.0, *ALL* configuration is done via `filebrowser.yaml` configuration file.
|
||||||
|
|
||||||
This fork makes the following significant changes to filebrowser for origin:
|
This fork makes the following significant changes to filebrowser for origin:
|
||||||
|
|
||||||
1. [x] Improves search to use index instead of filesystem.
|
1. [x] Better search.
|
||||||
- Lightning fast, realtime results as you type
|
- Lightning fast
|
||||||
|
- realtime results as you type
|
||||||
- Works with more type filters
|
- Works with more type filters
|
||||||
1. [x] Improved and simplified GUI navbar and sidebar menu.
|
- interactive results page.
|
||||||
1. [x] Updated version and dependencies.
|
1. [x] Revamped and simplified GUI navbar and sidebar menu.
|
||||||
1. [x] **IMPORTANT** Moved all configurations to `filebrowser.yaml`.
|
1. [x] **IMPORTANT** Revamped configuration via `filebrowser.yml` config file.
|
||||||
|
1. [x] More configurations possible at a per-user level
|
||||||
|
- <img width="450" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/625bd7c4-5ee9-4011-aaae-2a388ab0813b">
|
||||||
|
1. [x] Additional compact view mode as well as refreshed view mode styles.
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
|
@ -47,26 +51,6 @@ work better in terms of asthetics and performance. Improved search,
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Search Performance
|
|
||||||
|
|
||||||
100x faster search. However, this will be at expense of RAM. if you have < 1 million
|
|
||||||
files and folders in the given scope, the RAM usage should be less than 200MB total. RAM requirements
|
|
||||||
should scale based on the number of directories.
|
|
||||||
|
|
||||||
Also , the approx. time to fully index will vary widely based on performance. A sufficiently performant
|
|
||||||
system should fully index within the first 5 minutes, potentially within the first few seconds.
|
|
||||||
|
|
||||||
For example, a low end 11th gen i5 with SSD indexes 128K files within 1 second:
|
|
||||||
|
|
||||||
```
|
|
||||||
2023/09/09 21:38:50 Initializing with config file: filebrowser.yaml
|
|
||||||
2023/09/09 21:38:50 Indexing files...
|
|
||||||
2023/09/09 21:38:50 Listening on [::]:8080
|
|
||||||
2023/09/09 21:38:51 Successfully indexed files.
|
|
||||||
2023/09/09 21:38:51 Files found : 123452
|
|
||||||
2023/09/09 21:38:51 Directories found : 1768
|
|
||||||
2023/09/09 21:38:51 Indexing scheduler will run every 5 minutes
|
|
||||||
```
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/errors"
|
"github.com/gtsteffaniak/filebrowser/errors"
|
||||||
"github.com/gtsteffaniak/filebrowser/files"
|
|
||||||
"github.com/gtsteffaniak/filebrowser/settings"
|
"github.com/gtsteffaniak/filebrowser/settings"
|
||||||
"github.com/gtsteffaniak/filebrowser/users"
|
"github.com/gtsteffaniak/filebrowser/users"
|
||||||
)
|
)
|
||||||
|
@ -44,8 +43,8 @@ func (a *HookAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
a.Users = usr
|
a.Users = usr
|
||||||
a.Settings = &settings.GlobalConfiguration
|
a.Settings = &settings.Config
|
||||||
a.Server = &settings.GlobalConfiguration.Server
|
a.Server = &settings.Config.Server
|
||||||
a.Cred = cred
|
a.Cred = cred
|
||||||
|
|
||||||
action, err := a.RunCommand()
|
action, err := a.RunCommand()
|
||||||
|
@ -207,7 +206,7 @@ func (a *HookAuth) GetUser(d *users.User) *users.User {
|
||||||
Locale: d.Locale,
|
Locale: d.Locale,
|
||||||
ViewMode: d.ViewMode,
|
ViewMode: d.ViewMode,
|
||||||
SingleClick: d.SingleClick,
|
SingleClick: d.SingleClick,
|
||||||
Sorting: files.Sorting{
|
Sorting: users.Sorting{
|
||||||
Asc: d.Sorting.Asc,
|
Asc: d.Sorting.Asc,
|
||||||
By: d.Sorting.By,
|
By: d.Sorting.By,
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,7 +24,7 @@ type JSONAuth struct {
|
||||||
|
|
||||||
// Auth authenticates the user via a json in content body.
|
// Auth authenticates the user via a json in content body.
|
||||||
func (a JSONAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
|
func (a JSONAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
|
||||||
config := &settings.GlobalConfiguration
|
config := &settings.Config
|
||||||
var cred jsonCred
|
var cred jsonCred
|
||||||
|
|
||||||
if r.Body == nil {
|
if r.Body == nil {
|
||||||
|
|
|
@ -15,7 +15,7 @@ type NoAuth struct{}
|
||||||
|
|
||||||
// Auth uses authenticates user 1.
|
// Auth uses authenticates user 1.
|
||||||
func (a NoAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
|
func (a NoAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
|
||||||
return usr.Get(settings.GlobalConfiguration.Server.Root, uint(1))
|
return usr.Get(settings.Config.Server.Root, uint(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginPage tells that no auth doesn't require a login page.
|
// LoginPage tells that no auth doesn't require a login page.
|
||||||
|
|
|
@ -21,7 +21,7 @@ type ProxyAuth struct {
|
||||||
// Auth authenticates the user via an HTTP header.
|
// Auth authenticates the user via an HTTP header.
|
||||||
func (a ProxyAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
|
func (a ProxyAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
|
||||||
username := r.Header.Get(a.Header)
|
username := r.Header.Get(a.Header)
|
||||||
user, err := usr.Get(settings.GlobalConfiguration.Server.Root, username)
|
user, err := usr.Get(settings.Config.Server.Root, username)
|
||||||
if err == errors.ErrNotExist {
|
if err == errors.ErrNotExist {
|
||||||
return nil, os.ErrPermission
|
return nil, os.ErrPermission
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,41 +5,40 @@
|
||||||
? github.com/gtsteffaniak/filebrowser/auth [no test files]
|
? github.com/gtsteffaniak/filebrowser/auth [no test files]
|
||||||
? github.com/gtsteffaniak/filebrowser/cmd [no test files]
|
? github.com/gtsteffaniak/filebrowser/cmd [no test files]
|
||||||
PASS
|
PASS
|
||||||
ok github.com/gtsteffaniak/filebrowser/diskcache 0.004s
|
ok github.com/gtsteffaniak/filebrowser/diskcache 0.003s
|
||||||
? github.com/gtsteffaniak/filebrowser/errors [no test files]
|
? github.com/gtsteffaniak/filebrowser/errors [no test files]
|
||||||
? github.com/gtsteffaniak/filebrowser/files [no test files]
|
goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
pkg: github.com/gtsteffaniak/filebrowser/files
|
||||||
|
cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz
|
||||||
|
BenchmarkFillIndex-8 10 3295862 ns/op 230448 B/op 1927 allocs/op
|
||||||
|
BenchmarkSearchAllIndexes-8 10 30033386 ns/op 19647893 B/op 298702 allocs/op
|
||||||
|
PASS
|
||||||
|
ok github.com/gtsteffaniak/filebrowser/files 0.392s
|
||||||
PASS
|
PASS
|
||||||
ok github.com/gtsteffaniak/filebrowser/fileutils 0.003s
|
ok github.com/gtsteffaniak/filebrowser/fileutils 0.003s
|
||||||
2023/10/18 10:19:52 Saving new user: username
|
2023/11/24 13:57:20 Saving new user: username
|
||||||
2023/10/18 10:19:52 Saving new user: username
|
2023/11/24 13:57:20 Saving new user: username
|
||||||
2023/10/18 10:19:52 Saving new user: username
|
2023/11/24 13:57:20 Saving new user: username
|
||||||
2023/10/18 10:19:52 Saving new user: username
|
2023/11/24 13:57:20 Saving new user: username
|
||||||
2023/10/18 10:19:52 Saving new user: username
|
2023/11/24 13:57:20 Saving new user: username
|
||||||
2023/10/18 10:19:52 Saving new user: username
|
2023/11/24 13:57:20 Saving new user: username
|
||||||
2023/10/18 10:19:52 Saving new user: username
|
2023/11/24 13:57:20 Saving new user: username
|
||||||
2023/10/18 10:19:52 Saving new user: username
|
2023/11/24 13:57:20 Saving new user: username
|
||||||
2023/10/18 10:19:52 h: 401 <nil>
|
2023/11/24 13:57:20 h: 401 <nil>
|
||||||
2023/10/18 10:19:52 h: 401 <nil>
|
2023/11/24 13:57:20 h: 401 <nil>
|
||||||
2023/10/18 10:19:52 h: 401 <nil>
|
2023/11/24 13:57:20 Saving new user: username
|
||||||
2023/10/18 10:19:52 h: 401 <nil>
|
2023/11/24 13:57:20 Saving new user: username
|
||||||
2023/10/18 10:19:52 Saving new user: username
|
2023/11/24 13:57:20 Saving new user: username
|
||||||
2023/10/18 10:19:52 Saving new user: username
|
2023/11/24 13:57:20 Saving new user: username
|
||||||
2023/10/18 10:19:52 Saving new user: username
|
2023/11/24 13:57:20 h: 401 <nil>
|
||||||
2023/10/18 10:19:52 Saving new user: username
|
2023/11/24 13:57:20 h: 401 <nil>
|
||||||
2023/10/18 10:19:53 h: 401 <nil>
|
2023/11/24 13:57:20 h: 401 <nil>
|
||||||
2023/10/18 10:19:53 h: 401 <nil>
|
2023/11/24 13:57:20 h: 401 <nil>
|
||||||
PASS
|
PASS
|
||||||
ok github.com/gtsteffaniak/filebrowser/http 0.208s
|
ok github.com/gtsteffaniak/filebrowser/http 0.208s
|
||||||
PASS
|
PASS
|
||||||
ok github.com/gtsteffaniak/filebrowser/img 0.124s
|
ok github.com/gtsteffaniak/filebrowser/img 0.118s
|
||||||
goos: linux
|
|
||||||
goarch: amd64
|
|
||||||
pkg: github.com/gtsteffaniak/filebrowser/index
|
|
||||||
cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz
|
|
||||||
BenchmarkFillIndex-8 10 3239196 ns/op 11289 B/op 448 allocs/op
|
|
||||||
BenchmarkSearchAllIndexes-8 10 6645964 ns/op 3176834 B/op 59104 allocs/op
|
|
||||||
PASS
|
|
||||||
ok github.com/gtsteffaniak/filebrowser/index 0.126s
|
|
||||||
PASS
|
PASS
|
||||||
ok github.com/gtsteffaniak/filebrowser/rules 0.002s
|
ok github.com/gtsteffaniak/filebrowser/rules 0.002s
|
||||||
PASS
|
PASS
|
||||||
|
|
|
@ -19,9 +19,9 @@ import (
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/auth"
|
"github.com/gtsteffaniak/filebrowser/auth"
|
||||||
"github.com/gtsteffaniak/filebrowser/diskcache"
|
"github.com/gtsteffaniak/filebrowser/diskcache"
|
||||||
|
"github.com/gtsteffaniak/filebrowser/files"
|
||||||
fbhttp "github.com/gtsteffaniak/filebrowser/http"
|
fbhttp "github.com/gtsteffaniak/filebrowser/http"
|
||||||
"github.com/gtsteffaniak/filebrowser/img"
|
"github.com/gtsteffaniak/filebrowser/img"
|
||||||
"github.com/gtsteffaniak/filebrowser/index"
|
|
||||||
"github.com/gtsteffaniak/filebrowser/settings"
|
"github.com/gtsteffaniak/filebrowser/settings"
|
||||||
"github.com/gtsteffaniak/filebrowser/users"
|
"github.com/gtsteffaniak/filebrowser/users"
|
||||||
)
|
)
|
||||||
|
@ -47,7 +47,7 @@ func init() {
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "filebrowser",
|
Use: "filebrowser",
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
serverConfig := settings.GlobalConfiguration.Server
|
serverConfig := settings.Config.Server
|
||||||
if !d.hadDB {
|
if !d.hadDB {
|
||||||
quickSetup(d)
|
quickSetup(d)
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ var rootCmd = &cobra.Command{
|
||||||
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
|
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
|
||||||
}
|
}
|
||||||
// initialize indexing and schedule indexing ever n minutes (default 5)
|
// initialize indexing and schedule indexing ever n minutes (default 5)
|
||||||
go index.Initialize(serverConfig.IndexingInterval)
|
go files.InitializeIndex(serverConfig.IndexingInterval, true)
|
||||||
_, err := os.Stat(serverConfig.Root)
|
_, err := os.Stat(serverConfig.Root)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
var listener net.Listener
|
var listener net.Listener
|
||||||
|
@ -118,23 +118,23 @@ func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfac
|
||||||
}
|
}
|
||||||
|
|
||||||
func quickSetup(d pythonData) {
|
func quickSetup(d pythonData) {
|
||||||
settings.GlobalConfiguration.Auth.Key = generateKey()
|
settings.Config.Auth.Key = generateKey()
|
||||||
if settings.GlobalConfiguration.Auth.Method == "noauth" {
|
if settings.Config.Auth.Method == "noauth" {
|
||||||
err := d.store.Auth.Save(&auth.NoAuth{})
|
err := d.store.Auth.Save(&auth.NoAuth{})
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
} else {
|
} else {
|
||||||
settings.GlobalConfiguration.Auth.Method = "password"
|
settings.Config.Auth.Method = "password"
|
||||||
err := d.store.Auth.Save(&auth.JSONAuth{})
|
err := d.store.Auth.Save(&auth.JSONAuth{})
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
}
|
}
|
||||||
err := d.store.Settings.Save(&settings.GlobalConfiguration)
|
err := d.store.Settings.Save(&settings.Config)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
err = d.store.Settings.SaveServer(&settings.GlobalConfiguration.Server)
|
err = d.store.Settings.SaveServer(&settings.Config.Server)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
user := &users.User{}
|
user := &users.User{}
|
||||||
settings.GlobalConfiguration.UserDefaults.Apply(user)
|
settings.Config.UserDefaults.Apply(user)
|
||||||
user.Username = settings.GlobalConfiguration.Auth.AdminUsername
|
user.Username = settings.Config.Auth.AdminUsername
|
||||||
user.Password = settings.GlobalConfiguration.Auth.AdminPassword
|
user.Password = settings.Config.Auth.AdminPassword
|
||||||
user.Perm.Admin = true
|
user.Perm.Admin = true
|
||||||
user.Scope = "./"
|
user.Scope = "./"
|
||||||
user.DarkMode = true
|
user.DarkMode = true
|
||||||
|
|
|
@ -83,7 +83,7 @@ func dbExists(path string) (bool, error) {
|
||||||
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
|
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
|
||||||
return func(cmd *cobra.Command, args []string) {
|
return func(cmd *cobra.Command, args []string) {
|
||||||
data := pythonData{hadDB: true}
|
data := pythonData{hadDB: true}
|
||||||
path := settings.GlobalConfiguration.Server.Database
|
path := settings.Config.Server.Database
|
||||||
exists, err := dbExists(path)
|
exists, err := dbExists(path)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
server:
|
server:
|
||||||
port: 8080
|
port: 8080
|
||||||
baseURL: "/"
|
baseURL: "/"
|
||||||
|
root: "/srv"
|
||||||
auth:
|
auth:
|
||||||
method: password
|
method: password
|
||||||
signup: true
|
signup: false
|
||||||
userDefaults:
|
userDefaults:
|
||||||
darkMode: true
|
darkMode: true
|
||||||
disableSettings: false
|
disableSettings: false
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package index
|
package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"mime"
|
"mime"
|
||||||
|
@ -15,22 +15,31 @@ var AllFiletypeOptions = []string{
|
||||||
"video",
|
"video",
|
||||||
"doc",
|
"doc",
|
||||||
"dir",
|
"dir",
|
||||||
|
"text",
|
||||||
}
|
}
|
||||||
var documentTypes = []string{
|
var documentTypes = []string{
|
||||||
".word",
|
".word",
|
||||||
".pdf",
|
".pdf",
|
||||||
".txt",
|
|
||||||
".doc",
|
".doc",
|
||||||
".docx",
|
".docx",
|
||||||
}
|
}
|
||||||
|
var textTypes = []string{
|
||||||
|
".text",
|
||||||
|
".sh",
|
||||||
|
".yaml",
|
||||||
|
".yml",
|
||||||
|
".json",
|
||||||
|
".bashrc",
|
||||||
|
".zshrc",
|
||||||
|
".env",
|
||||||
|
}
|
||||||
var compressedFile = []string{
|
var compressedFile = []string{
|
||||||
".7z",
|
".7z",
|
||||||
".rar",
|
".rar",
|
||||||
".zip",
|
".zip",
|
||||||
".tar",
|
".tar",
|
||||||
".tar.gz",
|
".gz",
|
||||||
".tar.xz",
|
".xz",
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchOptions struct {
|
type SearchOptions struct {
|
||||||
|
@ -137,12 +146,25 @@ func IsMatchingType(extension string, matchType string) bool {
|
||||||
switch matchType {
|
switch matchType {
|
||||||
case "doc":
|
case "doc":
|
||||||
return isDoc(extension)
|
return isDoc(extension)
|
||||||
|
case "pdf":
|
||||||
|
return extension == ".pdf"
|
||||||
|
case "text":
|
||||||
|
return isText(extension)
|
||||||
case "archive":
|
case "archive":
|
||||||
return isArchive(extension)
|
return isArchive(extension)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isText(extension string) bool {
|
||||||
|
for _, typefile := range textTypes {
|
||||||
|
if extension == typefile {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func isDoc(extension string) bool {
|
func isDoc(extension string) bool {
|
||||||
for _, typefile := range documentTypes {
|
for _, typefile := range documentTypes {
|
||||||
if extension == typefile {
|
if extension == typefile {
|
|
@ -8,33 +8,40 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"hash"
|
"hash"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
filepath "path/filepath"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/errors"
|
"github.com/gtsteffaniak/filebrowser/errors"
|
||||||
"github.com/gtsteffaniak/filebrowser/rules"
|
"github.com/gtsteffaniak/filebrowser/rules"
|
||||||
|
"github.com/gtsteffaniak/filebrowser/users"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
bytesInMegabyte int64 = 1000000
|
||||||
|
pathMutexes = make(map[string]*sync.Mutex)
|
||||||
|
pathMutexesMu sync.Mutex // Mutex to protect the pathMutexes map
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileInfo describes a file.
|
// FileInfo describes a file.
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
*Listing
|
*Listing
|
||||||
Fs afero.Fs `json:"-"`
|
Fs afero.Fs `json:"-"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
Extension string `json:"extension"`
|
Extension string `json:"-"`
|
||||||
ModTime time.Time `json:"modified"`
|
ModTime time.Time `json:"modified"`
|
||||||
Mode os.FileMode `json:"mode"`
|
CacheTime time.Time `json:"-"`
|
||||||
IsDir bool `json:"isDir"`
|
Mode os.FileMode `json:"-"`
|
||||||
IsSymlink bool `json:"isSymlink"`
|
IsDir bool `json:"isDir,omitempty"`
|
||||||
|
IsSymlink bool `json:"isSymlink,omitempty"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Subtitles []string `json:"subtitles,omitempty"`
|
Subtitles []string `json:"subtitles,omitempty"`
|
||||||
Content string `json:"content,omitempty"`
|
Content string `json:"content,omitempty"`
|
||||||
|
@ -54,6 +61,22 @@ type FileOptions struct {
|
||||||
Content bool
|
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
|
// 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
|
// 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.
|
// or a file. If it's a video file, it will also detect any subtitles.
|
||||||
|
@ -61,67 +84,120 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
|
||||||
if !opts.Checker.Check(opts.Path) {
|
if !opts.Checker.Check(opts.Path) {
|
||||||
return nil, os.ErrPermission
|
return nil, os.ErrPermission
|
||||||
}
|
}
|
||||||
|
file, err := stat(opts.Path, opts) // Pass opts.Path here
|
||||||
file, err := stat(opts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Expand {
|
if opts.Expand {
|
||||||
if file.IsDir {
|
if file.IsDir {
|
||||||
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
|
if err := file.readListing(opts.Path, opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return file, nil
|
return file, nil
|
||||||
}
|
}
|
||||||
|
err = file.detectType(opts.Path, opts.Modify, opts.Content, true)
|
||||||
err = file.detectType(opts.Modify, opts.Content, true)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return file, err
|
return file, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func stat(opts FileOptions) (*FileInfo, error) {
|
func FileInfoFaster(opts FileOptions) (*FileInfo, error) {
|
||||||
var file *FileInfo
|
// Lock access for the specific path
|
||||||
|
pathMutex := getMutex(opts.Path)
|
||||||
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
|
pathMutex.Lock()
|
||||||
info, _, err := lstaterFs.LstatIfPossible(opts.Path)
|
defer pathMutex.Unlock()
|
||||||
if err != nil {
|
if !opts.Checker.Check(opts.Path) {
|
||||||
return nil, err
|
return nil, os.ErrPermission
|
||||||
}
|
}
|
||||||
|
index := GetIndex(rootPath)
|
||||||
|
trimmed := strings.TrimPrefix(opts.Path, "/")
|
||||||
|
if trimmed == "" {
|
||||||
|
trimmed = "/"
|
||||||
|
}
|
||||||
|
adjustedPath := makeIndexPath(trimmed, index.Root)
|
||||||
|
var info FileInfo
|
||||||
|
info, exists := index.GetMetadataInfo(adjustedPath)
|
||||||
|
if exists {
|
||||||
|
// 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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshFileInfo(opts FileOptions) bool {
|
||||||
|
if !opts.Checker.Check(opts.Path) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
file, err := stat(opts.Path, opts) // Pass opts.Path here
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
index := GetIndex(rootPath)
|
||||||
|
trimmed := strings.TrimPrefix(opts.Path, "/")
|
||||||
|
if trimmed == "" {
|
||||||
|
trimmed = "/"
|
||||||
|
}
|
||||||
|
adjustedPath := makeIndexPath(trimmed, index.Root)
|
||||||
|
if file.IsDir {
|
||||||
|
err := file.readListing(opts.Path, opts.Checker, opts.ReadHeader)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
//_, exists := index.GetFileMetadata(adjustedPath)
|
||||||
|
return index.UpdateFileMetadata(adjustedPath, *file)
|
||||||
|
} else {
|
||||||
|
//_, exists := index.GetFileMetadata(adjustedPath)
|
||||||
|
return index.UpdateFileMetadata(adjustedPath, *file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stat(path string, opts FileOptions) (*FileInfo, error) {
|
||||||
|
var file *FileInfo
|
||||||
|
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
|
||||||
|
info, _, err := lstaterFs.LstatIfPossible(path)
|
||||||
|
if err == nil {
|
||||||
file = &FileInfo{
|
file = &FileInfo{
|
||||||
Fs: opts.Fs,
|
Fs: opts.Fs,
|
||||||
Path: opts.Path,
|
Path: opts.Path,
|
||||||
Name: info.Name(),
|
Name: info.Name(),
|
||||||
ModTime: info.ModTime(),
|
ModTime: info.ModTime(),
|
||||||
Mode: info.Mode(),
|
Mode: info.Mode(),
|
||||||
IsDir: info.IsDir(),
|
|
||||||
IsSymlink: IsSymlink(info.Mode()),
|
|
||||||
Size: info.Size(),
|
Size: info.Size(),
|
||||||
Extension: filepath.Ext(info.Name()),
|
Extension: filepath.Ext(info.Name()),
|
||||||
Token: opts.Token,
|
Token: opts.Token,
|
||||||
}
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
file.IsDir = true
|
||||||
|
}
|
||||||
|
if info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
file.IsSymlink = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// regular file
|
if file == nil || file.IsSymlink {
|
||||||
if file != nil && !file.IsSymlink {
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// fs doesn't support afero.Lstater interface or the file is a symlink
|
|
||||||
info, err := opts.Fs.Stat(opts.Path)
|
info, err := opts.Fs.Stat(opts.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// can't follow symlink
|
|
||||||
if file != nil && file.IsSymlink {
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// set correct file size in case of symlink
|
|
||||||
if file != nil && file.IsSymlink {
|
if file != nil && file.IsSymlink {
|
||||||
file.Size = info.Size()
|
file.Size = info.Size()
|
||||||
file.IsDir = info.IsDir()
|
file.IsDir = info.IsDir()
|
||||||
|
@ -139,6 +215,7 @@ func stat(opts FileOptions) (*FileInfo, error) {
|
||||||
Extension: filepath.Ext(info.Name()),
|
Extension: filepath.Ext(info.Name()),
|
||||||
Token: opts.Token,
|
Token: opts.Token,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return file, nil
|
return file, nil
|
||||||
}
|
}
|
||||||
|
@ -160,19 +237,15 @@ func (i *FileInfo) Checksum(algo string) error {
|
||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
|
|
||||||
var h hash.Hash
|
hashFuncs := map[string]hash.Hash{
|
||||||
|
"md5": md5.New(),
|
||||||
|
"sha1": sha1.New(),
|
||||||
|
"sha256": sha256.New(),
|
||||||
|
"sha512": sha512.New(),
|
||||||
|
}
|
||||||
|
|
||||||
//nolint:gosec
|
h, ok := hashFuncs[algo]
|
||||||
switch algo {
|
if !ok {
|
||||||
case "md5":
|
|
||||||
h = md5.New()
|
|
||||||
case "sha1":
|
|
||||||
h = sha1.New()
|
|
||||||
case "sha256":
|
|
||||||
h = sha256.New()
|
|
||||||
case "sha512":
|
|
||||||
h = sha512.New()
|
|
||||||
default:
|
|
||||||
return errors.ErrInvalidOption
|
return errors.ErrInvalidOption
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +258,7 @@ func (i *FileInfo) Checksum(algo string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RealPath gets the real path for the file, resolving symlinks if supported.
|
||||||
func (i *FileInfo) RealPath() string {
|
func (i *FileInfo) RealPath() string {
|
||||||
if realPathFs, ok := i.Fs.(interface {
|
if realPathFs, ok := i.Fs.(interface {
|
||||||
RealPath(name string) (fPath string, err error)
|
RealPath(name string) (fPath string, err error)
|
||||||
|
@ -198,72 +272,57 @@ func (i *FileInfo) RealPath() string {
|
||||||
return i.Path
|
return i.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use constants
|
// detectType detects the file type.
|
||||||
//
|
func (i *FileInfo) detectType(path string, modify, saveContent, readHeader bool) error {
|
||||||
//nolint:goconst
|
|
||||||
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
|
|
||||||
if IsNamedPipe(i.Mode) {
|
if IsNamedPipe(i.Mode) {
|
||||||
i.Type = "blob"
|
i.Type = "blob"
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// failing to detect the type should not return error.
|
|
||||||
// imagine the situation where a file in a dir with thousands
|
|
||||||
// of files couldn't be opened: we'd have immediately
|
|
||||||
// a 500 even though it doesn't matter. So we just log it.
|
|
||||||
|
|
||||||
mimetype := mime.TypeByExtension(i.Extension)
|
|
||||||
|
|
||||||
var buffer []byte
|
var buffer []byte
|
||||||
if readHeader {
|
if readHeader {
|
||||||
buffer = i.readFirstBytes()
|
buffer = i.readFirstBytes()
|
||||||
|
mimetype := mime.TypeByExtension(i.Extension)
|
||||||
if mimetype == "" {
|
if mimetype == "" {
|
||||||
mimetype = http.DetectContentType(buffer)
|
http.DetectContentType(buffer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ext := filepath.Ext(i.Name)
|
||||||
switch {
|
for _, fileType := range AllFiletypeOptions {
|
||||||
case strings.HasPrefix(mimetype, "video"):
|
if IsMatchingType(ext, fileType) {
|
||||||
i.Type = "video"
|
i.Type = fileType
|
||||||
i.detectSubtitles()
|
}
|
||||||
return nil
|
switch i.Type {
|
||||||
case strings.HasPrefix(mimetype, "audio"):
|
case "text":
|
||||||
i.Type = "audio"
|
|
||||||
return nil
|
|
||||||
case strings.HasPrefix(mimetype, "image"):
|
|
||||||
i.Type = "image"
|
|
||||||
return nil
|
|
||||||
case strings.HasSuffix(mimetype, "pdf"):
|
|
||||||
i.Type = "pdf"
|
|
||||||
return nil
|
|
||||||
case (strings.HasPrefix(mimetype, "text") || !isBinary(buffer)) && i.Size <= 10*1024*1024: // 10 MB
|
|
||||||
i.Type = "text"
|
|
||||||
|
|
||||||
if !modify {
|
if !modify {
|
||||||
i.Type = "textImmutable"
|
i.Type = "textImmutable"
|
||||||
}
|
}
|
||||||
|
|
||||||
if saveContent {
|
if saveContent {
|
||||||
afs := &afero.Afero{Fs: i.Fs}
|
afs := &afero.Afero{Fs: i.Fs}
|
||||||
content, err := afs.ReadFile(i.Path)
|
content, err := afs.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
i.Content = string(content)
|
i.Content = string(content)
|
||||||
}
|
}
|
||||||
return nil
|
case "video":
|
||||||
default:
|
parentDir := strings.TrimRight(path, i.Name)
|
||||||
|
i.detectSubtitles(parentDir)
|
||||||
|
case "doc":
|
||||||
|
if ext == ".pdf" {
|
||||||
|
i.Type = "pdf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i.Type == "" {
|
||||||
i.Type = "blob"
|
i.Type = "blob"
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readFirstBytes reads the first bytes of the file.
|
||||||
func (i *FileInfo) readFirstBytes() []byte {
|
func (i *FileInfo) readFirstBytes() []byte {
|
||||||
reader, err := i.Fs.Open(i.Path)
|
reader, err := i.Fs.Open(i.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
|
||||||
i.Type = "blob"
|
i.Type = "blob"
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -272,7 +331,6 @@ func (i *FileInfo) readFirstBytes() []byte {
|
||||||
buffer := make([]byte, 512) //nolint:gomnd
|
buffer := make([]byte, 512) //nolint:gomnd
|
||||||
n, err := reader.Read(buffer)
|
n, err := reader.Read(buffer)
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
log.Print(err)
|
|
||||||
i.Type = "blob"
|
i.Type = "blob"
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -280,29 +338,43 @@ func (i *FileInfo) readFirstBytes() []byte {
|
||||||
return buffer[:n]
|
return buffer[:n]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *FileInfo) detectSubtitles() {
|
// detectSubtitles detects subtitles for video files.
|
||||||
|
func (i *FileInfo) detectSubtitles(parentDir string) {
|
||||||
if i.Type != "video" {
|
if i.Type != "video" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
i.Subtitles = []string{}
|
i.Subtitles = []string{}
|
||||||
ext := filepath.Ext(i.Path)
|
ext := filepath.Ext(i.Name)
|
||||||
|
dir, err := os.Open(parentDir)
|
||||||
|
if err != nil {
|
||||||
|
// Directory must have been deleted, remove it from the index
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Read the directory contents
|
||||||
|
files, err := dir.Readdir(-1)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// detect multiple languages. Base*.vtt
|
|
||||||
// TODO: give subtitles descriptive names (lang) and track attributes
|
|
||||||
parentDir := strings.TrimRight(i.Path, i.Name)
|
|
||||||
dir, err := afero.ReadDir(i.Fs, parentDir)
|
|
||||||
if err == nil {
|
|
||||||
base := strings.TrimSuffix(i.Name, ext)
|
base := strings.TrimSuffix(i.Name, ext)
|
||||||
for _, f := range dir {
|
subtitleExts := []string{".vtt", ".txt", ".srt", ".lrc"}
|
||||||
if !f.IsDir() && strings.HasPrefix(f.Name(), base) && strings.HasSuffix(f.Name(), ".vtt") {
|
|
||||||
i.Subtitles = append(i.Subtitles, path.Join(parentDir, f.Name()))
|
for _, f := range files {
|
||||||
|
if f.IsDir() || !strings.HasPrefix(f.Name(), base) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, subtitleExt := range subtitleExts {
|
||||||
|
if strings.HasSuffix(f.Name(), subtitleExt) {
|
||||||
|
i.Subtitles = append(i.Subtitles, filepath.Join(parentDir, f.Name()))
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
// readListing reads the contents of a directory and fills the listing.
|
||||||
|
func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bool) error {
|
||||||
afs := &afero.Afero{Fs: i.Fs}
|
afs := &afero.Afero{Fs: i.Fs}
|
||||||
dir, err := afs.ReadDir(i.Path)
|
dir, err := afs.ReadDir(i.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -311,13 +383,14 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
||||||
|
|
||||||
listing := &Listing{
|
listing := &Listing{
|
||||||
Items: []*FileInfo{},
|
Items: []*FileInfo{},
|
||||||
|
Path: i.Path,
|
||||||
NumDirs: 0,
|
NumDirs: 0,
|
||||||
NumFiles: 0,
|
NumFiles: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, f := range dir {
|
for _, f := range dir {
|
||||||
name := f.Name()
|
name := f.Name()
|
||||||
fPath := path.Join(i.Path, name)
|
fPath := filepath.Join(i.Path, name)
|
||||||
|
|
||||||
if !checker.Check(fPath) {
|
if !checker.Check(fPath) {
|
||||||
continue
|
continue
|
||||||
|
@ -326,8 +399,6 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
||||||
isSymlink, isInvalidLink := false, false
|
isSymlink, isInvalidLink := false, false
|
||||||
if IsSymlink(f.Mode()) {
|
if IsSymlink(f.Mode()) {
|
||||||
isSymlink = true
|
isSymlink = true
|
||||||
// It's a symbolic link. We try to follow it. If it doesn't work,
|
|
||||||
// we stay with the link information instead of the target's.
|
|
||||||
info, err := i.Fs.Stat(fPath)
|
info, err := i.Fs.Stat(fPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
f = info
|
f = info
|
||||||
|
@ -337,15 +408,16 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
file := &FileInfo{
|
file := &FileInfo{
|
||||||
Fs: i.Fs,
|
|
||||||
Name: name,
|
Name: name,
|
||||||
Size: f.Size(),
|
Size: f.Size(),
|
||||||
ModTime: f.ModTime(),
|
ModTime: f.ModTime(),
|
||||||
Mode: f.Mode(),
|
Mode: f.Mode(),
|
||||||
IsDir: f.IsDir(),
|
}
|
||||||
IsSymlink: isSymlink,
|
if f.IsDir() {
|
||||||
Extension: filepath.Ext(name),
|
file.IsDir = true
|
||||||
Path: fPath,
|
}
|
||||||
|
if isSymlink {
|
||||||
|
file.IsSymlink = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if file.IsDir {
|
if file.IsDir {
|
||||||
|
@ -356,7 +428,7 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
||||||
if isInvalidLink {
|
if isInvalidLink {
|
||||||
file.Type = "invalid_link"
|
file.Type = "invalid_link"
|
||||||
} else {
|
} else {
|
||||||
err := file.detectType(true, false, readHeader)
|
err := file.detectType(path, true, false, readHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -369,3 +441,23 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
||||||
i.Listing = listing
|
i.Listing = listing
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
func IsNamedPipe(mode os.FileMode) bool {
|
||||||
|
return mode&os.ModeNamedPipe != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsSymlink(mode os.FileMode) bool {
|
||||||
|
return mode&os.ModeSymlink != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMutex(path string) *sync.Mutex {
|
||||||
|
// Lock access to pathMutexes map
|
||||||
|
pathMutexesMu.Lock()
|
||||||
|
defer pathMutexesMu.Unlock()
|
||||||
|
|
||||||
|
// Create a mutex for the path if it doesn't exist
|
||||||
|
if pathMutexes[path] == nil {
|
||||||
|
pathMutexes[path] = &sync.Mutex{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathMutexes[path]
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
package files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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
|
||||||
|
NumDirs int
|
||||||
|
NumFiles int
|
||||||
|
inProgress bool
|
||||||
|
quickList []File
|
||||||
|
LastIndexed time.Time
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
rootPath string = "/srv"
|
||||||
|
indexes []*Index
|
||||||
|
indexesMutex sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitializeIndex(intervalMinutes uint32, schedule bool) {
|
||||||
|
if schedule {
|
||||||
|
go indexingScheduler(intervalMinutes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexingScheduler(intervalMinutes uint32) {
|
||||||
|
if settings.Config.Server.Root != "" {
|
||||||
|
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
|
||||||
|
si.LastIndexed = time.Now()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error during indexing: %v", err)
|
||||||
|
}
|
||||||
|
if si.NumFiles+si.NumDirs > 0 {
|
||||||
|
timeIndexedInSeconds := int(time.Since(startTime).Seconds())
|
||||||
|
log.Println("Successfully indexed files.")
|
||||||
|
log.Printf("Time spent indexing: %v seconds\n", timeIndexedInSeconds)
|
||||||
|
log.Printf("Files found: %v\n", si.NumFiles)
|
||||||
|
log.Printf("Directories found: %v\n", si.NumDirs)
|
||||||
|
}
|
||||||
|
// Sleep for the specified interval
|
||||||
|
time.Sleep(time.Duration(intervalMinutes) * time.Minute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
dir, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
// Directory must have been deleted, remove it from the index
|
||||||
|
si.RemoveDirectory(adjustedPath)
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the 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)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (si *Index) InsertFiles(path string) {
|
||||||
|
adjustedPath := makeIndexPath(path, si.Root)
|
||||||
|
subDirectory := Directory{}
|
||||||
|
buffer := bytes.Buffer{}
|
||||||
|
|
||||||
|
for _, f := range si.GetQuickList() {
|
||||||
|
buffer.WriteString(f.Name + ";")
|
||||||
|
si.UpdateCount("files")
|
||||||
|
}
|
||||||
|
// Use GetMetadataInfo and SetFileMetadata for safer read and write operations
|
||||||
|
subDirectory.Files = buffer.String()
|
||||||
|
si.SetDirectoryInfo(adjustedPath, subDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
adjustedPath := strings.TrimPrefix(path, root+"/")
|
||||||
|
adjustedPath = strings.TrimSuffix(adjustedPath, "/")
|
||||||
|
if adjustedPath == "" {
|
||||||
|
adjustedPath = "/"
|
||||||
|
}
|
||||||
|
return adjustedPath
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package index
|
package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
@ -6,28 +6,35 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gtsteffaniak/filebrowser/settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkFillIndex(b *testing.B) {
|
func BenchmarkFillIndex(b *testing.B) {
|
||||||
indexes = Index{
|
InitializeIndex(5, false)
|
||||||
Dirs: []string{},
|
si := GetIndex(settings.Config.Server.Root)
|
||||||
Files: []string{},
|
|
||||||
}
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
createMockData(50, 3) // 1000 dirs, 3 files per dir
|
si.createMockData(50, 3) // 1000 dirs, 3 files per dir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createMockData(numDirs, numFilesPerDir int) {
|
func (si *Index) createMockData(numDirs, numFilesPerDir int) {
|
||||||
for i := 0; i < numDirs; i++ {
|
for i := 0; i < numDirs; i++ {
|
||||||
dirName := generateRandomPath(rand.Intn(3) + 1)
|
dirName := generateRandomPath(rand.Intn(3) + 1)
|
||||||
addToIndex("/", dirName, true)
|
files := []File{}
|
||||||
|
// Append a new Directory to the slice
|
||||||
for j := 0; j < numFilesPerDir; j++ {
|
for j := 0; j < numFilesPerDir; j++ {
|
||||||
fileName := "file-" + getRandomTerm() + getRandomExtension()
|
newFile := File{
|
||||||
addToIndex("/"+dirName, fileName, false)
|
Name: "file-" + getRandomTerm() + getRandomExtension(),
|
||||||
|
IsDir: false,
|
||||||
}
|
}
|
||||||
|
files = append(files, newFile)
|
||||||
|
}
|
||||||
|
si.UpdateQuickListForTests(files)
|
||||||
|
si.InsertFiles(dirName)
|
||||||
|
si.InsertDirs(dirName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +98,7 @@ func TestGetIndex(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := GetIndex(); !reflect.DeepEqual(got, tt.want) {
|
if got := GetIndex("root"); !reflect.DeepEqual(got, tt.want) {
|
||||||
t.Errorf("GetIndex() = %v, want %v", got, tt.want)
|
t.Errorf("GetIndex() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -110,7 +117,7 @@ func TestInitializeIndex(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
Initialize(tt.args.intervalMinutes)
|
InitializeIndex(tt.args.intervalMinutes, false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,54 +138,3 @@ func Test_indexingScheduler(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_indexFiles(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
path string
|
|
||||||
numFiles *int
|
|
||||||
numDirs *int
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want int
|
|
||||||
want1 int
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, got1, err := indexFiles(tt.args.path, tt.args.numFiles, tt.args.numDirs)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("indexFiles() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("indexFiles() got = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
if got1 != tt.want1 {
|
|
||||||
t.Errorf("indexFiles() got1 = %v, want %v", got1, tt.want1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_addToIndex(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
path string
|
|
||||||
fileName string
|
|
||||||
isDir bool
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
addToIndex(tt.args.path, tt.args.fileName, tt.args.isDir)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,111 +0,0 @@
|
||||||
package files
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/maruel/natural"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Listing is a collection of files.
|
|
||||||
type Listing struct {
|
|
||||||
Items []*FileInfo `json:"items"`
|
|
||||||
NumDirs int `json:"numDirs"`
|
|
||||||
NumFiles int `json:"numFiles"`
|
|
||||||
Sorting Sorting `json:"sorting"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApplySort applies the sort order using .Order and .Sort
|
|
||||||
//
|
|
||||||
//nolint:goconst
|
|
||||||
func (l Listing) ApplySort() {
|
|
||||||
// Check '.Order' to know how to sort
|
|
||||||
// TODO: use enum
|
|
||||||
if !l.Sorting.Asc {
|
|
||||||
switch l.Sorting.By {
|
|
||||||
case "name":
|
|
||||||
sort.Sort(sort.Reverse(byName(l)))
|
|
||||||
case "size":
|
|
||||||
sort.Sort(sort.Reverse(bySize(l)))
|
|
||||||
case "modified":
|
|
||||||
sort.Sort(sort.Reverse(byModified(l)))
|
|
||||||
default:
|
|
||||||
// If not one of the above, do nothing
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else { // If we had more Orderings we could add them here
|
|
||||||
switch l.Sorting.By {
|
|
||||||
case "name":
|
|
||||||
sort.Sort(byName(l))
|
|
||||||
case "size":
|
|
||||||
sort.Sort(bySize(l))
|
|
||||||
case "modified":
|
|
||||||
sort.Sort(byModified(l))
|
|
||||||
default:
|
|
||||||
sort.Sort(byName(l))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implement sorting for Listing
|
|
||||||
type byName Listing
|
|
||||||
type bySize Listing
|
|
||||||
type byModified Listing
|
|
||||||
|
|
||||||
// By Name
|
|
||||||
func (l byName) Len() int {
|
|
||||||
return len(l.Items)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l byName) Swap(i, j int) {
|
|
||||||
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Treat upper and lower case equally
|
|
||||||
func (l byName) Less(i, j int) bool {
|
|
||||||
if l.Items[i].IsDir && !l.Items[j].IsDir {
|
|
||||||
return l.Sorting.Asc
|
|
||||||
}
|
|
||||||
|
|
||||||
if !l.Items[i].IsDir && l.Items[j].IsDir {
|
|
||||||
return !l.Sorting.Asc
|
|
||||||
}
|
|
||||||
|
|
||||||
return natural.Less(strings.ToLower(l.Items[j].Name), strings.ToLower(l.Items[i].Name))
|
|
||||||
}
|
|
||||||
|
|
||||||
// By Size
|
|
||||||
func (l bySize) Len() int {
|
|
||||||
return len(l.Items)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l bySize) Swap(i, j int) {
|
|
||||||
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
const directoryOffset = -1 << 31 // = math.MinInt32
|
|
||||||
func (l bySize) Less(i, j int) bool {
|
|
||||||
iSize, jSize := l.Items[i].Size, l.Items[j].Size
|
|
||||||
if l.Items[i].IsDir {
|
|
||||||
iSize = directoryOffset + iSize
|
|
||||||
}
|
|
||||||
if l.Items[j].IsDir {
|
|
||||||
jSize = directoryOffset + jSize
|
|
||||||
}
|
|
||||||
return iSize < jSize
|
|
||||||
}
|
|
||||||
|
|
||||||
// By Modified
|
|
||||||
func (l byModified) Len() int {
|
|
||||||
return len(l.Items)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l byModified) Swap(i, j int) {
|
|
||||||
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l byModified) Less(i, j int) bool {
|
|
||||||
iModified, jModified := l.Items[i].ModTime, l.Items[j].ModTime
|
|
||||||
return iModified.Sub(jModified) < 0
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package index
|
package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
@ -12,9 +12,7 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
sessionInProgress sync.Map
|
sessionInProgress sync.Map
|
||||||
mutex sync.RWMutex
|
|
||||||
maxSearchResults = 100
|
maxSearchResults = 100
|
||||||
bytesInMegabyte int64 = 1000000
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (si *Index) Search(search string, scope string, sourceSession string) ([]string, map[string]map[string]bool) {
|
func (si *Index) Search(search string, scope string, sourceSession string) ([]string, map[string]map[string]bool) {
|
||||||
|
@ -24,28 +22,19 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
|
||||||
runningHash := generateRandomHash(4)
|
runningHash := generateRandomHash(4)
|
||||||
sessionInProgress.Store(sourceSession, runningHash) // Store the value in the sync.Map
|
sessionInProgress.Store(sourceSession, runningHash) // Store the value in the sync.Map
|
||||||
searchOptions := ParseSearch(search)
|
searchOptions := ParseSearch(search)
|
||||||
mutex.RLock()
|
|
||||||
defer mutex.RUnlock()
|
|
||||||
fileListTypes := make(map[string]map[string]bool)
|
fileListTypes := make(map[string]map[string]bool)
|
||||||
matching := []string{}
|
matching := []string{}
|
||||||
|
count := 0
|
||||||
|
|
||||||
for _, searchTerm := range searchOptions.Terms {
|
for _, searchTerm := range searchOptions.Terms {
|
||||||
if searchTerm == "" {
|
if searchTerm == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Iterate over the embedded index.Index fields Dirs and Files
|
si.mu.Lock()
|
||||||
for _, i := range []string{"Dirs", "Files"} {
|
defer si.mu.Unlock()
|
||||||
isDir := false
|
for dirName, dir := range si.Directories {
|
||||||
count := 0
|
isDir := true
|
||||||
var paths []string
|
files := strings.Split(dir.Files, ";")
|
||||||
|
|
||||||
switch i {
|
|
||||||
case "Dirs":
|
|
||||||
isDir = true
|
|
||||||
paths = si.Dirs
|
|
||||||
case "Files":
|
|
||||||
paths = si.Files
|
|
||||||
}
|
|
||||||
for _, path := range paths {
|
|
||||||
value, found := sessionInProgress.Load(sourceSession)
|
value, found := sessionInProgress.Load(sourceSession)
|
||||||
if !found || value != runningHash {
|
if !found || value != runningHash {
|
||||||
return []string{}, map[string]map[string]bool{}
|
return []string{}, map[string]map[string]bool{}
|
||||||
|
@ -53,17 +42,41 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
|
||||||
if count > maxSearchResults {
|
if count > maxSearchResults {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
pathName := scopedPathNameFilter(path, scope, isDir)
|
pathName := scopedPathNameFilter(dirName, scope, isDir)
|
||||||
if pathName == "" {
|
if pathName == "" {
|
||||||
|
continue // path not matched
|
||||||
|
}
|
||||||
|
|
||||||
|
fileTypes := map[string]bool{}
|
||||||
|
matches, fileType := containsSearchTerm(dirName, searchTerm, *searchOptions, isDir, fileTypes)
|
||||||
|
if matches {
|
||||||
|
fileListTypes[pathName] = fileType
|
||||||
|
matching = append(matching, pathName)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
isDir = false
|
||||||
|
for _, file := range files {
|
||||||
|
if file == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
value, found := sessionInProgress.Load(sourceSession)
|
||||||
|
if !found || value != runningHash {
|
||||||
|
return []string{}, map[string]map[string]bool{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > maxSearchResults {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fullName := pathName + file
|
||||||
fileTypes := map[string]bool{}
|
fileTypes := map[string]bool{}
|
||||||
matches, fileType := containsSearchTerm(path, searchTerm, *searchOptions, isDir, fileTypes)
|
|
||||||
|
matches, fileType := containsSearchTerm(fullName, searchTerm, *searchOptions, isDir, fileTypes)
|
||||||
if !matches {
|
if !matches {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fileListTypes[pathName] = fileType
|
|
||||||
matching = append(matching, pathName)
|
fileListTypes[fullName] = fileType
|
||||||
|
matching = append(matching, fullName)
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package index
|
package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
@ -8,12 +8,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkSearchAllIndexes(b *testing.B) {
|
func BenchmarkSearchAllIndexes(b *testing.B) {
|
||||||
indexes = Index{
|
InitializeIndex(5, false)
|
||||||
Dirs: []string{},
|
si := GetIndex(rootPath)
|
||||||
Files: []string{},
|
|
||||||
}
|
si.createMockData(50, 3) // 1000 dirs, 3 files per dir
|
||||||
// Create mock data
|
|
||||||
createMockData(50, 3) // 1000 dirs, 3 files per dir
|
|
||||||
|
|
||||||
// Generate 100 random search terms
|
// Generate 100 random search terms
|
||||||
searchTerms := generateRandomSearchTerms(100)
|
searchTerms := generateRandomSearchTerms(100)
|
||||||
|
@ -23,7 +21,7 @@ func BenchmarkSearchAllIndexes(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
// Execute the SearchAllIndexes function
|
// Execute the SearchAllIndexes function
|
||||||
for _, term := range searchTerms {
|
for _, term := range searchTerms {
|
||||||
indexes.Search(term, "/", "test")
|
si.Search(term, "/", "test")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,20 +74,37 @@ func TestParseSearch(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
for _, term := range searchTerms {
|
||||||
|
go si.Search(term, "/", "test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSearchIndexes(t *testing.T) {
|
func TestSearchIndexes(t *testing.T) {
|
||||||
indexes = Index{
|
index := Index{
|
||||||
Dirs: []string{
|
Directories: map[string]Directory{
|
||||||
"/test",
|
"test": {
|
||||||
"/test/path",
|
Files: "audio1.wav;",
|
||||||
"/new/test/path",
|
},
|
||||||
|
"test/path": {
|
||||||
|
Files: "file.txt;",
|
||||||
|
},
|
||||||
|
"new": {},
|
||||||
|
"new/test": {
|
||||||
|
Files: "audio.wav;video.mp4;video.MP4;",
|
||||||
|
},
|
||||||
|
"new/test/path": {
|
||||||
|
Files: "archive.zip;",
|
||||||
},
|
},
|
||||||
Files: []string{
|
|
||||||
"/test/path/file.txt",
|
|
||||||
"/test/audio1.wav",
|
|
||||||
"/new/test/audio.wav",
|
|
||||||
"/new/test/video.mp4",
|
|
||||||
"/new/test/video.MP4",
|
|
||||||
"/new/test/path/archive.zip",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
@ -109,9 +124,10 @@ func TestSearchIndexes(t *testing.T) {
|
||||||
{
|
{
|
||||||
search: "test",
|
search: "test",
|
||||||
scope: "/",
|
scope: "/",
|
||||||
expectedResult: []string{"test/"},
|
expectedResult: []string{"test/", "new/test/"},
|
||||||
expectedTypes: map[string]map[string]bool{
|
expectedTypes: map[string]map[string]bool{
|
||||||
"test/": map[string]bool{"dir": true},
|
"test/": map[string]bool{"dir": true},
|
||||||
|
"new/test/": map[string]bool{"dir": true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -137,19 +153,35 @@ func TestSearchIndexes(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.search, func(t *testing.T) {
|
t.Run(tt.search, func(t *testing.T) {
|
||||||
actualResult, actualTypes := indexes.Search(tt.search, tt.scope, "")
|
actualResult, actualTypes := index.Search(tt.search, tt.scope, "")
|
||||||
assert.Equal(t, tt.expectedResult, actualResult)
|
assert.Equal(t, tt.expectedResult, actualResult)
|
||||||
if len(tt.expectedTypes) > 0 {
|
if !reflect.DeepEqual(tt.expectedTypes, actualTypes) {
|
||||||
for key, value := range tt.expectedTypes {
|
t.Fatalf("\n got: %+v\n want: %+v", actualTypes, tt.expectedTypes)
|
||||||
actualValue, exists := actualTypes[key]
|
|
||||||
assert.True(t, exists, "Expected type key '%s' not found in actual types", key)
|
|
||||||
assert.Equal(t, value, actualValue, "Type value mismatch for key '%s'", key)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_scopedPathNameFilter(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
pathName string
|
||||||
|
scope 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 := scopedPathNameFilter(tt.args.pathName, tt.args.scope, false); got != tt.want {
|
||||||
|
t.Errorf("scopedPathNameFilter() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
func Test_isDoc(t *testing.T) {
|
func Test_isDoc(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
extension string
|
extension string
|
|
@ -1,7 +0,0 @@
|
||||||
package files
|
|
||||||
|
|
||||||
// Sorting contains a sorting order.
|
|
||||||
type Sorting struct {
|
|
||||||
By string `json:"by"`
|
|
||||||
Asc bool `json:"asc"`
|
|
||||||
}
|
|
|
@ -0,0 +1,171 @@
|
||||||
|
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.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)
|
||||||
|
}
|
||||||
|
// Release the read lock before calling SetFileMetadata
|
||||||
|
}
|
||||||
|
return si.SetFileMetadata(adjustedPath, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFileMetadata sets the FileInfo for the specified directory in the index.
|
||||||
|
func (si *Index) SetFileMetadata(adjustedPath string, info FileInfo) bool {
|
||||||
|
si.mu.Lock()
|
||||||
|
defer si.mu.Unlock()
|
||||||
|
_, exists := si.Directories[adjustedPath]
|
||||||
|
if !exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
info.CacheTime = time.Now()
|
||||||
|
si.Directories[adjustedPath].Metadata[adjustedPath] = info
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetadataInfo retrieves the FileInfo from the specified directory in the index.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return FileInfo{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDirectoryInfo sets the directory information in the index.
|
||||||
|
func (si *Index) SetDirectoryInfo(adjustedPath string, dir Directory) {
|
||||||
|
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) {
|
||||||
|
si.mu.RLock()
|
||||||
|
dir, exists := si.Directories[adjustedPath]
|
||||||
|
si.mu.RUnlock()
|
||||||
|
if exists {
|
||||||
|
return dir, true
|
||||||
|
}
|
||||||
|
return Directory{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (si *Index) RemoveDirectory(path string) {
|
||||||
|
si.mu.Lock()
|
||||||
|
defer si.mu.Unlock()
|
||||||
|
delete(si.Directories, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (si *Index) UpdateCount(given string) {
|
||||||
|
si.mu.Lock()
|
||||||
|
defer si.mu.Unlock()
|
||||||
|
if given == "files" {
|
||||||
|
si.NumFiles++
|
||||||
|
} else if given == "dirs" {
|
||||||
|
si.NumDirs++
|
||||||
|
} else {
|
||||||
|
log.Println("could not update unknown type: ", given)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (si *Index) resetCount() {
|
||||||
|
si.mu.Lock()
|
||||||
|
defer si.mu.Unlock()
|
||||||
|
si.NumDirs = 0
|
||||||
|
si.NumFiles = 0
|
||||||
|
si.inProgress = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetIndex(root string) *Index {
|
||||||
|
for _, index := range indexes {
|
||||||
|
if index.Root == root {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if settings.Config.Server.Root != "" {
|
||||||
|
rootPath = settings.Config.Server.Root
|
||||||
|
}
|
||||||
|
newIndex := &Index{
|
||||||
|
Root: rootPath,
|
||||||
|
Directories: make(map[string]Directory), // Initialize the map
|
||||||
|
NumDirs: 0,
|
||||||
|
NumFiles: 0,
|
||||||
|
inProgress: false,
|
||||||
|
}
|
||||||
|
indexesMutex.Lock()
|
||||||
|
indexes = append(indexes, newIndex)
|
||||||
|
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
|
||||||
|
}
|
|
@ -1,59 +0,0 @@
|
||||||
package files
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
func isBinary(content []byte) bool {
|
|
||||||
maybeStr := string(content)
|
|
||||||
runeCnt := utf8.RuneCount(content)
|
|
||||||
runeIndex := 0
|
|
||||||
gotRuneErrCnt := 0
|
|
||||||
firstRuneErrIndex := -1
|
|
||||||
|
|
||||||
const (
|
|
||||||
// 8 and below are control chars (e.g. backspace, null, eof, etc)
|
|
||||||
maxControlCharsCode = 8
|
|
||||||
// 0xFFFD(65533) is the "error" Rune or "Unicode replacement character"
|
|
||||||
// see https://golang.org/pkg/unicode/utf8/#pkg-constants
|
|
||||||
unicodeReplacementChar = 0xFFFD
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, b := range maybeStr {
|
|
||||||
if b <= maxControlCharsCode {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if b == unicodeReplacementChar {
|
|
||||||
// if it is not the last (utf8.UTFMax - x) rune
|
|
||||||
if runeCnt > utf8.UTFMax && runeIndex < runeCnt-utf8.UTFMax {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// else it is the last (utf8.UTFMax - x) rune
|
|
||||||
// there maybe Vxxx, VVxx, VVVx, thus, we may got max 3 0xFFFD rune (assume V is the byte we got)
|
|
||||||
// for Chinese, it can only be Vxx, VVx, we may got max 2 0xFFFD rune
|
|
||||||
gotRuneErrCnt++
|
|
||||||
|
|
||||||
// mark the first time
|
|
||||||
if firstRuneErrIndex == -1 {
|
|
||||||
firstRuneErrIndex = runeIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
runeIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
// if last (utf8.UTFMax - x ) rune has the "error" Rune, but not all
|
|
||||||
if firstRuneErrIndex != -1 && gotRuneErrCnt != runeCnt-firstRuneErrIndex {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsNamedPipe(mode os.FileMode) bool {
|
|
||||||
return mode&os.ModeNamedPipe != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsSymlink(mode os.FileMode) bool {
|
|
||||||
return mode&os.ModeSymlink != 0
|
|
||||||
}
|
|
|
@ -11,7 +11,6 @@ require (
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||||
github.com/google/go-cmp v0.5.9
|
github.com/google/go-cmp v0.5.9
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/maruel/natural v1.1.0
|
|
||||||
github.com/marusama/semaphore/v2 v2.5.0
|
github.com/marusama/semaphore/v2 v2.5.0
|
||||||
github.com/mholt/archiver/v3 v3.5.1
|
github.com/mholt/archiver/v3 v3.5.1
|
||||||
github.com/shirou/gopsutil/v3 v3.23.8
|
github.com/shirou/gopsutil/v3 v3.23.8
|
||||||
|
@ -20,10 +19,9 @@ require (
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
|
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
|
||||||
golang.org/x/crypto v0.12.0
|
golang.org/x/crypto v0.14.0
|
||||||
golang.org/x/image v0.12.0
|
golang.org/x/image v0.12.0
|
||||||
golang.org/x/text v0.13.0
|
golang.org/x/text v0.13.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
@ -50,8 +48,8 @@ require (
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||||
go.etcd.io/bbolt v1.3.4 // indirect
|
go.etcd.io/bbolt v1.3.4 // indirect
|
||||||
golang.org/x/net v0.10.0 // indirect
|
golang.org/x/net v0.17.0 // indirect
|
||||||
golang.org/x/sys v0.11.0 // indirect
|
golang.org/x/sys v0.13.0 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|
|
@ -204,8 +204,6 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
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/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
github.com/maruel/natural v1.1.0 h1:2z1NgP/Vae+gYrtC0VuvrTJ6U35OuyUqDdfluLqMWuQ=
|
|
||||||
github.com/maruel/natural v1.1.0/go.mod h1:eFVhYCcUOfZFxXoDZam8Ktya72wa79fNC3lc/leA0DQ=
|
|
||||||
github.com/marusama/semaphore/v2 v2.5.0 h1:o/1QJD9DBYOWRnDhPwDVAXQn6mQYD0gZaS1Tpx6DJGM=
|
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/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 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||||
|
@ -282,8 +280,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
|
@ -359,8 +357,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
@ -427,8 +425,9 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
@ -595,8 +594,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
|
|
@ -112,7 +112,7 @@ type signupBody struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
if !settings.GlobalConfiguration.Auth.Signup {
|
if !settings.Config.Auth.Signup {
|
||||||
return http.StatusMethodNotAllowed, nil
|
return http.StatusMethodNotAllowed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
|
||||||
Username: info.Username,
|
Username: info.Username,
|
||||||
Password: info.Password,
|
Password: info.Password,
|
||||||
}
|
}
|
||||||
settings.GlobalConfiguration.UserDefaults.Apply(user)
|
settings.Config.UserDefaults.Apply(user)
|
||||||
userHome, err := d.settings.MakeUserDir(user.Username, user.Scope, d.server.Root)
|
userHome, err := d.settings.MakeUserDir(user.Username, user.Scope, d.server.Root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
||||||
|
@ -142,7 +142,7 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
|
||||||
}
|
}
|
||||||
user.Scope = userHome
|
user.Scope = userHome
|
||||||
log.Printf("new user: %s, home dir: [%s].", user.Username, userHome)
|
log.Printf("new user: %s, home dir: [%s].", user.Username, userHome)
|
||||||
settings.GlobalConfiguration.UserDefaults.Apply(user)
|
settings.Config.UserDefaults.Apply(user)
|
||||||
err = d.store.Users.Save(user)
|
err = d.store.Users.Save(user)
|
||||||
if err == errors.ErrExist {
|
if err == errors.ErrExist {
|
||||||
return http.StatusConflict, err
|
return http.StatusConflict, err
|
||||||
|
|
|
@ -45,7 +45,7 @@ func previewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, re
|
||||||
return http.StatusBadRequest, err
|
return http.StatusBadRequest, err
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := files.NewFileInfo(files.FileOptions{
|
file, err := files.FileInfoFaster(files.FileOptions{
|
||||||
Fs: d.user.Fs,
|
Fs: d.user.Fs,
|
||||||
Path: "/" + vars["path"],
|
Path: "/" + vars["path"],
|
||||||
Modify: d.user.Perm.Modify,
|
Modify: d.user.Perm.Modify,
|
||||||
|
@ -53,10 +53,10 @@ func previewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, re
|
||||||
ReadHeader: d.server.TypeDetectionByHeader,
|
ReadHeader: d.server.TypeDetectionByHeader,
|
||||||
Checker: d,
|
Checker: d,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
}
|
}
|
||||||
|
|
||||||
setContentDisposition(w, r, file)
|
setContentDisposition(w, r, file)
|
||||||
|
|
||||||
switch file.Type {
|
switch file.Type {
|
||||||
|
@ -81,7 +81,6 @@ func handleImagePreview(
|
||||||
(previewSize == PreviewSizeThumb && !enableThumbnails) {
|
(previewSize == PreviewSizeThumb && !enableThumbnails) {
|
||||||
return rawFileHandler(w, r, file)
|
return rawFileHandler(w, r, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
format, err := imgSvc.FormatFromExtension(file.Extension)
|
format, err := imgSvc.FormatFromExtension(file.Extension)
|
||||||
// Unsupported extensions directly return the raw data
|
// Unsupported extensions directly return the raw data
|
||||||
if err == img.ErrUnsupportedFormat || format == img.FormatGif {
|
if err == img.ErrUnsupportedFormat || format == img.FormatGif {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/files"
|
"github.com/gtsteffaniak/filebrowser/files"
|
||||||
"github.com/gtsteffaniak/filebrowser/share"
|
"github.com/gtsteffaniak/filebrowser/share"
|
||||||
|
"github.com/gtsteffaniak/filebrowser/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
var withHashFile = func(fn handleFunc) handleFunc {
|
var withHashFile = func(fn handleFunc) handleFunc {
|
||||||
|
@ -35,7 +36,7 @@ var withHashFile = func(fn handleFunc) handleFunc {
|
||||||
|
|
||||||
d.user = user
|
d.user = user
|
||||||
|
|
||||||
file, err := files.NewFileInfo(files.FileOptions{
|
file, err := files.FileInfoFaster(files.FileOptions{
|
||||||
Fs: d.user.Fs,
|
Fs: d.user.Fs,
|
||||||
Path: link.Path,
|
Path: link.Path,
|
||||||
Modify: d.user.Perm.Modify,
|
Modify: d.user.Perm.Modify,
|
||||||
|
@ -62,7 +63,7 @@ var withHashFile = func(fn handleFunc) handleFunc {
|
||||||
// set fs root to the shared file/folder
|
// set fs root to the shared file/folder
|
||||||
d.user.Fs = afero.NewBasePathFs(d.user.Fs, basePath)
|
d.user.Fs = afero.NewBasePathFs(d.user.Fs, basePath)
|
||||||
|
|
||||||
file, err = files.NewFileInfo(files.FileOptions{
|
file, err = files.FileInfoFaster(files.FileOptions{
|
||||||
Fs: d.user.Fs,
|
Fs: d.user.Fs,
|
||||||
Path: filePath,
|
Path: filePath,
|
||||||
Modify: d.user.Perm.Modify,
|
Modify: d.user.Perm.Modify,
|
||||||
|
@ -98,8 +99,7 @@ var publicShareHandler = withHashFile(func(w http.ResponseWriter, r *http.Reques
|
||||||
file := d.raw.(*files.FileInfo)
|
file := d.raw.(*files.FileInfo)
|
||||||
|
|
||||||
if file.IsDir {
|
if file.IsDir {
|
||||||
file.Listing.Sorting = files.Sorting{By: "name", Asc: false}
|
file.Listing.Sorting = users.Sorting{By: "name", Asc: false}
|
||||||
file.Listing.ApplySort()
|
|
||||||
return renderJSON(w, r, file)
|
return renderJSON(w, r, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,7 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
|
||||||
return http.StatusAccepted, nil
|
return http.StatusAccepted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := files.NewFileInfo(files.FileOptions{
|
file, err := files.FileInfoFaster(files.FileOptions{
|
||||||
Fs: d.user.Fs,
|
Fs: d.user.Fs,
|
||||||
Path: r.URL.Path,
|
Path: r.URL.Path,
|
||||||
Modify: d.user.Perm.Modify,
|
Modify: d.user.Perm.Modify,
|
||||||
|
|
|
@ -20,7 +20,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
file, err := files.NewFileInfo(files.FileOptions{
|
file, err := files.FileInfoFaster(files.FileOptions{
|
||||||
Fs: d.user.Fs,
|
Fs: d.user.Fs,
|
||||||
Path: r.URL.Path,
|
Path: r.URL.Path,
|
||||||
Modify: d.user.Perm.Modify,
|
Modify: d.user.Perm.Modify,
|
||||||
|
@ -32,13 +32,10 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
}
|
}
|
||||||
|
|
||||||
if file.IsDir {
|
if file.IsDir {
|
||||||
file.Listing.Sorting = d.user.Sorting
|
file.Listing.Sorting = d.user.Sorting
|
||||||
file.Listing.ApplySort()
|
|
||||||
return renderJSON(w, r, file)
|
return renderJSON(w, r, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
if checksum := r.URL.Query().Get("checksum"); checksum != "" {
|
if checksum := r.URL.Query().Get("checksum"); checksum != "" {
|
||||||
err := file.Checksum(checksum)
|
err := file.Checksum(checksum)
|
||||||
if err == errors.ErrInvalidOption {
|
if err == errors.ErrInvalidOption {
|
||||||
|
@ -46,9 +43,6 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// do not waste bandwidth if we just want the checksum
|
|
||||||
file.Content = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderJSON(w, r, file)
|
return renderJSON(w, r, file)
|
||||||
|
@ -60,7 +54,7 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := files.NewFileInfo(files.FileOptions{
|
file, err := files.FileInfoFaster(files.FileOptions{
|
||||||
Fs: d.user.Fs,
|
Fs: d.user.Fs,
|
||||||
Path: r.URL.Path,
|
Path: r.URL.Path,
|
||||||
Modify: d.user.Perm.Modify,
|
Modify: d.user.Perm.Modify,
|
||||||
|
@ -102,7 +96,7 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := files.NewFileInfo(files.FileOptions{
|
file, err := files.FileInfoFaster(files.FileOptions{
|
||||||
Fs: d.user.Fs,
|
Fs: d.user.Fs,
|
||||||
Path: r.URL.Path,
|
Path: r.URL.Path,
|
||||||
Modify: d.user.Perm.Modify,
|
Modify: d.user.Perm.Modify,
|
||||||
|
@ -308,7 +302,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
|
||||||
src = path.Clean("/" + src)
|
src = path.Clean("/" + src)
|
||||||
dst = path.Clean("/" + dst)
|
dst = path.Clean("/" + dst)
|
||||||
|
|
||||||
file, err := files.NewFileInfo(files.FileOptions{
|
file, err := files.FileInfoFaster(files.FileOptions{
|
||||||
Fs: d.user.Fs,
|
Fs: d.user.Fs,
|
||||||
Path: src,
|
Path: src,
|
||||||
Modify: d.user.Perm.Modify,
|
Modify: d.user.Perm.Modify,
|
||||||
|
@ -338,14 +332,13 @@ type DiskUsageResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
file, err := files.NewFileInfo(files.FileOptions{
|
file, err := files.FileInfoFaster(files.FileOptions{
|
||||||
Fs: d.user.Fs,
|
Fs: d.user.Fs,
|
||||||
Path: r.URL.Path,
|
Path: r.URL.Path,
|
||||||
Modify: d.user.Perm.Modify,
|
Modify: d.user.Perm.Modify,
|
||||||
Expand: false,
|
Expand: false,
|
||||||
ReadHeader: false,
|
ReadHeader: false,
|
||||||
Checker: d,
|
Checker: d,
|
||||||
Content: false,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
|
|
|
@ -4,7 +4,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/index"
|
"github.com/gtsteffaniak/filebrowser/files"
|
||||||
|
"github.com/gtsteffaniak/filebrowser/settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var searchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var searchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
|
@ -13,7 +14,7 @@ var searchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *dat
|
||||||
// Retrieve the User-Agent and X-Auth headers from the request
|
// Retrieve the User-Agent and X-Auth headers from the request
|
||||||
sessionId := r.Header.Get("SessionId")
|
sessionId := r.Header.Get("SessionId")
|
||||||
userScope := r.Header.Get("UserScope")
|
userScope := r.Header.Get("UserScope")
|
||||||
index := *index.GetIndex()
|
index := files.GetIndex(settings.Config.Server.Root)
|
||||||
combinedScope := strings.TrimPrefix(userScope+r.URL.Path, ".")
|
combinedScope := strings.TrimPrefix(userScope+r.URL.Path, ".")
|
||||||
combinedScope = strings.TrimPrefix(combinedScope, "/")
|
combinedScope = strings.TrimPrefix(combinedScope, "/")
|
||||||
results, fileTypes := index.Search(query, combinedScope, sessionId)
|
results, fileTypes := index.Search(query, combinedScope, sessionId)
|
||||||
|
|
|
@ -21,9 +21,9 @@ type settingsData struct {
|
||||||
|
|
||||||
var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
data := &settingsData{
|
data := &settingsData{
|
||||||
Signup: settings.GlobalConfiguration.Auth.Signup,
|
Signup: settings.Config.Auth.Signup,
|
||||||
CreateUserDir: settings.GlobalConfiguration.Server.CreateUserDir,
|
CreateUserDir: settings.Config.Server.CreateUserDir,
|
||||||
UserHomeBasePath: settings.GlobalConfiguration.Server.UserHomeBasePath,
|
UserHomeBasePath: settings.Config.Server.UserHomeBasePath,
|
||||||
Defaults: d.settings.UserDefaults,
|
Defaults: d.settings.UserDefaults,
|
||||||
Rules: d.settings.Rules,
|
Rules: d.settings.Rules,
|
||||||
Frontend: d.settings.Frontend,
|
Frontend: d.settings.Frontend,
|
||||||
|
|
|
@ -30,12 +30,12 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
|
||||||
"Name": d.settings.Frontend.Name,
|
"Name": d.settings.Frontend.Name,
|
||||||
"DisableExternal": d.settings.Frontend.DisableExternal,
|
"DisableExternal": d.settings.Frontend.DisableExternal,
|
||||||
"DisableUsedPercentage": d.settings.Frontend.DisableUsedPercentage,
|
"DisableUsedPercentage": d.settings.Frontend.DisableUsedPercentage,
|
||||||
"darkMode": settings.GlobalConfiguration.UserDefaults.DarkMode,
|
"darkMode": settings.Config.UserDefaults.DarkMode,
|
||||||
"Color": d.settings.Frontend.Color,
|
"Color": d.settings.Frontend.Color,
|
||||||
"BaseURL": d.server.BaseURL,
|
"BaseURL": d.server.BaseURL,
|
||||||
"Version": version.Version,
|
"Version": version.Version,
|
||||||
"StaticURL": path.Join(d.server.BaseURL, "/static"),
|
"StaticURL": path.Join(d.server.BaseURL, "/static"),
|
||||||
"Signup": settings.GlobalConfiguration.Auth.Signup,
|
"Signup": settings.Config.Auth.Signup,
|
||||||
"NoAuth": d.settings.Auth.Method == "noauth",
|
"NoAuth": d.settings.Auth.Method == "noauth",
|
||||||
"AuthMethod": d.settings.Auth.Method,
|
"AuthMethod": d.settings.Auth.Method,
|
||||||
"LoginPage": auther.LoginPage(),
|
"LoginPage": auther.LoginPage(),
|
||||||
|
|
|
@ -19,6 +19,11 @@ var (
|
||||||
NonModifiableFieldsForNonAdmin = []string{"Username", "Scope", "LockPassword", "Perm", "Commands", "Rules"}
|
NonModifiableFieldsForNonAdmin = []string{"Username", "Scope", "LockPassword", "Perm", "Commands", "Rules"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SortingSettings represents the sorting settings.
|
||||||
|
type Sorting struct {
|
||||||
|
By string `json:"by"`
|
||||||
|
Asc bool `json:"asc"`
|
||||||
|
}
|
||||||
type modifyUserRequest struct {
|
type modifyUserRequest struct {
|
||||||
modifyRequest
|
modifyRequest
|
||||||
Data *users.User `json:"data"`
|
Data *users.User `json:"data"`
|
||||||
|
|
|
@ -1,138 +0,0 @@
|
||||||
package index
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/settings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Index struct {
|
|
||||||
Dirs []string
|
|
||||||
Files []string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
rootPath string = "/srv"
|
|
||||||
indexes Index
|
|
||||||
indexMutex sync.RWMutex
|
|
||||||
lastIndexed time.Time
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetIndex() *Index {
|
|
||||||
return &indexes
|
|
||||||
}
|
|
||||||
|
|
||||||
func Initialize(intervalMinutes uint32) {
|
|
||||||
// Initialize the index
|
|
||||||
indexes = Index{
|
|
||||||
Dirs: []string{},
|
|
||||||
Files: []string{},
|
|
||||||
}
|
|
||||||
rootPath = settings.GlobalConfiguration.Server.Root
|
|
||||||
var numFiles, numDirs int
|
|
||||||
log.Println("Indexing files...")
|
|
||||||
lastIndexedStart := time.Now()
|
|
||||||
// Call the function to index files and directories
|
|
||||||
totalNumFiles, totalNumDirs, err := indexFiles(rootPath, &numFiles, &numDirs)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
lastIndexed = lastIndexedStart
|
|
||||||
go indexingScheduler(intervalMinutes)
|
|
||||||
log.Println("Successfully indexed files.")
|
|
||||||
log.Println("Files found :", totalNumFiles)
|
|
||||||
log.Println("Directories found :", totalNumDirs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func indexingScheduler(intervalMinutes uint32) {
|
|
||||||
log.Printf("Indexing scheduler will run every %v minutes", intervalMinutes)
|
|
||||||
for {
|
|
||||||
indexes.Dirs = slices.Compact(indexes.Dirs)
|
|
||||||
indexes.Files = slices.Compact(indexes.Files)
|
|
||||||
time.Sleep(time.Duration(intervalMinutes) * time.Minute)
|
|
||||||
var numFiles, numDirs int
|
|
||||||
lastIndexedStart := time.Now()
|
|
||||||
totalNumFiles, totalNumDirs, err := indexFiles(rootPath, &numFiles, &numDirs)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
lastIndexed = lastIndexedStart
|
|
||||||
if totalNumFiles+totalNumDirs > 0 {
|
|
||||||
log.Println("re-indexing found changes and updated the index.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeFromSlice(slice []string, target string) []string {
|
|
||||||
for i, s := range slice {
|
|
||||||
if s == target {
|
|
||||||
// Swap the target element with the last element
|
|
||||||
slice[i], slice[len(slice)-1] = slice[len(slice)-1], slice[i]
|
|
||||||
// Resize the slice to exclude the last element
|
|
||||||
slice = slice[:len(slice)-1]
|
|
||||||
break // Exit the loop, assuming there's only one target element
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return slice
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define a function to recursively index files and directories
|
|
||||||
func indexFiles(path string, numFiles *int, numDirs *int) (int, int, error) {
|
|
||||||
// Check if the current directory has been modified since last indexing
|
|
||||||
dir, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
// directory must have been deleted, remove from index
|
|
||||||
indexes.Dirs = removeFromSlice(indexes.Dirs, path)
|
|
||||||
indexes.Files = removeFromSlice(indexes.Files, path)
|
|
||||||
}
|
|
||||||
defer dir.Close()
|
|
||||||
dirInfo, err := dir.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return *numFiles, *numDirs, err
|
|
||||||
}
|
|
||||||
// Compare the last modified time of the directory with the last indexed time
|
|
||||||
if dirInfo.ModTime().Before(lastIndexed) {
|
|
||||||
return *numFiles, *numDirs, nil
|
|
||||||
}
|
|
||||||
// Read the directory contents
|
|
||||||
files, err := dir.Readdir(-1)
|
|
||||||
if err != nil {
|
|
||||||
return *numFiles, *numDirs, err
|
|
||||||
}
|
|
||||||
// Iterate over the files and directories
|
|
||||||
for _, file := range files {
|
|
||||||
if file.IsDir() {
|
|
||||||
*numDirs++
|
|
||||||
addToIndex(path, file.Name(), true)
|
|
||||||
_, _, err := indexFiles(path+"/"+file.Name(), numFiles, numDirs) // recursive
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Could not index :", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
*numFiles++
|
|
||||||
addToIndex(path, file.Name(), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return *numFiles, *numDirs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func addToIndex(path string, fileName string, isDir bool) {
|
|
||||||
indexMutex.Lock()
|
|
||||||
defer indexMutex.Unlock()
|
|
||||||
path = strings.TrimPrefix(path, rootPath+"/")
|
|
||||||
path = strings.TrimSuffix(path, "/")
|
|
||||||
adjustedPath := path + "/" + fileName
|
|
||||||
if path == rootPath {
|
|
||||||
adjustedPath = fileName
|
|
||||||
}
|
|
||||||
if isDir {
|
|
||||||
indexes.Dirs = append(indexes.Dirs, adjustedPath)
|
|
||||||
} else {
|
|
||||||
indexes.Files = append(indexes.Files, adjustedPath)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,21 +3,23 @@ package settings
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/goccy/go-yaml"
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/gtsteffaniak/filebrowser/users"
|
"github.com/gtsteffaniak/filebrowser/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
var GlobalConfiguration Settings
|
var Config Settings
|
||||||
|
|
||||||
func Initialize(configFile string) {
|
func Initialize(configFile string) {
|
||||||
yamlData := loadConfigFile(configFile)
|
yamlData := loadConfigFile(configFile)
|
||||||
GlobalConfiguration = setDefaults()
|
Config = setDefaults()
|
||||||
err := yaml.Unmarshal(yamlData, &GlobalConfiguration)
|
err := yaml.Unmarshal(yamlData, &Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
||||||
}
|
}
|
||||||
GlobalConfiguration.UserDefaults.Perm = GlobalConfiguration.UserDefaults.Permissions
|
Config.UserDefaults.Perm = Config.UserDefaults.Permissions
|
||||||
|
Config.Server.Root = strings.TrimSuffix(Config.Server.Root, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfigFile(configFile string) []byte {
|
func loadConfigFile(configFile string) []byte {
|
||||||
|
@ -25,7 +27,7 @@ func loadConfigFile(configFile string) []byte {
|
||||||
yamlFile, err := os.Open(configFile)
|
yamlFile, err := os.Open(configFile)
|
||||||
if err != nil {
|
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)
|
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)
|
||||||
GlobalConfiguration = setDefaults()
|
Config = setDefaults()
|
||||||
return []byte{}
|
return []byte{}
|
||||||
}
|
}
|
||||||
defer yamlFile.Close()
|
defer yamlFile.Close()
|
||||||
|
|
|
@ -12,14 +12,14 @@ func TestConfigLoadChanged(t *testing.T) {
|
||||||
yamlData := loadConfigFile("./testingConfig.yaml")
|
yamlData := loadConfigFile("./testingConfig.yaml")
|
||||||
// Marshal the YAML data to a more human-readable format
|
// Marshal the YAML data to a more human-readable format
|
||||||
newConfig := setDefaults()
|
newConfig := setDefaults()
|
||||||
GlobalConfiguration := setDefaults()
|
Config := setDefaults()
|
||||||
|
|
||||||
err := yaml.Unmarshal(yamlData, &newConfig)
|
err := yaml.Unmarshal(yamlData, &newConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
||||||
}
|
}
|
||||||
// Use go-cmp to compare the two structs
|
// Use go-cmp to compare the two structs
|
||||||
if diff := cmp.Diff(newConfig, GlobalConfiguration); diff == "" {
|
if diff := cmp.Diff(newConfig, Config); diff == "" {
|
||||||
t.Errorf("No change when there should have been (-want +got):\n%s", diff)
|
t.Errorf("No change when there should have been (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ func TestConfigLoadSpecificValues(t *testing.T) {
|
||||||
yamlData := loadConfigFile("./testingConfig.yaml")
|
yamlData := loadConfigFile("./testingConfig.yaml")
|
||||||
// Marshal the YAML data to a more human-readable format
|
// Marshal the YAML data to a more human-readable format
|
||||||
newConfig := setDefaults()
|
newConfig := setDefaults()
|
||||||
GlobalConfiguration := setDefaults()
|
Config := setDefaults()
|
||||||
|
|
||||||
err := yaml.Unmarshal(yamlData, &newConfig)
|
err := yaml.Unmarshal(yamlData, &newConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -39,16 +39,16 @@ func TestConfigLoadSpecificValues(t *testing.T) {
|
||||||
globalVal interface{}
|
globalVal interface{}
|
||||||
newVal interface{}
|
newVal interface{}
|
||||||
}{
|
}{
|
||||||
{"Auth.Method", GlobalConfiguration.Auth.Method, newConfig.Auth.Method},
|
{"Auth.Method", Config.Auth.Method, newConfig.Auth.Method},
|
||||||
{"Auth.Method", GlobalConfiguration.Auth.Method, newConfig.Auth.Method},
|
{"Auth.Method", Config.Auth.Method, newConfig.Auth.Method},
|
||||||
{"Frontend.disableExternal", GlobalConfiguration.Frontend.DisableExternal, newConfig.Frontend.DisableExternal},
|
{"Frontend.disableExternal", Config.Frontend.DisableExternal, newConfig.Frontend.DisableExternal},
|
||||||
{"UserDefaults.HideDotfiles", GlobalConfiguration.UserDefaults.HideDotfiles, newConfig.UserDefaults.HideDotfiles},
|
{"UserDefaults.HideDotfiles", Config.UserDefaults.HideDotfiles, newConfig.UserDefaults.HideDotfiles},
|
||||||
{"Server.Database", GlobalConfiguration.Server.Database, newConfig.Server.Database},
|
{"Server.Database", Config.Server.Database, newConfig.Server.Database},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
if tc.globalVal == tc.newVal {
|
if tc.globalVal == tc.newVal {
|
||||||
t.Errorf("Differences should have been found:\n\tGlobalConfig.%s: %v \n\tSetConfig: %v \n", tc.fieldName, tc.globalVal, tc.newVal)
|
t.Errorf("Differences should have been found:\n\tConfig.%s: %v \n\tSetConfig: %v \n", tc.fieldName, tc.globalVal, tc.newVal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/files"
|
|
||||||
"github.com/gtsteffaniak/filebrowser/rules"
|
"github.com/gtsteffaniak/filebrowser/rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,6 +20,12 @@ type Permissions struct {
|
||||||
Download bool `json:"download"`
|
Download bool `json:"download"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SortingSettings represents the sorting settings.
|
||||||
|
type Sorting struct {
|
||||||
|
By string `json:"by"`
|
||||||
|
Asc bool `json:"asc"`
|
||||||
|
}
|
||||||
|
|
||||||
// User describes a user.
|
// User describes a user.
|
||||||
type User struct {
|
type User struct {
|
||||||
DarkMode bool `json:"darkMode"`
|
DarkMode bool `json:"darkMode"`
|
||||||
|
@ -35,7 +40,7 @@ type User struct {
|
||||||
SingleClick bool `json:"singleClick"`
|
SingleClick bool `json:"singleClick"`
|
||||||
Perm Permissions `json:"perm"`
|
Perm Permissions `json:"perm"`
|
||||||
Commands []string `json:"commands"`
|
Commands []string `json:"commands"`
|
||||||
Sorting files.Sorting `json:"sorting"`
|
Sorting Sorting `json:"sorting"`
|
||||||
Fs afero.Fs `json:"-" yaml:"-"`
|
Fs afero.Fs `json:"-" yaml:"-"`
|
||||||
Rules []rules.Rule `json:"rules"`
|
Rules []rules.Rule `json:"rules"`
|
||||||
HideDotfiles bool `json:"hideDotfiles"`
|
HideDotfiles bool `json:"hideDotfiles"`
|
||||||
|
|
|
@ -2,7 +2,7 @@ package version
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Version is the current File Browser version.
|
// Version is the current File Browser version.
|
||||||
Version = "(0.2.1)"
|
Version = "(0.2.2)"
|
||||||
// CommitSHA is the commmit sha.
|
// CommitSHA is the commmit sha.
|
||||||
CommitSHA = "(unknown)"
|
CommitSHA = "(unknown)"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
|
:class="{ activebutton: this.isMaximized && this.isSelected}"
|
||||||
class="item"
|
class="item"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
@ -13,15 +14,20 @@
|
||||||
:aria-label="name"
|
:aria-label="name"
|
||||||
:aria-selected="isSelected"
|
:aria-selected="isSelected"
|
||||||
>
|
>
|
||||||
<div>
|
<div
|
||||||
|
@click="toggleClick"
|
||||||
|
:class="{ activetitle: this.isMaximized && this.isSelected }"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-if="readOnly == undefined && type === 'image' && isThumbsEnabled"
|
v-if="readOnly === undefined && type === 'image' && isThumbsEnabled && isInView"
|
||||||
v-lazy="thumbnailUrl"
|
v-lazy="thumbnailUrl"
|
||||||
|
:class="{ activeimg: this.isMaximized && this.isSelected }"
|
||||||
|
ref="thumbnail"
|
||||||
/>
|
/>
|
||||||
<i v-else class="material-icons"></i>
|
<i :class="{ iconActive: this.isMaximized && this.isSelected }" v-else class="material-icons"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div :class="{ activecontent: this.isMaximized && this.isSelected }">
|
||||||
<p class="name">{{ name }}</p>
|
<p class="name">{{ name }}</p>
|
||||||
|
|
||||||
<p v-if="isDir" class="size" data-order="-1">—</p>
|
<p v-if="isDir" class="size" data-order="-1">—</p>
|
||||||
|
@ -34,6 +40,27 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.activebutton {
|
||||||
|
height: 10em;
|
||||||
|
}
|
||||||
|
.activecontent {
|
||||||
|
height: 5em !important;
|
||||||
|
display: grid !important;
|
||||||
|
}
|
||||||
|
.activeimg {
|
||||||
|
width: 8em !important;
|
||||||
|
height: 8em !important;
|
||||||
|
}
|
||||||
|
.iconActive {
|
||||||
|
font-size: 6em !important;
|
||||||
|
}
|
||||||
|
.activetitle {
|
||||||
|
width: 9em !important;
|
||||||
|
margin-right: 1em !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { enableThumbs } from "@/utils/constants";
|
import { enableThumbs } from "@/utils/constants";
|
||||||
import { getHumanReadableFilesize } from "@/utils/filesizes";
|
import { getHumanReadableFilesize } from "@/utils/filesizes";
|
||||||
|
@ -46,6 +73,8 @@ export default {
|
||||||
name: "item",
|
name: "item",
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
isThumbnailInView: false,
|
||||||
|
isMaximized: false,
|
||||||
touches: 0,
|
touches: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -63,6 +92,16 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(["user", "selected", "req", "jwt"]),
|
...mapState(["user", "selected", "req", "jwt"]),
|
||||||
...mapGetters(["selectedCount"]),
|
...mapGetters(["selectedCount"]),
|
||||||
|
isClicked() {
|
||||||
|
if (this.user.singleClick || !this.allowedView ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Assuming toggleClick returns a boolean value
|
||||||
|
return !this.isMaximized;
|
||||||
|
},
|
||||||
|
allowedView() {
|
||||||
|
return this.user.viewMode != "gallery" && this.user.viewMode != "normal"
|
||||||
|
},
|
||||||
singleClick() {
|
singleClick() {
|
||||||
return this.readOnly == undefined && this.user.singleClick;
|
return this.readOnly == undefined && this.user.singleClick;
|
||||||
},
|
},
|
||||||
|
@ -84,8 +123,12 @@ export default {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
thumbnailUrl() {
|
thumbnailUrl() {
|
||||||
|
let path = this.req.path
|
||||||
|
if (this.req.path == "/") {
|
||||||
|
path = ""
|
||||||
|
}
|
||||||
const file = {
|
const file = {
|
||||||
path: this.path,
|
path: path +"/"+this.name,
|
||||||
modified: this.modified,
|
modified: this.modified,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -94,11 +137,41 @@ export default {
|
||||||
isThumbsEnabled() {
|
isThumbsEnabled() {
|
||||||
return enableThumbs;
|
return enableThumbs;
|
||||||
},
|
},
|
||||||
|
isInView() {
|
||||||
|
return enableThumbs;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const observer = new IntersectionObserver(this.handleIntersect, {
|
||||||
|
root: null,
|
||||||
|
rootMargin: '0px',
|
||||||
|
threshold: 0.5, // Adjust threshold as needed
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the thumbnail element and start observing
|
||||||
|
const thumbnailElement = this.$refs.thumbnail; // Add ref="thumbnail" to the <img> tag
|
||||||
|
if (thumbnailElement) {
|
||||||
|
observer.observe(thumbnailElement);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleIntersect(entries, observer) {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
this.isThumbnailInView = true;
|
||||||
|
// Stop observing once thumbnail is in view
|
||||||
|
observer.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
toggleClick() {
|
||||||
|
this.isMaximized = this.isClicked;
|
||||||
|
},
|
||||||
...mapMutations(["addSelected", "removeSelected", "resetSelected"]),
|
...mapMutations(["addSelected", "removeSelected", "resetSelected"]),
|
||||||
humanSize: function () {
|
humanSize: function () {
|
||||||
return this.type == "invalid_link" ? "invalid link" : getHumanReadableFilesize(this.size);
|
return this.type == "invalid_link"
|
||||||
|
? "invalid link"
|
||||||
|
: getHumanReadableFilesize(this.size);
|
||||||
},
|
},
|
||||||
humanTime: function () {
|
humanTime: function () {
|
||||||
if (this.readOnly == undefined && this.user.dateFormat) {
|
if (this.readOnly == undefined && this.user.dateFormat) {
|
||||||
|
@ -231,7 +304,6 @@ export default {
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.singleClick &&
|
!this.singleClick &&
|
||||||
!event.ctrlKey &&
|
!event.ctrlKey &&
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
.dashboard .row {
|
.dashboard .row {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0 -.5em;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
#listing {
|
#listing {
|
||||||
--item-selected: white;
|
--item-selected: white;
|
||||||
|
transition: all;
|
||||||
|
animation-duration: 0.25s;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.rtl #listing {
|
body.rtl #listing {
|
||||||
|
@ -67,6 +69,7 @@ body.rtl #listing {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
margin-right: 0.1em;
|
margin-right: 0.1em;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
|
border-radius: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
|
@ -87,19 +90,19 @@ body.rtl #listing {
|
||||||
|
|
||||||
#listing {
|
#listing {
|
||||||
padding-top: 1em;
|
padding-top: 1em;
|
||||||
margin: 0 -0.5em;
|
padding-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.gallery .item,
|
#listing.gallery .item,
|
||||||
#listing.compact .item,
|
#listing.compact .item,
|
||||||
#listing.normal .item,
|
#listing.normal .item,
|
||||||
#listing.list .item {
|
#listing.list .item {
|
||||||
max-width: 300px;
|
|
||||||
margin: .5em;
|
margin: .5em;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12);
|
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.gallery .item {
|
#listing.gallery .item {
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
|
@ -153,6 +156,7 @@ body.rtl #listing {
|
||||||
#listing.gallery .item img {
|
#listing.gallery .item img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
border-radius: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.gallery .size,
|
#listing.gallery .size,
|
||||||
|
@ -294,6 +298,7 @@ body.rtl #listing {
|
||||||
#listing.list .item div:first-of-type img {
|
#listing.list .item div:first-of-type img {
|
||||||
width: 2em;
|
width: 2em;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
|
border-radius: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.list .item div:last-of-type {
|
#listing.list .item div:last-of-type {
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
margin-bottom: 5em;
|
margin-bottom: 5em;
|
||||||
}
|
}
|
||||||
#listing .item {
|
#listing .item {
|
||||||
min-width: 100%
|
width: 100%
|
||||||
}
|
}
|
||||||
body.rtl #listing {
|
body.rtl #listing {
|
||||||
margin-right: unset;
|
margin-right: unset;
|
||||||
|
|
|
@ -152,7 +152,7 @@
|
||||||
"images": "الصور",
|
"images": "الصور",
|
||||||
"music": "الموسيقى",
|
"music": "الموسيقى",
|
||||||
"pdf": "PDF",
|
"pdf": "PDF",
|
||||||
"pressToSearch": "No results found in indexed search.",
|
"pressToSearch": "Press enter to search...",
|
||||||
"search": "البحث...",
|
"search": "البحث...",
|
||||||
"typeToSearch": "Type to search...",
|
"typeToSearch": "Type to search...",
|
||||||
"types": "الأنواع",
|
"types": "الأنواع",
|
||||||
|
|
|
@ -36,7 +36,8 @@
|
||||||
"toggleSidebar": "Seitenleiste anzeigen",
|
"toggleSidebar": "Seitenleiste anzeigen",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"openFile": "Datei öffnen"
|
"openFile": "Datei öffnen",
|
||||||
|
"continue": "Fortfahren"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "Download Datei",
|
"downloadFile": "Download Datei",
|
||||||
|
@ -147,7 +148,7 @@
|
||||||
"rename": "Umbenennen",
|
"rename": "Umbenennen",
|
||||||
"renameMessage": "Fügen Sie einen Namen ein für",
|
"renameMessage": "Fügen Sie einen Namen ein für",
|
||||||
"replace": "Ersetzen",
|
"replace": "Ersetzen",
|
||||||
"replaceMessage": "Eine der Datei mit dem gleichen Namen, wie die Sie hochladen wollen, existiert bereits. Soll die vorhandene Datei ersetzt werden ?\n",
|
"replaceMessage": "Eine der Datei mit dem gleichen Namen, wie die Sie hochladen wollen, existiert bereits. Soll die vorhandene Datei übersprungen oder ersetzt werden?\n",
|
||||||
"schedule": "Plan",
|
"schedule": "Plan",
|
||||||
"scheduleMessage": "Wählen Sie ein Datum und eine Zeit für die Veröffentlichung dieses Beitrags.",
|
"scheduleMessage": "Wählen Sie ein Datum und eine Zeit für die Veröffentlichung dieses Beitrags.",
|
||||||
"show": "Anzeigen",
|
"show": "Anzeigen",
|
||||||
|
@ -184,10 +185,14 @@
|
||||||
"commandRunnerHelp": "Hier könne Sie Befehle eintragen, welche bei den benannten Aktionen ausgeführt werden. Sie müssen pro Zeile jeweils einen Befehl eingeben. Die Umgebungsvariable {0} und {1} sind verfügbar, wobei {0} relative zu {1} ist. Für mehr Informationen über diese Funktion und die verfügbaren Umgebungsvariablen, lesen Sie bitte die {2}.",
|
"commandRunnerHelp": "Hier könne Sie Befehle eintragen, welche bei den benannten Aktionen ausgeführt werden. Sie müssen pro Zeile jeweils einen Befehl eingeben. Die Umgebungsvariable {0} und {1} sind verfügbar, wobei {0} relative zu {1} ist. Für mehr Informationen über diese Funktion und die verfügbaren Umgebungsvariablen, lesen Sie bitte die {2}.",
|
||||||
"commandsUpdated": "Befehle aktualisiert!",
|
"commandsUpdated": "Befehle aktualisiert!",
|
||||||
"createUserDir": "Automatisches Erstellen des Home-Verzeichnisses beim Anlegen neuer Benutzer",
|
"createUserDir": "Automatisches Erstellen des Home-Verzeichnisses beim Anlegen neuer Benutzer",
|
||||||
|
"tusUploads": "Gestückelter Upload",
|
||||||
|
"tusUploadsHelp": "File Browser unterstützt das Hochladen von gestückelten Dateien und ermöglicht so einen effizienten, zuverlässigen, fortsetzbaren und gestückelten Datei-Upload auch in unzuverlässigen Netzwerken.",
|
||||||
|
"tusUploadsChunkSize": "Gibt die maximale Größe pro Anfrage an (direkte Uploads werden für kleinere Uploads verwendet). Bitte geben Sie eine Byte-Angabe oder eine Zeichenfolge wie 10 MB, 1 GB usw. an",
|
||||||
|
"tusUploadsRetryCount": "Anzahl der Wiederholungsversuche, wenn das Hochladen eines Stückes fehlschlägt.",
|
||||||
"customStylesheet": "Individuelles Stylesheet",
|
"customStylesheet": "Individuelles Stylesheet",
|
||||||
"defaultUserDescription": "Das sind die Standardeinstellung für Benutzer",
|
"defaultUserDescription": "Das sind die Standardeinstellung für Benutzer",
|
||||||
"disableExternalLinks": "Externe Links deaktivieren (außer Dokumentation)",
|
"disableExternalLinks": "Externe Links deaktivieren (außer Dokumentation)",
|
||||||
"disableUsedDiskPercentage": "Disable used disk percentage graph",
|
"disableUsedDiskPercentage": "Diagramm zur Festplattennutzung deaktivieren",
|
||||||
"documentation": "Dokumentation",
|
"documentation": "Dokumentation",
|
||||||
"examples": "Beispiele",
|
"examples": "Beispiele",
|
||||||
"executeOnShell": "In Shell ausführen",
|
"executeOnShell": "In Shell ausführen",
|
||||||
|
|
|
@ -0,0 +1,282 @@
|
||||||
|
{
|
||||||
|
"buttons": {
|
||||||
|
"cancel": "Ακύρωση",
|
||||||
|
"close": "Κλείσιμο",
|
||||||
|
"copy": "Αντιγραφή",
|
||||||
|
"copyFile": "Αντιγραφή αρχείου",
|
||||||
|
"copyToClipboard": "Αντιγραφή στο πρόχειρο",
|
||||||
|
"copyDownloadLinkToClipboard": "Αντιγραφή συνδέσμου λήψης στο πρόχειρο",
|
||||||
|
"create": "Δημιουργία",
|
||||||
|
"delete": "Διαγραφή",
|
||||||
|
"download": "Λήψη",
|
||||||
|
"file": "Αρχείο",
|
||||||
|
"folder": "Φάκελος",
|
||||||
|
"hideDotfiles": "Απόκρυψη κρυφών αρχείων",
|
||||||
|
"info": "Πληροφορίες",
|
||||||
|
"more": "Περισσότερα",
|
||||||
|
"move": "Μετακίνηση",
|
||||||
|
"moveFile": "Μετακίνηση αρχείου",
|
||||||
|
"new": "Νέο",
|
||||||
|
"next": "Επόμενο",
|
||||||
|
"ok": "Εντάξει",
|
||||||
|
"permalink": "Λήψη μόνιμου συνδέσμου",
|
||||||
|
"previous": "Προηγούμενο",
|
||||||
|
"publish": "Δημοσίευση",
|
||||||
|
"rename": "Μετονομασία",
|
||||||
|
"replace": "Αντικατάσταση",
|
||||||
|
"reportIssue": "Αναφορά προβλήματος",
|
||||||
|
"save": "Αποθήκευση",
|
||||||
|
"schedule": "Προγραμματισμός",
|
||||||
|
"search": "Αναζήτηση",
|
||||||
|
"select": "Επιλογή",
|
||||||
|
"selectMultiple": "Επιλογή πολλαπλών",
|
||||||
|
"share": "Κοινοποίηση",
|
||||||
|
"submit": "Υποβολή",
|
||||||
|
"switchView": "Εναλλαγή προβολής",
|
||||||
|
"toggleSidebar": "(Απ-)ενεργοποίησης της πλευρικής μπάρας",
|
||||||
|
"update": "Ενημέρωση",
|
||||||
|
"upload": "Μεταφόρτωση",
|
||||||
|
"openFile": "Άνοιγμα αρχείου",
|
||||||
|
"continue": "Συνέχεια"
|
||||||
|
},
|
||||||
|
"download": {
|
||||||
|
"downloadFile": "Λήψη αρχείου",
|
||||||
|
"downloadFolder": "Λήψη φακέλου",
|
||||||
|
"downloadSelected": "Λήψη επιλεγμένων"
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"abortUpload": "Είστε σίγουροι ότι θέλετε να διακόψετε τη μεταφόρτωση;"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"forbidden": "Δεν έχετε άδεια πρόσβασης σε αυτό.",
|
||||||
|
"internal": "Προέκυψε εσωτερικό σφάλμα.",
|
||||||
|
"notFound": "Αυτή η τοποθεσία δεν μπορεί να βρεθεί.",
|
||||||
|
"connection": "Ο διακομιστής δεν είναι διαθέσιμος."
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"body": "Περιεχόμενο",
|
||||||
|
"clear": "Καθαρισμός",
|
||||||
|
"closePreview": "Κλείσιμο προεπισκόπησης",
|
||||||
|
"files": "Αρχεία",
|
||||||
|
"folders": "Φάκελοι",
|
||||||
|
"home": "Αρχική",
|
||||||
|
"lastModified": "Τελευταία τροποποίηση",
|
||||||
|
"loading": "Φορτώνει…",
|
||||||
|
"lonely": "Δεν υπάρχει τίποτα εδώ (ακόμη)…",
|
||||||
|
"metadata": "Μεταδεδομένα",
|
||||||
|
"multipleSelectionEnabled": "Ενεργοποιημένη επιλογή πολλαπλών",
|
||||||
|
"name": "Όνομα",
|
||||||
|
"size": "Μέγεθος",
|
||||||
|
"sortByLastModified": "Ταξινόμηση κατά πρόσφατη τροποποίηση",
|
||||||
|
"sortByName": "Ταξινόμηση κατά όνομα",
|
||||||
|
"sortBySize": "Ταξινόμηση κατά μέγεθος",
|
||||||
|
"noPreview": "Η προεπισκόπηση δεν είναι διαθέσιμη για αυτό το αρχείο."
|
||||||
|
},
|
||||||
|
"help": {
|
||||||
|
"click": "επιλέξτε αρχείο ή φάκελο",
|
||||||
|
"ctrl": {
|
||||||
|
"click": "επιλογή πολλαπλών αρχείων ή φακέλων",
|
||||||
|
"f": "ανοίγει την αναζήτηση",
|
||||||
|
"s": "αποθηκεύει ένα αρχείο ή εκκινεί λήψη του φακέλου στον οποίο βρίσκεστε"
|
||||||
|
},
|
||||||
|
"del": "διαγραφή επιλεγμένων στοιχείων",
|
||||||
|
"doubleClick": "ανοίγει ένα αρχείο ή φάκελο",
|
||||||
|
"esc": "καθαρίζει την επιλογή ή/και κλείνει το παράθυρο",
|
||||||
|
"f1": "αυτή η πληροφορία",
|
||||||
|
"f2": "μετονομασία αρχείου",
|
||||||
|
"help": "Βοήθεια"
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"he": "עברית",
|
||||||
|
"hu": "Magyar",
|
||||||
|
"ar": "العربية",
|
||||||
|
"de": "Deutsch",
|
||||||
|
"en": "English",
|
||||||
|
"es": "Español",
|
||||||
|
"el": "Ελληνικά",
|
||||||
|
"fr": "Français",
|
||||||
|
"is": "Icelandic",
|
||||||
|
"it": "Italiano",
|
||||||
|
"ja": "日本語",
|
||||||
|
"ko": "한국어",
|
||||||
|
"nlBE": "Dutch (Belgium)",
|
||||||
|
"pl": "Polski",
|
||||||
|
"pt": "Português",
|
||||||
|
"ptBR": "Português (Brasil)",
|
||||||
|
"ro": "Romanian",
|
||||||
|
"ru": "Русский",
|
||||||
|
"sk": "Slovenčina",
|
||||||
|
"svSE": "Swedish (Sweden)",
|
||||||
|
"tr": "Türkçe",
|
||||||
|
"ua": "Українська",
|
||||||
|
"zhCN": "中文 (简体)",
|
||||||
|
"zhTW": "中文 (繁體)"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"createAnAccount": "Δημιουργία λογαριασμού",
|
||||||
|
"loginInstead": "Έχετε ήδη λογαριασμό",
|
||||||
|
"password": "Κωδικός πρόσβασης",
|
||||||
|
"passwordConfirm": "Επιβεβαίωση κωδικού πρόσβασης",
|
||||||
|
"passwordsDontMatch": "Οι κωδικοί πρόσβασης δεν ταιριάζουν",
|
||||||
|
"signup": "Εγγραφή",
|
||||||
|
"submit": "Είσοδος",
|
||||||
|
"username": "Όνομα χρήστη",
|
||||||
|
"usernameTaken": "Το όνομα χρήστη χρησιμοποιείται ήδη",
|
||||||
|
"wrongCredentials": "Λάθος όνομα ή/και κωδικός πρόσβασης"
|
||||||
|
},
|
||||||
|
"permanent": "Μόνιμο",
|
||||||
|
"prompts": {
|
||||||
|
"copy": "Αντιγραφή",
|
||||||
|
"copyMessage": "Επιλέξτε τοποθεσία για αντιγραφή των αρχείων σας:",
|
||||||
|
"deleteMessageMultiple": "Είστε σίγουροι ότι θέλετε να διαγράψετε {count} αρχεία;",
|
||||||
|
"deleteMessageSingle": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το αρχείο/φάκελο;",
|
||||||
|
"deleteMessageShare": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτή την κοινοποίηση ({path});",
|
||||||
|
"deleteTitle": "Διαγραφή αρχείων",
|
||||||
|
"displayName": "Εμφάνιση ονόματος:",
|
||||||
|
"download": "Λήψη αρχείων",
|
||||||
|
"downloadMessage": "Επιλέξτε τη μορφή που θέλετε να λάβετε.",
|
||||||
|
"error": "Προέκυψε κάποιο σφάλμα",
|
||||||
|
"fileInfo": "Πληροφορίες αρχείου",
|
||||||
|
"filesSelected": "Επιλέχθηκαν {count} αρχεία.",
|
||||||
|
"lastModified": "Τελευταία τροποποίηση",
|
||||||
|
"move": "Μετακίνηση",
|
||||||
|
"moveMessage": "Επιλέξτε νέα τοποθεσία για τα αρχεία / τους φακέλους σας:",
|
||||||
|
"newArchetype": "Δημιουργία νέας ανάρτησης με βάση έναν αρχέτυπο. Το αρχείο σας θα δημιουργηθεί στο φάκελο περιεχομένου.",
|
||||||
|
"newDir": "Νέος φάκελος",
|
||||||
|
"newDirMessage": "Γράψτε το όνομα του νέου φακέλου.",
|
||||||
|
"newFile": "Νέο αρχείο",
|
||||||
|
"newFileMessage": "Γράψτε το όνομα του νέου αρχείου.",
|
||||||
|
"numberDirs": "Αριθμός φακέλων",
|
||||||
|
"numberFiles": "Αριθμός αρχείων",
|
||||||
|
"rename": "Μετονομασία",
|
||||||
|
"renameMessage": "Εισαγάγετε ένα νέο όνομα για το",
|
||||||
|
"replace": "Αντικατάσταση",
|
||||||
|
"replaceMessage": "Ένα από τα αρχεία που προσπαθείτε να μεταφορτώσετε δημιουργεί σύγκρουση με υπάρχον αρχείο λόγω του ονόματός του. Θέλετε να συνεχίσετε τη μεταφόρτωση ή να αντικαταστήσετε το υπάρχον;\n",
|
||||||
|
"schedule": "Προγραμματισμός",
|
||||||
|
"scheduleMessage": "Επιλέξτε μια ημερομηνία και ώρα για τον προγραμματισμό της δημοσίευσης αυτής της ανάρτησης.",
|
||||||
|
"show": "Εμφάνιση",
|
||||||
|
"size": "Μέγεθος",
|
||||||
|
"upload": "Μεταφόρτωση",
|
||||||
|
"uploadFiles": "Μεταφόρτωση {files} αρχείων…",
|
||||||
|
"uploadMessage": "Επιλέξτε μια επιλογή για τη μεταφόρτωση.",
|
||||||
|
"optionalPassword": "Προαιρετικός κωδικός πρόσβασης"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"images": "Εικόνες",
|
||||||
|
"music": "Μουσική",
|
||||||
|
"pdf": "PDF",
|
||||||
|
"pressToSearch": "Πατήστε Enter για αναζήτηση…",
|
||||||
|
"search": "Αναζήτηση…",
|
||||||
|
"typeToSearch": "Πληκτρολογήστε για αναζήτηση…",
|
||||||
|
"types": "Τύποι",
|
||||||
|
"video": "Βίντεο"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"admin": "Διαχειριστής",
|
||||||
|
"administrator": "Διαχειριστής",
|
||||||
|
"allowCommands": "Εκτέλεση εντολών",
|
||||||
|
"allowEdit": "Επεξεργασία, μετονομασία και διαγραφή αρχείων ή φακέλων",
|
||||||
|
"allowNew": "Δημιουργία νέων αρχείων και φακέλων",
|
||||||
|
"allowPublish": "Δημοσίευση νέων αναρτήσεων και σελίδων",
|
||||||
|
"allowSignup": "Να επιτρέπεται η εγγραφή νέων χρηστών",
|
||||||
|
"avoidChanges": "(αφήστε το κενό για αποφυγή αλλαγών)",
|
||||||
|
"branding": "Εξατομίκευση",
|
||||||
|
"brandingDirectoryPath": "Διαδρομή φακέλου εξατομίκευσης",
|
||||||
|
"brandingHelp": "Μπορείτε να προσαρμόσετε την εμφάνισης της εφαρμογής File Browser αλλάζοντας το όνομά της, αντικαθιστώντας το λογότυπό της, προσθέτοντας προσαρμοσμένα στυλ και ακόμα και απενεργοποιώντας εξωτερικούς συνδέσμους προς το GitHub.\nΓια περισσότερες πληροφορίες σχετικά με αυτές τις προσαρμογές, ελέγξτε το {0}.",
|
||||||
|
"changePassword": "Αλλαγή κωδικού πρόσβασης",
|
||||||
|
"commandRunner": "Εκτέλεση εντολών",
|
||||||
|
"commandRunnerHelp": "Εδώ μπορείτε να ορίσετε εντολές που εκτελούνται στα ονομασμένα γεγονότα και δραστηριότητες. Πρέπει να γράψετε μία εντολή ανά γραμμή. Οι μεταβλητές περιβάλλοντος {0} και {1} θα είναι διαθέσιμες, και θα είναι {0} σχετικές με το {1}. Για περισσότερες πληροφορίες σχετικά με αυτή τη λειτουργία και τις διαθέσιμες μεταβλητές περιβάλλοντος, παρακαλώ διαβάστε το {2}.",
|
||||||
|
"commandsUpdated": "Οι εντολές ενημερώθηκαν!",
|
||||||
|
"createUserDir": "Αυτόματη δημιουργία φακέλου χρήστη κατά την προσθήκη νέου χρήστη",
|
||||||
|
"tusUploads": "Τμηματικές μεταφορές αρχείων",
|
||||||
|
"tusUploadsHelp": "Η εφαρμογή File Browser υποστηρίζει τμηματικές μεταφορτώσεις αρχείων, επιτρέποντας την αποδοτική, αξιόπιστη και συνεχιζόμενη μεταφόρτωση αρχείων ακόμα και σε ασταθείς συνδέσεις δικτύου.",
|
||||||
|
"tusUploadsChunkSize": "Υποδεικνύει το μέγιστο μέγεθος ενός αιτήματος μεταφόρτωσης (για μικρότερες μεταφορές αρχείων θα χρησιμοποιηθούν απευθείας και όχι τμηματικές μεταφορτώσεις). Μπορείτε να εισάγετε έναν ακέραιο αριθμό που υποδηλώνει το μέγεθος σε bytes, ή κείμενο με αριθμό και μονάδα μέτρησης μεγέθους δεδομένων, όπως 10MB, 1GB κλπ.",
|
||||||
|
"tusUploadsRetryCount": "Αριθμός επαναληπτικών δοκιμών που θα πραγματοποιηθούν αν αποτύχει η μεταφόρτωση ενός τμήματος.",
|
||||||
|
"userHomeBasePath": "Βασική διαδρομή αρχείων για τους φακέλους των χρηστών",
|
||||||
|
"userScopeGenerationPlaceholder": "Η εμβέλεια εφαρμογής θα δημιουργηθεί αυτόματα",
|
||||||
|
"createUserHomeDirectory": "Δημιουργία φακέλου χρήστη",
|
||||||
|
"customStylesheet": "Προσαρμοσμένο στυλ εμφάνισης (stylesheet)",
|
||||||
|
"defaultUserDescription": "Αυτές είναι οι προεπιλεγμένες ρυθμίσεις για νέους χρήστες.",
|
||||||
|
"disableExternalLinks": "Απενεργοποίηση εξωτερικών συνδέσμων (εκτός από συνδέσμους προς τις οδηγίες χρήσης)",
|
||||||
|
"disableUsedDiskPercentage": "Απενεργοποίηση γραφήματος ποσοστού χρήσης χώρου αποθήκευσης",
|
||||||
|
"documentation": "οδηγίες χρήσης",
|
||||||
|
"examples": "Παραδείγματα",
|
||||||
|
"executeOnShell": "Εκτέλεση στο κέλυφος",
|
||||||
|
"executeOnShellDescription": "Από προεπιλογή, η εφαρμογή File Browser εκτελεί τις εντολές καλώντας τα προγράμματα των εντολών απευθείας. Αν θέλετε να τις εκτελέσετε σε ένα κέλυφος (όπως το Bash ή το PowerShell), μπορείτε να το καθορίσετε εδώ με τις απαιτούμενες παραμέτρους. Εάν οριστεί, η εντολή που εκτελείτε θα προστίθεται ως παράμετρος. Αυτό ισχύει τόσο για τις εντολές χρήστη όσο και για τους αγκίστρους συμβάντων (event hooks).",
|
||||||
|
"globalRules": "Πρόκειται για ένα γενικό σύνολο κανόνων που επιτρέπουν και απαγορεύουν διάφορες λειτουργίες και ισχύουν για κάθε χρήστη. Μπορείτε να καθορίσετε συγκεκριμένους κανόνες στις ρυθμίσεις κάθε χρήστη για να παρακάμψετε τους γενικούς κανόνες.",
|
||||||
|
"globalSettings": "Γενικές ρυθμίσεις",
|
||||||
|
"hideDotfiles": "Απόκρυψη κρυφών αρχείων (dotfiles)",
|
||||||
|
"insertPath": "Εισάγετε διαδρομή",
|
||||||
|
"insertRegex": "Εισάγετε έκφραση regex",
|
||||||
|
"instanceName": "Όνομα περιβάλλοντος",
|
||||||
|
"language": "Γλώσσα",
|
||||||
|
"lockPassword": "Αποτρέψτε τον χρήστη από την αλλαγή του κωδικού πρόσβασης",
|
||||||
|
"newPassword": "Νέος κωδικός πρόσβασης",
|
||||||
|
"newPasswordConfirm": "Επιβεβαιώστε τον νέο κωδικό πρόσβασης",
|
||||||
|
"newUser": "Νέος χρήστης",
|
||||||
|
"password": "Κωδικός πρόσβασης",
|
||||||
|
"passwordUpdated": "Ο κωδικός πρόσβασης ενημερώθηκε!",
|
||||||
|
"path": "Διαδρομή",
|
||||||
|
"perm": {
|
||||||
|
"create": "Δημιουργία αρχείων και φακέλων",
|
||||||
|
"delete": "Διαγραφή αρχείων και φακέλων",
|
||||||
|
"download": "Λήψη",
|
||||||
|
"execute": "Εκτέλεση εντολών",
|
||||||
|
"modify": "Επεξεργασία αρχείων",
|
||||||
|
"rename": "Μετονομασία ή μετακίνηση αρχείων και φακέλων",
|
||||||
|
"share": "Κοινοποίηση αρχείων"
|
||||||
|
},
|
||||||
|
"permissions": "Δικαιώματα",
|
||||||
|
"permissionsHelp": "Μπορείτε να ορίσετε τον χρήστη ως διαχειριστή ή να επιλέξετε τα δικαιώματα μεμονωμένα. Αν επιλέξετε \"Διαχειριστής\", όλες οι υπόλοιπες επιλογές θα είναι αυτόματα επιλεγμένες. Η διαχείριση χρηστών παραμένει προνόμιο ενός χρήστη με τον ρόλο του διαχειριστή.\n",
|
||||||
|
"profileSettings": "Ρυθμίσεις προφίλ",
|
||||||
|
"ruleExample1": "αποκλείει την πρόσβαση σε οποιοδήποτε κρυφό αρχείο (όπως .git, .gitignore) σε κάθε φάκελο.\n",
|
||||||
|
"ruleExample2": "αποκλείει την πρόσβαση στο αρχείο με το όνομα Caddyfile στον ριζικό φάκελο της εμβέλειας του κανόνα.",
|
||||||
|
"rules": "Κανόνες",
|
||||||
|
"rulesHelp": "Εδώ μπορείτε να ορίσετε ένα σύνολο κανόνων που επιτρέπουν και απαγορεύουν διάφορες λειτουργίες για τον συγκεκριμένο χρήστη. Τα αποκλεισμένα αρχεία δεν θα εμφανίζονται στα περιεχόμενα των αντίστοιχων φακέλων και δεν θα είναι προσβάσιμα από τον χρήστη. Υποστηρίζονται εκφράσεις regex και διαδρομές σχετικές με την εμβέλεια αρχείων των χρηστών.\n",
|
||||||
|
"scope": "Εμβέλεια",
|
||||||
|
"setDateFormat": "Ορισμός ακριβούς μορφής ημερομηνίας",
|
||||||
|
"settingsUpdated": "Οι ρυθμίσεις ενημερώθηκαν!",
|
||||||
|
"shareDuration": "Διάρκεια κοινοποίησης",
|
||||||
|
"shareManagement": "Διαχείριση κοινοποίησης",
|
||||||
|
"shareDeleted": "Η κοινοποίηση διαγράφηκε!",
|
||||||
|
"singleClick": "Χρήση μονού κλικ για να ανοίξετε αρχεία και φακέλους",
|
||||||
|
"themes": {
|
||||||
|
"dark": "Σκοτεινό",
|
||||||
|
"light": "Φωτεινό",
|
||||||
|
"title": "Μοτίβο"
|
||||||
|
},
|
||||||
|
"user": "Χρήστης",
|
||||||
|
"userCommands": "Εντολές χρήστη",
|
||||||
|
"userCommandsHelp": "Μια λίστα με τις διαθέσιμες εντολές για αυτόν το χρήστη, χωρισμένες μεταξύ τους με κενά. Παράδειγμα:\n",
|
||||||
|
"userCreated": "Ο χρήστης δημιουργήθηκε!",
|
||||||
|
"userDefaults": "Προεπιλεγμένες ρυθμίσεις χρήστη",
|
||||||
|
"userDeleted": "Ο χρήστης διαγράφηκε!",
|
||||||
|
"userManagement": "Διαχείριση χρηστών",
|
||||||
|
"userUpdated": "Ο χρήστης ενημερώθηκε!",
|
||||||
|
"username": "Όνομα χρήστη",
|
||||||
|
"users": "Χρήστες"
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"help": "Βοήθεια",
|
||||||
|
"hugoNew": "Νέο Hugo",
|
||||||
|
"login": "Σύνδεση",
|
||||||
|
"logout": "Αποσύνδεση",
|
||||||
|
"myFiles": "Τα αρχεία μου",
|
||||||
|
"newFile": "Νέο αρχείο",
|
||||||
|
"newFolder": "Νέος φάκελος",
|
||||||
|
"preview": "Προεπισκόπηση",
|
||||||
|
"settings": "Ρυθμίσεις",
|
||||||
|
"signup": "Εγγραφή",
|
||||||
|
"siteSettings": "Ρυθμίσεις ιστότοπου"
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"linkCopied": "Ο σύνδεσμος αντιγράφηκε!"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"days": "Ημέρες",
|
||||||
|
"hours": "Ώρες",
|
||||||
|
"minutes": "Λεπτά",
|
||||||
|
"seconds": "Δευτερόλεπτα",
|
||||||
|
"unit": "Μονάδα χρόνου"
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,13 +37,17 @@
|
||||||
"toggleSidebar": "Toggle sidebar",
|
"toggleSidebar": "Toggle sidebar",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"openFile": "Open file"
|
"openFile": "Open file",
|
||||||
|
"continue": "Continue"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "Download File",
|
"downloadFile": "Download File",
|
||||||
"downloadFolder": "Download Folder",
|
"downloadFolder": "Download Folder",
|
||||||
"downloadSelected": "Download Selected"
|
"downloadSelected": "Download Selected"
|
||||||
},
|
},
|
||||||
|
"upload": {
|
||||||
|
"abortUpload": "Are you sure you want to abort?"
|
||||||
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"forbidden": "You don't have permissions to access this.",
|
"forbidden": "You don't have permissions to access this.",
|
||||||
"internal": "Something really went wrong.",
|
"internal": "Something really went wrong.",
|
||||||
|
@ -88,6 +92,7 @@
|
||||||
"hu": "Magyar",
|
"hu": "Magyar",
|
||||||
"ar": "العربية",
|
"ar": "العربية",
|
||||||
"de": "Deutsch",
|
"de": "Deutsch",
|
||||||
|
"el": "Ελληνικά",
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"es": "Español",
|
"es": "Español",
|
||||||
"fr": "Français",
|
"fr": "Français",
|
||||||
|
@ -148,7 +153,7 @@
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"renameMessage": "Insert a new name for",
|
"renameMessage": "Insert a new name for",
|
||||||
"replace": "Replace",
|
"replace": "Replace",
|
||||||
"replaceMessage": "One of the files you're trying to upload is conflicting because of its name. Do you wish to replace the existing one?\n",
|
"replaceMessage": "One of the files you're trying to upload is conflicting because of its name. Do you wish to continue to upload or replace the existing one?\n",
|
||||||
"schedule": "Schedule",
|
"schedule": "Schedule",
|
||||||
"scheduleMessage": "Pick a date and time to schedule the publication of this post.",
|
"scheduleMessage": "Pick a date and time to schedule the publication of this post.",
|
||||||
"show": "Show",
|
"show": "Show",
|
||||||
|
@ -164,7 +169,7 @@
|
||||||
"pdf": "PDF",
|
"pdf": "PDF",
|
||||||
"pressToSearch": "No results found in indexed search.",
|
"pressToSearch": "No results found in indexed search.",
|
||||||
"search": "Search...",
|
"search": "Search...",
|
||||||
"typeToSearch": "Type to search... (3 characters minimum)",
|
"typeToSearch": "Type to search... (3 character minimum)",
|
||||||
"types": "Types",
|
"types": "Types",
|
||||||
"video": "Video"
|
"video": "Video"
|
||||||
},
|
},
|
||||||
|
@ -185,18 +190,22 @@
|
||||||
"commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.",
|
"commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.",
|
||||||
"commandsUpdated": "Commands updated!",
|
"commandsUpdated": "Commands updated!",
|
||||||
"createUserDir": "Auto create user home dir while adding new user",
|
"createUserDir": "Auto create user home dir while adding new user",
|
||||||
|
"tusUploads": "Chunked Uploads",
|
||||||
|
"tusUploadsHelp": "File Browser supports chunked file uploads, allowing for the creation of efficient, reliable, resumable and chunked file uploads even on unreliable networks.",
|
||||||
|
"tusUploadsChunkSize": "Indicates to maximum size of a request (direct uploads will be used for smaller uploads). You may input a plain integer denoting a bytes input or a string like 10MB, 1GB etc.",
|
||||||
|
"tusUploadsRetryCount": "Number of retries to perform if a chunk fails to upload.",
|
||||||
"userHomeBasePath": "Base path for user home directories",
|
"userHomeBasePath": "Base path for user home directories",
|
||||||
"userScopeGenerationPlaceholder": "The scope will be auto generated",
|
"userScopeGenerationPlaceholder": "The scope will be auto generated",
|
||||||
"createUserHomeDirectory": "Create user home directory",
|
"createUserHomeDirectory": "Create user home directory",
|
||||||
"customStylesheet": "Custom Stylesheet",
|
"customStylesheet": "Custom Stylesheet",
|
||||||
"defaultUserDescription": "This are the default settings for new users.",
|
"defaultUserDescription": "These are the default settings for new users.",
|
||||||
"disableExternalLinks": "Disable external links (except documentation)",
|
"disableExternalLinks": "Disable external links (except documentation)",
|
||||||
"disableUsedDiskPercentage": "Disable used disk percentage graph",
|
"disableUsedDiskPercentage": "Disable used disk percentage graph",
|
||||||
"documentation": "documentation",
|
"documentation": "documentation",
|
||||||
"examples": "Examples",
|
"examples": "Examples",
|
||||||
"executeOnShell": "Execute on shell",
|
"executeOnShell": "Execute on shell",
|
||||||
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
|
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
|
||||||
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.",
|
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override these ones.",
|
||||||
"globalSettings": "Global Settings",
|
"globalSettings": "Global Settings",
|
||||||
"hideDotfiles": "Hide dotfiles",
|
"hideDotfiles": "Hide dotfiles",
|
||||||
"insertPath": "Insert the path",
|
"insertPath": "Insert the path",
|
||||||
|
|
|
@ -5,6 +5,7 @@ import he from "./he.json";
|
||||||
import hu from "./hu.json";
|
import hu from "./hu.json";
|
||||||
import ar from "./ar.json";
|
import ar from "./ar.json";
|
||||||
import de from "./de.json";
|
import de from "./de.json";
|
||||||
|
import el from "./el.json";
|
||||||
import en from "./en.json";
|
import en from "./en.json";
|
||||||
import es from "./es.json";
|
import es from "./es.json";
|
||||||
import fr from "./fr.json";
|
import fr from "./fr.json";
|
||||||
|
@ -38,6 +39,9 @@ export function detectLocale() {
|
||||||
case /^ar.*/i.test(locale):
|
case /^ar.*/i.test(locale):
|
||||||
locale = "ar";
|
locale = "ar";
|
||||||
break;
|
break;
|
||||||
|
case /^el.*/i.test(locale):
|
||||||
|
locale = "el";
|
||||||
|
break;
|
||||||
case /^es.*/i.test(locale):
|
case /^es.*/i.test(locale):
|
||||||
locale = "es";
|
locale = "es";
|
||||||
break;
|
break;
|
||||||
|
@ -114,6 +118,7 @@ const i18n = new VueI18n({
|
||||||
hu: removeEmpty(hu),
|
hu: removeEmpty(hu),
|
||||||
ar: removeEmpty(ar),
|
ar: removeEmpty(ar),
|
||||||
de: removeEmpty(de),
|
de: removeEmpty(de),
|
||||||
|
el: removeEmpty(el),
|
||||||
en: en,
|
en: en,
|
||||||
es: removeEmpty(es),
|
es: removeEmpty(es),
|
||||||
fr: removeEmpty(fr),
|
fr: removeEmpty(fr),
|
||||||
|
|
|
@ -152,7 +152,7 @@
|
||||||
"images": "画像",
|
"images": "画像",
|
||||||
"music": "音楽",
|
"music": "音楽",
|
||||||
"pdf": "PDF",
|
"pdf": "PDF",
|
||||||
"pressToSearch": "No results found in indexed search.",
|
"pressToSearch": "Press enter to search...",
|
||||||
"search": "検索...",
|
"search": "検索...",
|
||||||
"typeToSearch": "Type to search...",
|
"typeToSearch": "Type to search...",
|
||||||
"types": "種類",
|
"types": "種類",
|
||||||
|
|
|
@ -8,6 +8,7 @@ import Vue from "@/utils/vue";
|
||||||
import { recaptcha, loginPage } from "@/utils/constants";
|
import { recaptcha, loginPage } from "@/utils/constants";
|
||||||
import { login, validateLogin } from "@/utils/auth";
|
import { login, validateLogin } from "@/utils/auth";
|
||||||
import App from "@/App";
|
import App from "@/App";
|
||||||
|
export const eventBus = new Vue(); // creating an event bus.
|
||||||
|
|
||||||
cssVars();
|
cssVars();
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,14 @@ import upload from "./modules/upload";
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
|
editor: null,
|
||||||
user: null,
|
user: null,
|
||||||
req: {},
|
req: {
|
||||||
|
sorting: {
|
||||||
|
by: 'name', // Initial sorting field
|
||||||
|
asc: true, // Initial sorting order
|
||||||
|
},
|
||||||
|
},
|
||||||
oldReq: {},
|
oldReq: {},
|
||||||
clipboard: {
|
clipboard: {
|
||||||
key: "",
|
key: "",
|
||||||
|
|
|
@ -74,6 +74,25 @@ const mutations = {
|
||||||
state.oldReq = state.req;
|
state.oldReq = state.req;
|
||||||
state.req = value;
|
state.req = value;
|
||||||
},
|
},
|
||||||
|
// Inside your mutations object
|
||||||
|
updateListingSortConfig(state, { field, asc }) {
|
||||||
|
state.req.sorting.by = field;
|
||||||
|
state.req.sorting.asc = asc;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateListingItems(state) {
|
||||||
|
// Sort the items array based on the sorting settings
|
||||||
|
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) {
|
||||||
|
return valueA > valueB ? 1 : -1;
|
||||||
|
} else {
|
||||||
|
return valueA < valueB ? 1 : -1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
updateClipboard: (state, value) => {
|
updateClipboard: (state, value) => {
|
||||||
state.clipboard.key = value.key;
|
state.clipboard.key = value.key;
|
||||||
state.clipboard.items = value.items;
|
state.clipboard.items = value.items;
|
||||||
|
|
|
@ -100,15 +100,6 @@ export function scanFiles(dt) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectType(mimetype) {
|
|
||||||
if (mimetype.startsWith("video")) return "video";
|
|
||||||
if (mimetype.startsWith("audio")) return "audio";
|
|
||||||
if (mimetype.startsWith("image")) return "image";
|
|
||||||
if (mimetype.startsWith("pdf")) return "pdf";
|
|
||||||
if (mimetype.startsWith("text")) return "text";
|
|
||||||
return "blob";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleFiles(files, base, overwrite = false) {
|
export function handleFiles(files, base, overwrite = false) {
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
let id = store.state.upload.id;
|
let id = store.state.upload.id;
|
||||||
|
@ -130,7 +121,7 @@ export function handleFiles(files, base, overwrite = false) {
|
||||||
path,
|
path,
|
||||||
file,
|
file,
|
||||||
overwrite,
|
overwrite,
|
||||||
...(!file.isDir && { type: detectType(file.type) }),
|
...(!file.isDir && { type: file.type }),
|
||||||
};
|
};
|
||||||
|
|
||||||
store.dispatch("upload/upload", item);
|
store.dispatch("upload/upload", item);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<breadcrumbs base="/files" />
|
<breadcrumbs base="/files" />
|
||||||
|
|
||||||
<errors v-if="error" :errorCode="error.status" />
|
<errors v-if="error" :errorCode="error.status" />
|
||||||
<component v-else-if="currentView" :is="currentView"></component>
|
<component v-else-if="currentView" :is="currentView"></component>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
@ -24,8 +23,9 @@ import { mapState, mapMutations } from "vuex";
|
||||||
import HeaderBar from "@/components/header/HeaderBar";
|
import HeaderBar from "@/components/header/HeaderBar";
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs";
|
import Breadcrumbs from "@/components/Breadcrumbs";
|
||||||
import Errors from "@/views/Errors";
|
import Errors from "@/views/Errors";
|
||||||
import Preview from "@/views/files/Preview";
|
import Preview from "@/views/files/Preview.vue";
|
||||||
import Listing from "@/views/files/Listing";
|
import Listing from "@/views/files/Listing.vue";
|
||||||
|
import Editor from "@/views/files/Editor.vue";
|
||||||
|
|
||||||
function clean(path) {
|
function clean(path) {
|
||||||
return path.endsWith("/") ? path.slice(0, -1) : path;
|
return path.endsWith("/") ? path.slice(0, -1) : path;
|
||||||
|
@ -39,7 +39,7 @@ export default {
|
||||||
Errors,
|
Errors,
|
||||||
Preview,
|
Preview,
|
||||||
Listing,
|
Listing,
|
||||||
Editor: () => import("@/views/files/Editor"),
|
Editor,
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -85,6 +85,14 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
|
main {
|
||||||
|
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
main::-webkit-scrollbar {
|
||||||
|
display: none; /* Safari and Chrome */
|
||||||
|
}
|
||||||
/* Use the class .dark-mode to apply styles conditionally */
|
/* Use the class .dark-mode to apply styles conditionally */
|
||||||
.dark-mode {
|
.dark-mode {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
|
|
|
@ -163,7 +163,7 @@ import moment from "moment";
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs";
|
import Breadcrumbs from "@/components/Breadcrumbs";
|
||||||
import Errors from "@/views/Errors";
|
import Errors from "@/views/Errors";
|
||||||
import QrcodeVue from "qrcode.vue";
|
import QrcodeVue from "qrcode.vue";
|
||||||
import Item from "@/components/files/ListingItem";
|
import Item from "@/components/files/ListingItem.vue";
|
||||||
import Clipboard from "clipboard";
|
import Clipboard from "clipboard";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
<header-bar>
|
<header-bar>
|
||||||
<action icon="close" :label="$t('buttons.close')" @action="close()" />
|
<action icon="close" :label="$t('buttons.close')" @action="close()" />
|
||||||
<title class="topTitle">{{ req.name }}</title>
|
<title class="topTitle">{{ req.name }}</title>
|
||||||
|
|
||||||
<action v-if="user.perm.modify" id="save-button" icon="save" :label="$t('buttons.save')"
|
<action v-if="user.perm.modify" id="save-button" icon="save" :label="$t('buttons.save')"
|
||||||
@action="save()" />
|
@action="save()" />
|
||||||
</header-bar>
|
</header-bar>
|
||||||
|
@ -24,7 +23,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from "vuex";
|
import { mapState } from "vuex";
|
||||||
import { files as api } from "@/api";
|
import { eventBus } from "@/main";
|
||||||
|
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ import HeaderBar from "@/components/header/HeaderBar";
|
||||||
import Action from "@/components/header/Action";
|
import Action from "@/components/header/Action";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "editor",
|
name: "editorBar",
|
||||||
components: {
|
components: {
|
||||||
HeaderBar,
|
HeaderBar,
|
||||||
Action,
|
Action,
|
||||||
|
@ -77,7 +77,6 @@ export default {
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
window.removeEventListener("keydown", this.keyEvent);
|
window.removeEventListener("keydown", this.keyEvent);
|
||||||
this.editor.destroy();
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
back() {
|
back() {
|
||||||
|
@ -99,9 +98,8 @@ export default {
|
||||||
async save() {
|
async save() {
|
||||||
const button = "save";
|
const button = "save";
|
||||||
buttons.loading("save");
|
buttons.loading("save");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.put(this.$route.path, this.editor.getValue());
|
eventBus.$emit("handleEditorValueRequest", "data");
|
||||||
buttons.success(button);
|
buttons.success(button);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
buttons.done(button);
|
buttons.done(button);
|
||||||
|
|
|
@ -5,12 +5,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { eventBus } from "@/main";
|
||||||
import { mapState } from "vuex";
|
import { mapState } from "vuex";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import buttons from "@/utils/buttons";
|
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import ace from "ace-builds/src-min-noconflict/ace.js";
|
import ace from "ace-builds/src-min-noconflict/ace.js";
|
||||||
import modelist from "ace-builds/src-min-noconflict/ext-modelist.js";
|
import "ace-builds/src-min-noconflict/theme-chrome";
|
||||||
|
import "ace-builds/src-min-noconflict/theme-twilight";
|
||||||
|
import { darkMode } from "@/utils/constants";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "editor",
|
name: "editor",
|
||||||
|
@ -18,6 +20,11 @@ export default {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isDarkMode() {
|
||||||
|
return this.user && Object.prototype.hasOwnProperty.call(this.user, "darkMode")
|
||||||
|
? this.user.darkMode
|
||||||
|
: darkMode;
|
||||||
|
},
|
||||||
...mapState(["req", "user"]),
|
...mapState(["req", "user"]),
|
||||||
breadcrumbs() {
|
breadcrumbs() {
|
||||||
let parts = this.$route.path.split("/");
|
let parts = this.$route.path.split("/");
|
||||||
|
@ -58,16 +65,29 @@ export default {
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
const fileContent = this.req.content || "";
|
const fileContent = this.req.content || "";
|
||||||
|
|
||||||
this.editor = ace.edit("editor", {
|
this.editor = ace.edit("editor", {
|
||||||
value: fileContent,
|
value: fileContent,
|
||||||
showPrintMargin: false,
|
showPrintMargin: false,
|
||||||
|
theme: "ace/theme/chrome",
|
||||||
readOnly: this.req.type === "textImmutable",
|
readOnly: this.req.type === "textImmutable",
|
||||||
mode: modelist.getModeForPath(this.req.name).mode,
|
wrap: false,
|
||||||
wrap: true,
|
|
||||||
});
|
});
|
||||||
|
// Set the basePath for Ace Editor
|
||||||
|
ace.config.set("basePath", "/node_modules/ace-builds/src-min-noconflict");
|
||||||
|
if (this.isDarkMode) {
|
||||||
|
this.editor.setTheme("ace/theme/twilight");
|
||||||
|
}
|
||||||
|
eventBus.$on("handleEditorValueRequest", this.handleEditorValueRequest);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleEditorValueRequest() {
|
||||||
|
console.log("trying to save");
|
||||||
|
try {
|
||||||
|
api.put(this.$route.path, this.editor.getValue());
|
||||||
|
} catch (e) {
|
||||||
|
this.$showError(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
back() {
|
back() {
|
||||||
let uri = url.removeLastDir(this.$route.path) + "/";
|
let uri = url.removeLastDir(this.$route.path) + "/";
|
||||||
this.$router.push({ path: uri });
|
this.$router.push({ path: uri });
|
||||||
|
@ -80,22 +100,9 @@ export default {
|
||||||
if (String.fromCharCode(event.which).toLowerCase() !== "s") {
|
if (String.fromCharCode(event.which).toLowerCase() !== "s") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.save();
|
this.save();
|
||||||
},
|
},
|
||||||
async save() {
|
|
||||||
const button = "save";
|
|
||||||
buttons.loading("save");
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.put(this.$route.path, this.editor.getValue());
|
|
||||||
buttons.success(button);
|
|
||||||
} catch (e) {
|
|
||||||
buttons.done(button);
|
|
||||||
this.$showError(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
close() {
|
close() {
|
||||||
this.$store.commit("updateRequest", {});
|
this.$store.commit("updateRequest", {});
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="selectedCount > 0" id="file-selection">
|
<div v-if="selectedCount > 0" id="file-selection">
|
||||||
<span >{{ selectedCount }} selected</span>
|
<span>{{ selectedCount }} selected</span>
|
||||||
<template>
|
<template>
|
||||||
<action
|
<action
|
||||||
v-if="headerButtons.select"
|
v-if="headerButtons.select"
|
||||||
|
@ -137,7 +137,7 @@
|
||||||
<h2>{{ $t("files.folders") }}</h2>
|
<h2>{{ $t("files.folders") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="req.numDirs > 0">
|
<div v-if="req.numDirs > 0" >
|
||||||
<item
|
<item
|
||||||
v-for="item in dirs"
|
v-for="item in dirs"
|
||||||
:key="base64(item.name)"
|
:key="base64(item.name)"
|
||||||
|
@ -209,7 +209,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
.header-items {
|
.header-items {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
|
@ -220,13 +219,13 @@
|
||||||
<script>
|
<script>
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import { mapState, mapGetters, mapMutations } from "vuex";
|
import { mapState, mapGetters, mapMutations } from "vuex";
|
||||||
import { users, files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
import css from "@/utils/css";
|
import css from "@/utils/css";
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
|
|
||||||
import Action from "@/components/header/Action";
|
import Action from "@/components/header/Action";
|
||||||
import Item from "@/components/files/ListingItem";
|
import Item from "@/components/files/ListingItem.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "listing",
|
name: "listing",
|
||||||
|
@ -236,7 +235,8 @@ export default {
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
showLimit: 50,
|
sortField: "name",
|
||||||
|
showLimit: 5000, // new directory limit
|
||||||
columnWidth: 280,
|
columnWidth: 280,
|
||||||
dragCounter: 0,
|
dragCounter: 0,
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
|
@ -266,10 +266,10 @@ export default {
|
||||||
if (item.isDir) {
|
if (item.isDir) {
|
||||||
dirs.push(item);
|
dirs.push(item);
|
||||||
} else {
|
} else {
|
||||||
|
item.Path = this.req.Path
|
||||||
files.push(item);
|
files.push(item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { dirs, files };
|
return { dirs, files };
|
||||||
},
|
},
|
||||||
dirs() {
|
dirs() {
|
||||||
|
@ -313,7 +313,7 @@ export default {
|
||||||
return icons[this.user.viewMode];
|
return icons[this.user.viewMode];
|
||||||
},
|
},
|
||||||
listingViewMode() {
|
listingViewMode() {
|
||||||
return this.user.viewMode
|
return this.user.viewMode;
|
||||||
},
|
},
|
||||||
headerButtons() {
|
headerButtons() {
|
||||||
return {
|
return {
|
||||||
|
@ -679,31 +679,21 @@ export default {
|
||||||
file.style.opacity = 1;
|
file.style.opacity = 1;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async sort(by) {
|
sort(field) {
|
||||||
let asc = false;
|
let asc = false;
|
||||||
|
if (
|
||||||
if (by === "name") {
|
(field === "name" && this.nameIcon === "arrow_upward") ||
|
||||||
if (this.nameIcon === "arrow_upward") {
|
(field === "size" && this.sizeIcon === "arrow_upward") ||
|
||||||
|
(field === "modified" && this.modifiedIcon === "arrow_upward")
|
||||||
|
) {
|
||||||
asc = true;
|
asc = true;
|
||||||
}
|
}
|
||||||
} else if (by === "size") {
|
|
||||||
if (this.sizeIcon === "arrow_upward") {
|
|
||||||
asc = true;
|
|
||||||
}
|
|
||||||
} else if (by === "modified") {
|
|
||||||
if (this.modifiedIcon === "arrow_upward") {
|
|
||||||
asc = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
// Commit the updateSort mutation
|
||||||
await users.update({ id: this.user.id, sorting: { by, asc } }, ["sorting"]);
|
this.$store.commit("updateListingSortConfig", { field, asc });
|
||||||
} catch (e) {
|
this.$store.commit("updateListingItems");
|
||||||
this.$showError(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$store.commit("setReload", true);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
openSearch() {
|
openSearch() {
|
||||||
this.$store.commit("showHover", "search");
|
this.$store.commit("showHover", "search");
|
||||||
},
|
},
|
||||||
|
|
|
@ -43,11 +43,11 @@
|
||||||
and watch it with your favorite video player!
|
and watch it with your favorite video player!
|
||||||
</video>
|
</video>
|
||||||
<object
|
<object
|
||||||
v-else-if="req.extension.toLowerCase() == '.pdf'"
|
v-else-if="req.type == 'pdf'"
|
||||||
class="pdf"
|
class="pdf"
|
||||||
:data="raw"
|
:data="raw"
|
||||||
></object>
|
></object>
|
||||||
<div v-else-if="req.type == 'blob'" class="info">
|
<div v-else-if="req.type == 'blob' || req.type == 'archive'" class="info">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<i class="material-icons">feedback</i>
|
<i class="material-icons">feedback</i>
|
||||||
{{ $t("files.noPreview") }}
|
{{ $t("files.noPreview") }}
|
||||||
|
|
Loading…
Reference in New Issue