Compare commits

..

23 Commits

Author SHA1 Message Date
Henrique Dias
17f1e08a58 chore(release): 2.46.1 2025-11-15 08:51:04 +01:00
Henrique Dias
ffc850454e fix: remove duplicated 'hide-defaults' flag (is 'hideDefaults') (#5548) 2025-11-15 08:50:22 +01:00
Henrique Dias
13814e1119 fix: env key replacer and remove unused function (#5547) 2025-11-15 08:40:37 +01:00
Henrique Dias
4e9e312984 chore(release): 2.46.0 2025-11-14 18:15:33 +01:00
Henrique Dias
ce3b407c51 docs: clarify status 2025-11-14 17:49:52 +01:00
transifex-integration[bot]
fb5d099f85 feat: Updates for project File Browser (#5544) 2025-11-14 17:47:21 +01:00
Omar Hussein
1ace579a55 feat: add context menu (#3343)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-14 16:53:16 +01:00
Lucky Jain
ac7b49c148 feat: add option to hide the login button from public-facing pages (#3922)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-14 16:21:08 +01:00
Henrique Dias
9d44932dba chore: use more standard golangci-lint options 2025-11-14 16:18:12 +01:00
Ahmad Hesam
0d973d3aad feat: add 'hide-dotfiles' as command line parameter (#3802)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-14 08:19:03 +01:00
Henrique Dias
cacc0999e9 chore: let functions be longer 2025-11-14 08:11:10 +01:00
Henrique Dias
42d1b6f3ae docs: fix badge in readme 2025-11-13 18:03:51 +01:00
Henrique Dias
bb10c3dfa9 docs: clarify release 2025-11-13 17:39:51 +01:00
Henrique Dias
ce76aa23a6 chore(release): 2.45.3 2025-11-13 17:39:11 +01:00
Henrique Dias
94b635daf8 ci: fix workflow command 2025-11-13 17:37:50 +01:00
Henrique Dias
31871aaa4b docs: update project status (#5513) 2025-11-13 17:34:52 +01:00
Henrique Dias
9d465663db chore(release): 2.45.3 2025-11-13 17:32:46 +01:00
Henrique Dias
70081f2647 docs: add note about flags and env (#5542) 2025-11-13 17:31:12 +01:00
Henrique Dias
fa9d2f266f ci: simplify the workflows (#5541) 2025-11-13 17:25:59 +01:00
Henrique Dias
8fcfb502ca docs: improve contribution documentation (#5540) 2025-11-13 17:16:50 +01:00
Henrique Dias
0bab2aba9e chore: use Task, split workflows 2025-11-13 16:51:32 +01:00
renovate[bot]
38951d950f chore(deps): update module github.com/golang-jwt/jwt/v4 to v5 (#5535)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-13 16:07:53 +01:00
renovate[bot]
dda8fdbcb2 chore(deps): update module golang.org/x/tools to v0.39.0 (#5522)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-13 14:28:28 +01:00
99 changed files with 1653 additions and 2865 deletions

View File

@@ -1,4 +1,4 @@
name: main name: Continuous Integration
on: on:
push: push:
@@ -9,8 +9,8 @@ on:
pull_request: pull_request:
jobs: jobs:
# linters
lint-frontend: lint-frontend:
name: Lint Frontend
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
@@ -22,26 +22,43 @@ jobs:
node-version: "24.x" node-version: "24.x"
cache: "pnpm" cache: "pnpm"
cache-dependency-path: "frontend/pnpm-lock.yaml" cache-dependency-path: "frontend/pnpm-lock.yaml"
- run: make lint-frontend - working-directory: frontend
run: |
pnpm install --frozen-lockfile
pnpm run lint
lint-backend: lint-backend:
name: Lint Backend
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: actions/setup-go@v6 - uses: actions/setup-go@v6
with: with:
go-version: '1.25' go-version: "1.25.x"
- run: make lint-backend - uses: golangci/golangci-lint-action@v9
lint: with:
runs-on: ubuntu-latest version: "latest"
needs: [lint-frontend, lint-backend]
steps:
- run: echo "done"
# tests test:
test-frontend: name: Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
- run: go test --race ./...
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version: '1.25'
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
with: with:
package_json_file: "frontend/package.json" package_json_file: "frontend/package.json"
@@ -50,26 +67,15 @@ jobs:
node-version: "24.x" node-version: "24.x"
cache: "pnpm" cache: "pnpm"
cache-dependency-path: "frontend/pnpm-lock.yaml" cache-dependency-path: "frontend/pnpm-lock.yaml"
- run: make test-frontend - name: Install Task
test-backend: uses: go-task/setup-task@v1
runs-on: ubuntu-latest - run: task build
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: '1.25'
- run: make test-backend
test:
runs-on: ubuntu-latest
needs: [test-frontend, test-backend]
steps:
- run: echo "done"
# release
release: release:
name: Release
needs: ["lint-frontend", "lint-backend", "test", "build"]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [lint, test]
if: startsWith(github.event.ref, 'refs/tags/v')
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
@@ -89,8 +95,9 @@ jobs:
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Build frontend - name: Install Task
run: make build-frontend uses: go-task/setup-task@v1
- run: task build-frontend
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@@ -103,3 +110,6 @@ jobs:
args: release --clean args: release --clean
env: env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }} GITHUB_TOKEN: ${{ secrets.GH_PAT }}

View File

@@ -1,12 +1,32 @@
name: Build and Deploy Site name: Docs
on: on:
pull_request:
paths:
- 'www'
- '*.md'
push: push:
branches: branches:
- master - master
jobs: jobs:
deploy: build:
name: Build Docs
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Install Task
uses: go-task/setup-task@v1
- name: Build site
run: task docs
build-and-release:
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
name: Build and Release Docs
permissions: permissions:
contents: read contents: read
deployments: write deployments: write
@@ -16,13 +36,12 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Install Task
uses: go-task/setup-task@v1
- name: Build site - name: Build site
run: make site run: task docs
- name: Deploy to Cloudflare Pages - name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3 uses: cloudflare/wrangler-action@v3
with: with:
@@ -30,3 +49,4 @@ jobs:
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy www/public --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }} command: pages deploy www/public --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }}
gitHubToken: ${{ secrets.GITHUB_TOKEN }} gitHubToken: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -13,7 +13,7 @@ permissions:
jobs: jobs:
main: main:
name: Validate PR title name: Validate Title
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: amannn/action-semantic-pull-request@v6 - uses: amannn/action-semantic-pull-request@v6
@@ -43,4 +43,4 @@ jobs:
uses: marocchino/sticky-pull-request-comment@v2 uses: marocchino/sticky-pull-request-comment@v2
with: with:
header: pr-title-lint-error header: pr-title-lint-error
delete: true delete: true

View File

@@ -1,20 +0,0 @@
name: Build Site
on:
pull_request:
paths:
- 'www'
- '*.md'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build site
run: make site

View File

@@ -1,132 +1,9 @@
version: "2" version: "2"
linters: linters:
# inverted configuration with `default: all` and `disable` is not scalable during updates of golangci-lint default: standard
default: none
enable:
- bodyclose
- dogsled
- dupl
- errcheck
- errorlint
- exhaustive
- funlen
- gocheckcompilerdirectives
- gochecknoinits
- gocritic
- gocyclo
- godox
- goprintffuncname
- gosec
- govet
- ineffassign
- lll
- misspell
- mnd
- nakedret
- nolintlint
- prealloc
- revive
- rowserrcheck
- staticcheck
- testifylint
- unconvert
- unparam
- unused
- whitespace
settings:
dupl:
threshold: 100
exhaustive:
default-signifies-exhaustive: false
funlen:
lines: 100
statements: 50
gocritic:
disabled-checks:
- dupImport # https://github.com/go-critic/go-critic/issues/845
- ifElseChain
- octalLiteral
- whyNoLint
- wrapperFunc
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
gocyclo:
min-complexity: 15
govet:
enable:
- nilness
- shadow
lll:
line-length: 140
misspell:
locale: US
mnd:
# don't include the "operation" and "assign"
checks:
- argument
- case
- condition
- return
ignored-numbers:
- "0"
- "1"
- "2"
- "3"
- "0666"
- "0700"
- "0700"
ignored-functions:
- strings.SplitN
- make
nolintlint:
allow-unused: false # report any unused nolint directives
require-explanation: false # require an explanation for nolint directives
require-specific: true # require nolint directives to be specific about which linter is being skipped
staticcheck:
checks:
- "all"
- "-QF*"
exclusions: exclusions:
generated: lax
presets: presets:
- comments
- common-false-positives
- legacy
- std-error-handling - std-error-handling
rules:
- linters:
- gochecknoinits
path: cmd/.*.go
- linters:
- dupl
- funlen
- gochecknoinits
- gocyclo
- lll
- scopelint
path: .*_test.go
- linters:
- misspell
text: "[aA]uther"
- linters:
- mnd
text: strconv.Parse
paths:
- frontend/
formatters:
enable:
- goimports
settings:
goimports:
local-prefixes:
- github.com/filebrowser/filebrowser
exclusions:
generated: lax
paths: paths:
- frontend/ - frontend/

View File

@@ -2,6 +2,28 @@
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [2.46.1](https://github.com/filebrowser/filebrowser/compare/v2.46.0...v2.46.1) (2025-11-15)
### Bug Fixes
* env key replacer and remove unused function ([#5547](https://github.com/filebrowser/filebrowser/issues/5547)) ([13814e1](https://github.com/filebrowser/filebrowser/commit/13814e11197ebd9101940883e3ca85998f86d442))
* remove duplicated 'hide-defaults' flag (is 'hideDefaults') ([#5548](https://github.com/filebrowser/filebrowser/issues/5548)) ([ffc8504](https://github.com/filebrowser/filebrowser/commit/ffc850454e4cb8f10b970511681d6c627340afc7))
## [2.46.0](https://github.com/filebrowser/filebrowser/compare/v2.45.3...v2.46.0) (2025-11-14)
### Features
* add 'hide-dotfiles' as command line parameter ([#3802](https://github.com/filebrowser/filebrowser/issues/3802)) ([0d973d3](https://github.com/filebrowser/filebrowser/commit/0d973d3aad70ceb88950f2cd9c297fc76e7955b1))
* add context menu ([#3343](https://github.com/filebrowser/filebrowser/issues/3343)) ([1ace579](https://github.com/filebrowser/filebrowser/commit/1ace579a553486bb15af2d11f537414156606434))
* add option to hide the login button from public-facing pages ([#3922](https://github.com/filebrowser/filebrowser/issues/3922)) ([ac7b49c](https://github.com/filebrowser/filebrowser/commit/ac7b49c1484b4e27a1149310542ccd1e90659ee2))
* Updates for project File Browser ([#5544](https://github.com/filebrowser/filebrowser/issues/5544)) ([fb5d099](https://github.com/filebrowser/filebrowser/commit/fb5d099f8514516216f407be012d2e3f25de2441))
## [2.45.3](https://github.com/filebrowser/filebrowser/compare/v2.45.2...v2.45.3) (2025-11-13)
This is a test release to ensure the updated workflow works.
## [2.45.2](https://github.com/filebrowser/filebrowser/compare/v2.45.1...v2.45.2) (2025-11-13) ## [2.45.2](https://github.com/filebrowser/filebrowser/compare/v2.45.1...v2.45.2) (2025-11-13)

View File

@@ -15,11 +15,23 @@ We encourage you to use git to manage your fork. To clone the main repository, j
git clone https://github.com/filebrowser/filebrowser git clone https://github.com/filebrowser/filebrowser
``` ```
We use [Taskfile](https://taskfile.dev/) to manage the different processes (building, releasing, etc) automatically.
## Build ## Build
You can fully build the project in order to produce a binary by running:
```bash
task build
```
## Development
For development, there are a few things to have in mind.
### Frontend ### Frontend
We are using [Node.js](https://nodejs.org/en/) on the frontend to manage the build process. The steps to build it are: We use [Node.js](https://nodejs.org/en/) on the frontend to manage the build process. Prepare the frontend environment:
```bash ```bash
# From the root of the repo, go to frontend/ # From the root of the repo, go to frontend/
@@ -27,37 +39,62 @@ cd frontend
# Install the dependencies # Install the dependencies
pnpm install pnpm install
```
# Build the frontend If you just want to develop the backend, you can create a static build of the frontend:
```bash
pnpm run build pnpm run build
``` ```
This will install the dependencies and build the frontend so you can then embed it into the Go app. Although, if you want to play with it, you'll get bored of building it after every change you do. So, you can run the command below to watch for changes: If you want to develop the frontend, start a development server which watches for changes:
```bash ```bash
pnpm run dev pnpm run dev
``` ```
Please note that you need to access File Browser's interface through the development server of the frontend.
### Backend ### Backend
First of all, you need to download the required dependencies. We are using the built-in `go mod` tool for dependency management. To get the modules, run: First prepare the backend environment by downloading all required dependencies:
```bash ```bash
go mod download go mod download
``` ```
The magic of File Browser is that the static assets are bundled into the final binary. For that, we use [Go embed.FS](https://golang.org/pkg/embed/). The files from `frontend/dist` will be embedded during the build process. You can now build or run File Browser as any other Go project:
To build File Browser is just like any other Go program:
```bash ```bash
# Build
go build go build
# Run
go run .
``` ```
To create a development build use the "dev" tag, this way the content inside the frontend folder will not be embedded in the binary but will be reloaded at every change: ## Documentation
We rely on Docker to abstract all the dependencies required for building the documentation.
To build the documentation to `www/public`:
```bash ```bash
go build -tags dev task docs
```
To start a local server on port `8000` to view the built documentation:
```bash
task docs-serve
```
## Release
To make a release, just run:
```bash
task release
``` ```
## Translations ## Translations

View File

@@ -1,88 +0,0 @@
include common.mk
include tools.mk
LDFLAGS += -X "$(MODULE)/version.Version=$(VERSION)" -X "$(MODULE)/version.CommitSHA=$(VERSION_HASH)"
SITE_DOCKER_FLAGS = \
-v $(CURDIR)/www:/docs \
-v $(CURDIR)/LICENSE:/docs/docs/LICENSE \
-v $(CURDIR)/SECURITY.md:/docs/docs/security.md \
-v $(CURDIR)/CHANGELOG.md:/docs/docs/changelog.md \
-v $(CURDIR)/CODE-OF-CONDUCT.md:/docs/docs/code-of-conduct.md \
-v $(CURDIR)/CONTRIBUTING.md:/docs/docs/contributing.md
## Build:
.PHONY: build
build: | build-frontend build-backend ## Build binary
.PHONY: build-frontend
build-frontend: ## Build frontend
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run build
.PHONY: build-backend
build-backend: ## Build backend
$Q $(go) build -ldflags '$(LDFLAGS)' -o .
.PHONY: test
test: | test-frontend test-backend ## Run all tests
.PHONY: test-frontend
test-frontend: ## Run frontend tests
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run typecheck
.PHONY: test-backend
test-backend: ## Run backend tests
$Q $(go) test -v ./...
.PHONY: lint
lint: lint-frontend lint-backend ## Run all linters
.PHONY: lint-frontend
lint-frontend: ## Run frontend linters
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run lint
.PHONY: lint-backend
lint-backend: | $(golangci-lint) ## Run backend linters
$Q $(golangci-lint) run -v
fmt: $(goimports) ## Format source files
$Q $(goimports) -local $(MODULE) -w $$(find . -type f -name '*.go' -not -path "./vendor/*")
clean: clean-tools ## Clean
## Release:
.PHONY: release-dry-run
release-dry-run:
pnpm dlx commit-and-tag-version --dry-run --skip
.PHONY: release
release:
pnpm dlx commit-and-tag-version -s
.PHONY: site
site: ## Build site
@rm -rf www/public
docker build -f www/Dockerfile --progress=plain -t filebrowser.site www
docker run --rm $(SITE_DOCKER_FLAGS) filebrowser.site build -d "public"
.PHONY: site-serve
site-serve: ## Serve site for development
docker build -f www/Dockerfile --progress=plain -t filebrowser.site www
docker run --rm -it -p 8000:8000 $(SITE_DOCKER_FLAGS) filebrowser.site
## Help:
help: ## Show this help
@echo ''
@echo 'Usage:'
@echo ' ${YELLOW}make${RESET} ${GREEN}<target> [options]${RESET}'
@echo ''
@echo 'Options:'
@$(call global_option, "V [0|1]", "enable verbose mode (default:0)")
@echo ''
@echo 'Targets:'
@awk 'BEGIN {FS = ":.*?## "} { \
if (/^[a-zA-Z_-]+:.*?##.*$$/) {printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n", $$1, $$2} \
else if (/^## .*$$/) {printf " ${CYAN}%s${RESET}\n", substr($$1,4)} \
}' $(MAKEFILE_LIST)

View File

@@ -2,7 +2,7 @@
<img src="https://raw.githubusercontent.com/filebrowser/filebrowser/master/branding/banner.png" width="550"/> <img src="https://raw.githubusercontent.com/filebrowser/filebrowser/master/branding/banner.png" width="550"/>
</p> </p>
[![Build](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml/badge.svg)](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml) [![Build](https://github.com/filebrowser/filebrowser/actions/workflows/ci.yaml/badge.svg)](https://github.com/filebrowser/filebrowser/actions/workflows/ci.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/filebrowser/filebrowser/v2)](https://goreportcard.com/report/github.com/filebrowser/filebrowser/v2) [![Go Report Card](https://goreportcard.com/badge/github.com/filebrowser/filebrowser/v2)](https://goreportcard.com/report/github.com/filebrowser/filebrowser/v2)
[![Version](https://img.shields.io/github/release/filebrowser/filebrowser.svg)](https://github.com/filebrowser/filebrowser/releases/latest) [![Version](https://img.shields.io/github/release/filebrowser/filebrowser.svg)](https://github.com/filebrowser/filebrowser/releases/latest)
@@ -14,18 +14,12 @@ Documentation on how to install, configure, and contribute to this project is ho
## Project Status ## Project Status
> [!WARNING] This project is a finished product which fulfills its goal: be a single binary web File Browser which can be run by anyone anywhere. That means that File Browser is currently on **maintenance-only** mode. Therefore, please note the following:
>
> This project is currently on **maintenance-only** mode, and is looking for new maintainers. For more information, please read the [discussion #4906](https://github.com/filebrowser/filebrowser/discussions/4906). Therefore, please note the following:
>
> - It can take a while until someone gets back to you. Please be patient.
> - [Issues][issues] are only being used to track bugs. Any unrelated issues will be converted into a [discussion][discussions].
> - No new features will be implemented until further notice. The priority is on triaging issues and merge bug fixes.
>
> If you're interested in maintaining this project, please reach out via the discussion above.
[issues]: https://github.com/filebrowser/filebrowser/issues - It can take a while until someone gets back to you. Please be patient.
[discussions]: https://github.com/filebrowser/filebrowser/discussions - [Issues](https://github.com/filebrowser/filebrowser/issues) are meant to track bugs. Unrelated issues will be converted into [discussions](https://github.com/filebrowser/filebrowser/discussions).
- No new features will be implemented by maintainers. Pull requests for new features will be reviewed on a case by case basis.
- The priority is triaging issues, addressing security issues and reviewing pull requests meant to solve bugs.
## Contributing ## Contributing

69
Taskfile.yml Normal file
View File

@@ -0,0 +1,69 @@
version: '3'
vars:
SITE_DOCKER_FLAGS: >-
-v ./www:/docs
-v ./LICENSE:/docs/docs/LICENSE
-v ./SECURITY.md:/docs/docs/security.md
-v ./CHANGELOG.md:/docs/docs/changelog.md
-v ./CODE-OF-CONDUCT.md:/docs/docs/code-of-conduct.md
-v ./CONTRIBUTING.md:/docs/docs/contributing.md
tasks:
build-frontend:
desc: Build frontend assets
dir: frontend
cmds:
- pnpm install --frozen-lockfile
- pnpm run build
build-backend:
desc: Build backend binary
cmds:
- go build -ldflags='-s -w -X "github.com/filebrowser/filebrowser/v2/version.Version={{.VERSION}}" -X "github.com/filebrowser/filebrowser/v2/version.CommitSHA={{.GIT_COMMIT}}"' -o filebrowser .
vars:
GIT_COMMIT:
sh: git log -n 1 --format=%h
VERSION:
sh: git describe --tags --abbrev=0 --match=v* | cut -c 2-
build:
desc: Build both frontend and backend
cmds:
- task: build-frontend
- task: build-backend
release-make:
internal: true
prompt: Do you wish to proceed?
cmds:
- pnpm dlx commit-and-tag-version -s
release-dry-run:
internal: true
cmds:
- pnpm dlx commit-and-tag-version --dry-run --skip
release:
desc: Create a new release
cmds:
- task: release-dry-run
- task: release-make
docs-image-make:
internal: true
cmds:
- docker build -f www/Dockerfile --progress=plain -t filebrowser.site www
docs:
desc: Generate documentation
cmds:
- rm -rf www/public
- task: docs-image-make
- docker run --rm {{.SITE_DOCKER_FLAGS}} filebrowser.site build -d "public"
docs-serve:
desc: Serve documentation
cmds:
- task: docs-image-make
- docker run --rm -it -p 8000:8000 {{.SITE_DOCKER_FLAGS}} filebrowser.site

View File

@@ -31,6 +31,7 @@ func addConfigFlags(flags *pflag.FlagSet) {
addServerFlags(flags) addServerFlags(flags)
addUserFlags(flags) addUserFlags(flags)
flags.BoolP("signup", "s", false, "allow users to signup") flags.BoolP("signup", "s", false, "allow users to signup")
flags.Bool("hide-login-button", false, "hide login button from public pages")
flags.Bool("create-user-dir", false, "generate user's home directory automatically") flags.Bool("create-user-dir", false, "generate user's home directory automatically")
flags.Uint("minimum-password-length", settings.DefaultMinimumPasswordLength, "minimum password length for new users") flags.Uint("minimum-password-length", settings.DefaultMinimumPasswordLength, "minimum password length for new users")
flags.String("shell", "", "shell command to which other commands should be appended") flags.String("shell", "", "shell command to which other commands should be appended")
@@ -192,9 +193,10 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup) fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
fmt.Fprintf(w, "Hide Login Button:\t%t\n", set.HideLoginButton)
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir) fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)
fmt.Fprintf(w, "Minimum Password Length:\t%d\n", set.MinimumPasswordLength) fmt.Fprintf(w, "Minimum Password Length:\t%d\n", set.MinimumPasswordLength)
fmt.Fprintf(w, "Auth method:\t%s\n", set.AuthMethod) fmt.Fprintf(w, "Auth Method:\t%s\n", set.AuthMethod)
fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " ")) fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " "))
fmt.Fprintln(w, "\nBranding:") fmt.Fprintln(w, "\nBranding:")
fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name) fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name)
@@ -215,6 +217,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tExec Enabled:\t%t\n", ser.EnableExec) fmt.Fprintf(w, "\tExec Enabled:\t%t\n", ser.EnableExec)
fmt.Fprintln(w, "\nDefaults:") fmt.Fprintln(w, "\nDefaults:")
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope) fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
fmt.Fprintf(w, "\tHideDotfiles:\t%t\n", set.Defaults.HideDotfiles)
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale) fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode) fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick) fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick)

View File

@@ -41,6 +41,11 @@ override the options.`,
return err return err
} }
hideLoginButton, err := getBool(flags, "hide-login-button")
if err != nil {
return err
}
createUserDir, err := getBool(flags, "create-user-dir") createUserDir, err := getBool(flags, "create-user-dir")
if err != nil { if err != nil {
return err return err
@@ -84,6 +89,7 @@ override the options.`,
s := &settings.Settings{ s := &settings.Settings{
Key: key, Key: key,
Signup: signup, Signup: signup,
HideLoginButton: hideLoginButton,
CreateUserDir: createUserDir, CreateUserDir: createUserDir,
MinimumPasswordLength: minLength, MinimumPasswordLength: minLength,
Shell: convertCmdStrToCmdArray(shell), Shell: convertCmdStrToCmdArray(shell),

View File

@@ -50,6 +50,8 @@ you want to change. Other options will remain unchanged.`,
ser.Port, err = getString(flags, flag.Name) ser.Port, err = getString(flags, flag.Name)
case "log": case "log":
ser.Log, err = getString(flags, flag.Name) ser.Log, err = getString(flags, flag.Name)
case "hide-login-button":
set.HideLoginButton, err = getBool(flags, flag.Name)
case "signup": case "signup":
set.Signup, err = getBool(flags, flag.Name) set.Signup, err = getBool(flags, flag.Name)
case "auth.method": case "auth.method":

View File

@@ -21,7 +21,7 @@ func init() {
func printToc(names []string) { func printToc(names []string) {
for i, name := range names { for i, name := range names {
name = strings.TrimSuffix(name, filepath.Ext(name)) name = strings.TrimSuffix(name, filepath.Ext(name))
name = strings.Replace(name, "-", " ", -1) name = strings.ReplaceAll(name, "-", " ")
names[i] = name names[i] = name
} }
@@ -29,7 +29,7 @@ func printToc(names []string) {
toc := "" toc := ""
for _, name := range names { for _, name := range names {
toc += "* [" + name + "](cli/" + strings.Replace(name, " ", "-", -1) + ".md)\n" toc += "* [" + name + "](cli/" + strings.ReplaceAll(name, " ", "-") + ".md)\n"
} }
fmt.Println(toc) fmt.Println(toc)
@@ -84,7 +84,7 @@ func generateDocs(cmd *cobra.Command, dir string) error {
} }
} }
basename := strings.Replace(cmd.CommandPath(), " ", "-", -1) + ".md" basename := strings.ReplaceAll(cmd.CommandPath(), " ", "-") + ".md"
filename := filepath.Join(dir, basename) filename := filepath.Join(dir, basename)
f, err := os.Create(filename) f, err := os.Create(filename)
if err != nil { if err != nil {

View File

@@ -420,6 +420,7 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error {
set := &settings.Settings{ set := &settings.Settings{
Key: generateKey(), Key: generateKey(),
Signup: false, Signup: false,
HideLoginButton: true,
CreateUserDir: false, CreateUserDir: false,
MinimumPasswordLength: settings.DefaultMinimumPasswordLength, MinimumPasswordLength: settings.DefaultMinimumPasswordLength,
UserHomeBasePath: settings.DefaultUsersHomeBasePath, UserHomeBasePath: settings.DefaultUsersHomeBasePath,
@@ -533,8 +534,7 @@ func initConfig() {
v.SetEnvPrefix("FB") v.SetEnvPrefix("FB")
v.AutomaticEnv() v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
if err := v.ReadInConfig(); err != nil { if err := v.ReadInConfig(); err != nil {
var configParseError v.ConfigParseError var configParseError v.ConfigParseError

View File

@@ -135,6 +135,8 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
defaults.Sorting.By, err = getString(flags, flag.Name) defaults.Sorting.By, err = getString(flags, flag.Name)
case "sorting.asc": case "sorting.asc":
defaults.Sorting.Asc, err = getBool(flags, flag.Name) defaults.Sorting.Asc, err = getBool(flags, flag.Name)
case "hideDotfiles":
defaults.HideDotfiles, err = getBool(flags, flag.Name)
} }
if err != nil { if err != nil {
visitErr = err visitErr = err

View File

@@ -23,16 +23,8 @@ import (
const dbPerms = 0640 const dbPerms = 0640
func returnErr(err error) error {
if err != nil {
return err
}
return nil
}
func getString(flags *pflag.FlagSet, flag string) (string, error) { func getString(flags *pflag.FlagSet, flag string) (string, error) {
s, err := flags.GetString(flag) return flags.GetString(flag)
return s, returnErr(err)
} }
func getMode(flags *pflag.FlagSet, flag string) (fs.FileMode, error) { func getMode(flags *pflag.FlagSet, flag string) (fs.FileMode, error) {
@@ -48,13 +40,11 @@ func getMode(flags *pflag.FlagSet, flag string) (fs.FileMode, error) {
} }
func getBool(flags *pflag.FlagSet, flag string) (bool, error) { func getBool(flags *pflag.FlagSet, flag string) (bool, error) {
b, err := flags.GetBool(flag) return flags.GetBool(flag)
return b, returnErr(err)
} }
func getUint(flags *pflag.FlagSet, flag string) (uint, error) { func getUint(flags *pflag.FlagSet, flag string) (uint, error) {
b, err := flags.GetUint(flag) return flags.GetUint(flag)
return b, returnErr(err)
} }
func generateKey() []byte { func generateKey() []byte {

View File

@@ -1,34 +0,0 @@
module.exports = {
rules: {
'body-leading-blank': [1, 'always'],
'body-max-line-length': [2, 'always', 100],
'footer-leading-blank': [1, 'always'],
'footer-max-line-length': [2, 'always', 100],
'header-max-length': [2, 'always', 100],
'scope-case': [2, 'always', 'lower-case'],
'subject-case': [
2,
'never',
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
],
'subject-full-stop': [2, 'never', '.'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
[
'feat',
'fix',
'perf',
'revert',
'refactor',
'build',
'ci',
'test',
'chore',
'docs',
],
],
},
};

View File

@@ -1,28 +0,0 @@
SHELL := /usr/bin/env bash
DATE ?= $(shell date +%FT%T%z)
BASE_PATH := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
cat $(CURDIR)/.version 2> /dev/null || echo v0)
VERSION_HASH = $(shell git rev-parse HEAD)
BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
go = GOGC=off go
MODULE = $(shell env GO111MODULE=on go list -m)
# printing
# $Q (quiet) is used in the targets as a replacer for @.
# This macro helps to print the command for debugging by setting V to 1. Example `make test-unit V=1`
V = 0
Q = $(if $(filter 1,$V),,@)
# $M is a macro to print a colored ▶ character. Example `$(info $(M) running coverage tests…)` will print "▶ running coverage tests…"
M = $(shell printf "\033[34;1m▶\033[0m")
GREEN := $(shell tput -Txterm setaf 2)
YELLOW := $(shell tput -Txterm setaf 3)
WHITE := $(shell tput -Txterm setaf 7)
CYAN := $(shell tput -Txterm setaf 6)
RESET := $(shell tput -Txterm sgr0)
define global_option
printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n" $(1) $(2)
endef

View File

@@ -1,14 +0,0 @@
//go:build dev
package frontend
import (
"io/fs"
"os"
)
var assets fs.FS = os.DirFS("frontend")
func Assets() fs.FS {
return assets
}

View File

@@ -0,0 +1,47 @@
<template>
<div
class="context-menu"
ref="contextMenu"
v-show="show"
:style="{
top: `${props.pos.y}px`,
left: `${left}px`,
}"
>
<slot />
</div>
</template>
<script setup lang="ts">
import { ref, watch, computed, onUnmounted } from "vue";
const emit = defineEmits(["hide"]);
const props = defineProps<{ show: boolean; pos: { x: number; y: number } }>();
const contextMenu = ref<HTMLElement | null>(null);
const left = computed(() => {
return Math.min(
props.pos.x,
window.innerWidth - (contextMenu.value?.clientWidth ?? 0)
);
});
const hideContextMenu = () => {
emit("hide");
};
watch(
() => props.show,
(val) => {
if (val) {
document.addEventListener("click", hideContextMenu);
} else {
document.removeEventListener("click", hideContextMenu);
}
}
);
onUnmounted(() => {
document.removeEventListener("click", hideContextMenu);
});
</script>

View File

@@ -1,3 +1,6 @@
<!-- This component taken directly from vue-simple-progress
since it didnt support Vue 3 but the component itself does
https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/components/Progress.vue -->
<template> <template>
<div> <div>
<div <div
@@ -41,164 +44,182 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { computed } from "vue"; // We're leaving this untouched as you can read in the beginning
const isNumber = function (n) {
const isNumber = (n: number | string): boolean => { return !isNaN(parseFloat(n)) && isFinite(n);
return !isNaN(parseFloat(n as string)) && isFinite(n as number);
}; };
const props = withDefaults( export default {
defineProps<{ name: "progress-bar",
val?: number; props: {
max?: number; val: {
size?: number | string; default: 0,
bgColor?: string; },
barColor?: string; max: {
barTransition?: string; default: 100,
barBorderRadius?: number; },
spacing?: number; size: {
text?: string; // either a number (pixel width/height) or 'tiny', 'small',
textAlign?: string; // 'medium', 'large', 'huge', 'massive' for common sizes
textPosition?: string; default: 3,
fontSize?: number; },
textFgColor?: string; "bg-color": {
}>(), type: String,
{ default: "#eee",
val: 0, },
max: 100, "bar-color": {
size: 3, type: String,
bgColor: "#eee", default: "#2196f3", // match .blue color to Material Design's 'Blue 500' color
barColor: "#2196f3", },
barTransition: "all 0.5s ease", "bar-transition": {
barBorderRadius: 0, type: String,
spacing: 4, default: "all 0.5s ease",
text: "", },
textAlign: "center", "bar-border-radius": {
textPosition: "bottom", type: Number,
fontSize: 13, default: 0,
textFgColor: "#222", },
} spacing: {
); type: Number,
default: 4,
},
text: {
type: String,
default: "",
},
"text-align": {
type: String,
default: "center", // 'left', 'right'
},
"text-position": {
type: String,
default: "bottom", // 'bottom', 'top', 'middle', 'inside'
},
"font-size": {
type: Number,
default: 13,
},
"text-fg-color": {
type: String,
default: "#222",
},
},
computed: {
pct() {
let pct = (this.val / this.max) * 100;
pct = pct.toFixed(2);
return Math.min(pct, this.max);
},
size_px() {
switch (this.size) {
case "tiny":
return 2;
case "small":
return 4;
case "medium":
return 8;
case "large":
return 12;
case "big":
return 16;
case "huge":
return 32;
case "massive":
return 64;
}
const pct = computed(() => { return isNumber(this.size) ? this.size : 32;
const pct = (props.val / props.max) * 100; },
const pctFixed = pct.toFixed(2); text_padding() {
return Math.min(parseFloat(pctFixed), props.max); switch (this.size) {
}); case "tiny":
case "small":
case "medium":
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(this.size_px / 8), 3), 12);
}
const size_px = computed(() => { return isNumber(this.spacing) ? this.spacing : 4;
switch (props.size) { },
case "tiny": text_font_size() {
return 2; switch (this.size) {
case "small": case "tiny":
return 4; case "small":
case "medium": case "medium":
return 8; case "large":
case "large": case "big":
return 12; case "huge":
case "big": case "massive":
return 16; return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32);
case "huge": }
return 32;
case "massive":
return 64;
}
return isNumber(props.size) ? (props.size as number) : 32; return isNumber(this.fontSize) ? this.fontSize : 13;
}); },
progress_style() {
const style = {
background: this.bgColor,
};
const text_padding = computed(() => { if (this.textPosition == "middle" || this.textPosition == "inside") {
switch (props.size) { style["position"] = "relative";
case "tiny": style["min-height"] = this.size_px + "px";
case "small": style["z-index"] = "-2";
case "medium": }
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(size_px.value / 8), 3), 12);
}
return isNumber(props.spacing) ? props.spacing : 4; if (this.barBorderRadius > 0) {
}); style["border-radius"] = this.barBorderRadius + "px";
}
const text_font_size = computed(() => { return style;
switch (props.size) { },
case "tiny": bar_style() {
case "small": const style = {
case "medium": background: this.barColor,
case "large": width: this.pct + "%",
case "big": height: this.size_px + "px",
case "huge": transition: this.barTransition,
case "massive": };
return Math.min(Math.max(Math.ceil(size_px.value * 1.4), 11), 32);
}
return isNumber(props.fontSize) ? props.fontSize : 13; if (this.barBorderRadius > 0) {
}); style["border-radius"] = this.barBorderRadius + "px";
}
const progress_style = computed(() => { if (this.textPosition == "middle" || this.textPosition == "inside") {
const style: Record<string, string> = { style["position"] = "absolute";
background: props.bgColor, style["top"] = "0";
}; style["height"] = "100%";
((style["min-height"] = this.size_px + "px"),
(style["z-index"] = "-1"));
}
if (props.textPosition == "middle" || props.textPosition == "inside") { return style;
style["position"] = "relative"; },
style["min-height"] = size_px.value + "px"; text_style() {
style["z-index"] = "-2"; const style = {
} color: this.textFgColor,
"font-size": this.text_font_size + "px",
"text-align": this.textAlign,
};
if (props.barBorderRadius > 0) { if (
style["border-radius"] = props.barBorderRadius + "px"; this.textPosition == "top" ||
} this.textPosition == "middle" ||
this.textPosition == "inside"
)
style["padding-bottom"] = this.text_padding + "px";
if (
this.textPosition == "bottom" ||
this.textPosition == "middle" ||
this.textPosition == "inside"
)
style["padding-top"] = this.text_padding + "px";
return style; return style;
}); },
},
const bar_style = computed(() => { };
const style: Record<string, string> = {
background: props.barColor,
width: pct.value + "%",
height: size_px.value + "px",
transition: props.barTransition,
};
if (props.barBorderRadius > 0) {
style["border-radius"] = props.barBorderRadius + "px";
}
if (props.textPosition == "middle" || props.textPosition == "inside") {
style["position"] = "absolute";
style["top"] = "0";
style["height"] = "100%";
style["min-height"] = size_px.value + "px";
style["z-index"] = "-1";
}
return style;
});
const text_style = computed(() => {
const style: Record<string, string> = {
color: props.textFgColor,
"font-size": text_font_size.value + "px",
"text-align": props.textAlign,
};
if (
props.textPosition == "top" ||
props.textPosition == "middle" ||
props.textPosition == "inside"
)
style["padding-bottom"] = text_padding.value + "px";
if (
props.textPosition == "bottom" ||
props.textPosition == "middle" ||
props.textPosition == "inside"
)
style["padding-top"] = text_padding.value + "px";
return style;
});
</script> </script>

View File

@@ -2,13 +2,13 @@
<div <div
class="shell" class="shell"
:class="{ ['shell--hidden']: !showShell }" :class="{ ['shell--hidden']: !showShell }"
:style="{ height: `${shellHeight}em`, direction: 'ltr' }" :style="{ height: `${this.shellHeight}em`, direction: 'ltr' }"
> >
<div <div
@pointerdown="startDrag()" @pointerdown="startDrag()"
@pointerup="stopDrag()" @pointerup="stopDrag()"
class="shell__divider" class="shell__divider"
:style="shellDrag ? { background: `${checkTheme()}` } : ''" :style="this.shellDrag ? { background: `${checkTheme()}` } : ''"
></div> ></div>
<div @click="focus" class="shell__content" ref="scrollable"> <div @click="focus" class="shell__content" ref="scrollable">
<div v-for="(c, index) in content" :key="index" class="shell__result"> <div v-for="(c, index) in content" :key="index" class="shell__result">
@@ -39,15 +39,13 @@
<div <div
@pointerup="stopDrag()" @pointerup="stopDrag()"
class="shell__overlay" class="shell__overlay"
v-show="shellDrag" v-show="this.shellDrag"
></div> ></div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { ref, computed, onMounted, onBeforeUnmount } from "vue"; import { mapState, mapActions } from "pinia";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
@@ -55,164 +53,142 @@ import { commands } from "@/api";
import { throttle } from "lodash-es"; import { throttle } from "lodash-es";
import { theme } from "@/utils/constants"; import { theme } from "@/utils/constants";
const route = useRoute(); export default {
name: "shell",
computed: {
...mapState(useLayoutStore, ["showShell"]),
...mapState(useFileStore, ["isFiles"]),
path: function () {
if (this.isFiles) {
return this.$route.path;
}
const fileStore = useFileStore(); return "";
const layoutStore = useLayoutStore();
const { showShell } = storeToRefs(layoutStore);
const { isFiles } = storeToRefs(fileStore);
const { toggleShell } = layoutStore;
const scrollable = ref<HTMLElement | null>(null);
const input = ref<HTMLElement | null>(null);
const content = ref<Array<{ text: string }>>([]);
const history = ref<string[]>([]);
const historyPos = ref(0);
const canInput = ref(true);
const shellDrag = ref(false);
const shellHeight = ref(25);
const fontsize = ref(
parseFloat(getComputedStyle(document.documentElement).fontSize)
);
const path = computed(() => {
if (isFiles.value) {
return route.path;
}
return "";
});
const checkTheme = () => {
if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)";
}
return "rgba(127, 127, 127, 0.4)";
};
const scroll = () => {
if (scrollable.value) {
scrollable.value.scrollTop = scrollable.value.scrollHeight;
}
};
const focus = () => {
input.value?.focus();
};
const handleDrag = throttle((event: PointerEvent) => {
const top = window.innerHeight / fontsize.value - 4;
const userPos = (window.innerHeight - event.clientY) / fontsize.value;
const divider = document.querySelector(".shell__divider") as HTMLElement;
const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value;
if (userPos <= top && userPos >= bottom) {
shellHeight.value = parseFloat(userPos.toFixed(2));
}
}, 32);
const resize = throttle(() => {
const top = window.innerHeight / fontsize.value - 4;
const divider = document.querySelector(".shell__divider") as HTMLElement;
const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value;
if (shellHeight.value > top) {
shellHeight.value = top;
} else if (shellHeight.value < bottom) {
shellHeight.value = bottom;
}
}, 32);
const startDrag = () => {
document.addEventListener("pointermove", handleDrag as any);
shellDrag.value = true;
};
const stopDrag = () => {
document.removeEventListener("pointermove", handleDrag as any);
shellDrag.value = false;
};
const historyUp = () => {
if (historyPos.value > 0 && input.value) {
historyPos.value--;
input.value.innerText = history.value[historyPos.value];
focus();
}
};
const historyDown = () => {
if (
historyPos.value >= 0 &&
historyPos.value < history.value.length - 1 &&
input.value
) {
historyPos.value++;
input.value.innerText = history.value[historyPos.value];
focus();
} else {
historyPos.value = history.value.length;
if (input.value) {
input.value.innerText = "";
}
}
};
const submit = (event: Event) => {
const target = event.target as HTMLElement;
const cmd = target.innerText.trim();
if (cmd === "") {
return;
}
if (cmd === "clear") {
content.value = [];
target.innerHTML = "";
return;
}
if (cmd === "exit") {
target.innerHTML = "";
toggleShell();
return;
}
canInput.value = false;
target.innerHTML = "";
const results = {
text: `${cmd}\n\n`,
};
history.value.push(cmd);
historyPos.value = history.value.length;
content.value.push(results);
commands(
path.value,
cmd,
(event: MessageEvent) => {
results.text += `${event.data}\n`;
scroll();
}, },
() => { },
results.text = results.text data: () => ({
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now content: [],
.trimEnd(); history: [],
canInput.value = true; historyPos: 0,
input.value?.focus(); canInput: true,
scroll(); shellDrag: false,
} shellHeight: 25,
); fontsize: parseFloat(getComputedStyle(document.documentElement).fontSize),
}),
mounted() {
window.addEventListener("resize", this.resize);
},
beforeUnmount() {
window.removeEventListener("resize", this.resize);
},
methods: {
...mapActions(useLayoutStore, ["toggleShell"]),
checkTheme() {
if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)";
}
return "rgba(127, 127, 127, 0.4)";
},
startDrag() {
document.addEventListener("pointermove", this.handleDrag);
this.shellDrag = true;
},
stopDrag() {
document.removeEventListener("pointermove", this.handleDrag);
this.shellDrag = false;
},
handleDrag: throttle(function (event) {
const top = window.innerHeight / this.fontsize - 4;
const userPos = (window.innerHeight - event.clientY) / this.fontsize;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (userPos <= top && userPos >= bottom) {
this.shellHeight = userPos.toFixed(2);
}
}, 32),
resize: throttle(function () {
const top = window.innerHeight / this.fontsize - 4;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (this.shellHeight > top) {
this.shellHeight = top;
} else if (this.shellHeight < bottom) {
this.shellHeight = bottom;
}
}, 32),
scroll: function () {
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
},
focus: function () {
this.$refs.input.focus();
},
historyUp() {
if (this.historyPos > 0) {
this.$refs.input.innerText = this.history[--this.historyPos];
this.focus();
}
},
historyDown() {
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
this.$refs.input.innerText = this.history[++this.historyPos];
this.focus();
} else {
this.historyPos = this.history.length;
this.$refs.input.innerText = "";
}
},
submit: function (event) {
const cmd = event.target.innerText.trim();
if (cmd === "") {
return;
}
if (cmd === "clear") {
this.content = [];
event.target.innerHTML = "";
return;
}
if (cmd === "exit") {
event.target.innerHTML = "";
this.toggleShell();
return;
}
this.canInput = false;
event.target.innerHTML = "";
const results = {
text: `${cmd}\n\n`,
};
this.history.push(cmd);
this.historyPos = this.history.length;
this.content.push(results);
commands(
this.path,
cmd,
(event) => {
results.text += `${event.data}\n`;
this.scroll();
},
() => {
results.text = results.text
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
.trimEnd();
this.canInput = true;
this.$refs.input.focus();
this.scroll();
}
);
},
},
}; };
onMounted(() => {
window.addEventListener("resize", resize as any);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", resize as any);
});
</script> </script>

View File

@@ -4,7 +4,7 @@
<template v-if="isLoggedIn"> <template v-if="isLoggedIn">
<button @click="toAccountSettings" class="action"> <button @click="toAccountSettings" class="action">
<i class="material-icons">person</i> <i class="material-icons">person</i>
<span>{{ user?.username }}</span> <span>{{ user.username }}</span>
</button> </button>
<button <button
class="action" class="action"
@@ -16,7 +16,7 @@
<span>{{ $t("sidebar.myFiles") }}</span> <span>{{ $t("sidebar.myFiles") }}</span>
</button> </button>
<div v-if="user?.perm.create"> <div v-if="user.perm.create">
<button <button
@click="showHover('newDir')" @click="showHover('newDir')"
class="action" class="action"
@@ -38,7 +38,7 @@
</button> </button>
</div> </div>
<div v-if="user?.perm.admin"> <div v-if="user.perm.admin">
<button <button
class="action" class="action"
@click="toGlobalSettings" @click="toGlobalSettings"
@@ -63,6 +63,7 @@
</template> </template>
<template v-else> <template v-else>
<router-link <router-link
v-if="!hideLoginButton"
class="action" class="action"
to="/login" to="/login"
:aria-label="$t('sidebar.login')" :aria-label="$t('sidebar.login')"
@@ -113,10 +114,9 @@
</nav> </nav>
</template> </template>
<script setup lang="ts"> <script>
import { reactive, ref, computed, watch, onUnmounted } from "vue"; import { reactive } from "vue";
import { storeToRefs } from "pinia"; import { mapActions, mapState } from "pinia";
import { useRoute, useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
@@ -125,6 +125,7 @@ import * as auth from "@/utils/auth";
import { import {
version, version,
signup, signup,
hideLoginButton,
disableExternal, disableExternal,
disableUsedPercentage, disableUsedPercentage,
noAuth, noAuth,
@@ -136,85 +137,85 @@ import prettyBytes from "pretty-bytes";
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 }; const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
const route = useRoute(); export default {
const router = useRouter(); name: "sidebar",
setup() {
const authStore = useAuthStore(); const usage = reactive(USAGE_DEFAULT);
const fileStore = useFileStore(); return { usage, usageAbortController: new AbortController() };
const layoutStore = useLayoutStore();
const { user, isLoggedIn } = storeToRefs(authStore);
const { isFiles } = storeToRefs(fileStore);
const { currentPromptName } = storeToRefs(layoutStore);
const { closeHovers, showHover } = layoutStore;
const usage = reactive(USAGE_DEFAULT);
const usageAbortController = ref(new AbortController());
const active = computed(() => {
return currentPromptName.value === "sidebar";
});
const canLogout = !noAuth && loginPage;
const abortOngoingFetchUsage = () => {
usageAbortController.value.abort();
};
const fetchUsage = async () => {
const path = route.path.endsWith("/") ? route.path : route.path + "/";
let usageStats = USAGE_DEFAULT;
if (disableUsedPercentage) {
return Object.assign(usage, usageStats);
}
try {
abortOngoingFetchUsage();
usageAbortController.value = new AbortController();
const usageData = await api.usage(path, usageAbortController.value.signal);
usageStats = {
used: prettyBytes(usageData.used, { binary: true }),
total: prettyBytes(usageData.total, { binary: true }),
usedPercentage: Math.round((usageData.used / usageData.total) * 100),
};
} finally {
return Object.assign(usage, usageStats);
}
};
const toRoot = () => {
router.push({ path: "/files" });
closeHovers();
};
const toAccountSettings = () => {
router.push({ path: "/settings/profile" });
closeHovers();
};
const toGlobalSettings = () => {
router.push({ path: "/settings/global" });
closeHovers();
};
const help = () => {
showHover("help");
};
const logout = () => {
auth.logout();
};
watch(
() => route.path,
(newPath) => {
if (newPath.includes("/files")) {
fetchUsage();
}
}, },
{ immediate: true } components: {
); ProgressBar,
},
onUnmounted(() => { inject: ["$showError"],
abortOngoingFetchUsage(); computed: {
}); ...mapState(useAuthStore, ["user", "isLoggedIn"]),
...mapState(useFileStore, ["isFiles", "reload"]),
...mapState(useLayoutStore, ["currentPromptName"]),
active() {
return this.currentPromptName === "sidebar";
},
signup: () => signup,
hideLoginButton: () => hideLoginButton,
version: () => version,
disableExternal: () => disableExternal,
disableUsedPercentage: () => disableUsedPercentage,
canLogout: () => !noAuth && loginPage,
},
methods: {
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
abortOngoingFetchUsage() {
this.usageAbortController.abort();
},
async fetchUsage() {
const path = this.$route.path.endsWith("/")
? this.$route.path
: this.$route.path + "/";
let usageStats = USAGE_DEFAULT;
if (this.disableUsedPercentage) {
return Object.assign(this.usage, usageStats);
}
try {
this.abortOngoingFetchUsage();
this.usageAbortController = new AbortController();
const usage = await api.usage(path, this.usageAbortController.signal);
usageStats = {
used: prettyBytes(usage.used, { binary: true }),
total: prettyBytes(usage.total, { binary: true }),
usedPercentage: Math.round((usage.used / usage.total) * 100),
};
} finally {
return Object.assign(this.usage, usageStats);
}
},
toRoot() {
this.$router.push({ path: "/files" });
this.closeHovers();
},
toAccountSettings() {
this.$router.push({ path: "/settings/profile" });
this.closeHovers();
},
toGlobalSettings() {
this.$router.push({ path: "/settings/global" });
this.closeHovers();
},
help() {
this.showHover("help");
},
logout: auth.logout,
},
watch: {
$route: {
handler(to) {
if (to.path.includes("/files")) {
this.fetchUsage();
}
},
immediate: true,
},
},
unmounted() {
this.abortOngoingFetchUsage();
},
};
</script> </script>

View File

@@ -20,6 +20,7 @@
:aria-label="name" :aria-label="name"
:aria-selected="isSelected" :aria-selected="isSelected"
:data-ext="getExtension(name).toLowerCase()" :data-ext="getExtension(name).toLowerCase()"
@contextmenu="contextMenu"
> >
<div> <div>
<img <img
@@ -239,6 +240,17 @@ const itemClick = (event: Event | KeyboardEvent) => {
else click(event); else click(event);
}; };
const contextMenu = (event: MouseEvent) => {
event.preventDefault();
if (
fileStore.selected.length === 0 ||
event.ctrlKey ||
fileStore.selected.indexOf(props.index) === -1
) {
click(event);
}
};
const click = (event: Event | KeyboardEvent) => { const click = (event: Event | KeyboardEvent) => {
if (!singleClick.value && fileStore.selectedCount !== 0) if (!singleClick.value && fileStore.selectedCount !== 0)
event.preventDefault(); event.preventDefault();

View File

@@ -8,7 +8,7 @@
<p>{{ $t("prompts.copyMessage") }}</p> <p>{{ $t("prompts.copyMessage") }}</p>
<file-list <file-list
ref="fileList" ref="fileList"
@update:selected="(val: string) => (dest = val)" @update:selected="(val) => (dest = val)"
tabindex="1" tabindex="1"
/> />
</div> </div>
@@ -17,10 +17,10 @@
class="card-action" class="card-action"
style="display: flex; align-items: center; justify-content: space-between" style="display: flex; align-items: center; justify-content: space-between"
> >
<template v-if="user?.perm.create"> <template v-if="user.perm.create">
<button <button
class="button button--flat" class="button button--flat"
@click="fileList?.createDir()" @click="$refs.fileList.createDir()"
:aria-label="$t('sidebar.newFolder')" :aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')"
style="justify-self: left" style="justify-self: left"
@@ -53,10 +53,8 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { ref, inject } from "vue"; import { mapActions, mapState, mapWritableState } from "pinia";
import { storeToRefs } from "pinia";
import { useRoute, useRouter } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
@@ -66,84 +64,90 @@ import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
import { removePrefix } from "@/api/utils"; import { removePrefix } from "@/api/utils";
const route = useRoute(); export default {
const router = useRouter(); name: "copy",
const $showError = inject<(error: unknown) => void>("$showError"); components: { FileList },
data: function () {
return {
current: window.location.pathname,
dest: null,
};
},
inject: ["$showError"],
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
copy: async function (event) {
event.preventDefault();
const items = [];
const fileStore = useFileStore(); // Create a new promise for each file.
const layoutStore = useLayoutStore(); for (const item of this.selected) {
const authStore = useAuthStore(); items.push({
from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name,
});
}
const { req, selected } = storeToRefs(fileStore); const action = async (overwrite, rename) => {
const { user } = storeToRefs(authStore); buttons.loading("copy");
const { showHover, closeHovers } = layoutStore;
const fileList = ref<InstanceType<typeof FileList> | null>(null); await api
const dest = ref<string | null>(null); .copy(items, overwrite, rename)
.then(() => {
buttons.success("copy");
this.preselect = removePrefix(items[0].to);
const copy = async (event: Event) => { if (this.$route.path === this.dest) {
event.preventDefault(); this.reload = true;
const items: Array<{ from: string; to: string; name: string }> = [];
// Create a new promise for each file. return;
for (const item of selected.value) { }
items.push({
from: req.value!.items[item].url,
to: dest.value! + encodeURIComponent(req.value!.items[item].name),
name: req.value!.items[item].name,
});
}
const action = async (overwrite: boolean, rename: boolean) => { this.$router.push({ path: this.dest });
buttons.loading("copy"); })
.catch((e) => {
buttons.done("copy");
this.$showError(e);
});
};
await api if (this.$route.path === this.dest) {
.copy(items, overwrite, rename) this.closeHovers();
.then(() => { action(false, true);
buttons.success("copy");
fileStore.preselect = removePrefix(items[0].to);
if (route.path === dest.value) { return;
fileStore.reload = true; }
return;
}
router.push({ path: dest.value! }); const dstItems = (await api.fetch(this.dest)).items;
}) const conflict = upload.checkConflict(items, dstItems);
.catch((e) => {
buttons.done("copy");
$showError?.(e);
});
};
if (route.path === dest.value) { let overwrite = false;
closeHovers(); let rename = false;
action(false, true);
return;
}
const dstItems = (await api.fetch(dest.value!)).items; if (conflict) {
const conflict = upload.checkConflict(items as any, dstItems); this.showHover({
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == "overwrite";
rename = option == "rename";
let overwrite = false; event.preventDefault();
let rename = false; this.closeHovers();
action(overwrite, rename);
},
});
if (conflict) { return;
showHover({ }
prompt: "replace-rename",
confirm: (event: Event, option: string) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault(); action(overwrite, rename);
closeHovers(); },
action(overwrite, rename); },
},
});
return;
}
action(overwrite, rename);
}; };
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-content"> <div class="card-content">
<p v-if="!isListing || selectedCount === 1"> <p v-if="!this.isListing || selectedCount === 1">
{{ $t("prompts.deleteMessageSingle") }} {{ $t("prompts.deleteMessageSingle") }}
</p> </p>
<p v-else> <p v-else>
@@ -32,62 +32,67 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { inject } from "vue"; import { mapActions, mapState, mapWritableState } from "pinia";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
const route = useRoute(); export default {
const $showError = inject<(error: unknown) => void>("$showError"); name: "delete",
inject: ["$showError"],
computed: {
...mapState(useFileStore, [
"isListing",
"selectedCount",
"req",
"selected",
]),
...mapState(useLayoutStore, ["currentPrompt"]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
submit: async function () {
buttons.loading("delete");
const fileStore = useFileStore(); try {
const layoutStore = useLayoutStore(); if (!this.isListing) {
await api.remove(this.$route.path);
buttons.success("delete");
const { isListing, selectedCount, req, selected } = storeToRefs(fileStore); this.currentPrompt?.confirm();
const { currentPrompt } = storeToRefs(layoutStore); this.closeHovers();
const { closeHovers } = layoutStore; return;
}
const submit = async () => { this.closeHovers();
buttons.loading("delete");
try { if (this.selectedCount === 0) {
if (!isListing.value) { return;
await api.remove(route.path); }
buttons.success("delete");
currentPrompt.value?.confirm(); const promises = [];
closeHovers(); for (const index of this.selected) {
return; promises.push(api.remove(this.req.items[index].url));
} }
closeHovers(); await Promise.all(promises);
buttons.success("delete");
if (selectedCount.value === 0) { const nearbyItem =
return; this.req.items[Math.max(0, Math.min(this.selected) - 1)];
}
const promises = []; this.preselect = nearbyItem?.path;
for (const index of selected.value) {
promises.push(api.remove(req.value!.items[index].url));
}
await Promise.all(promises); this.reload = true;
buttons.success("delete"); } catch (e) {
buttons.done("delete");
const nearbyItem = this.$showError(e);
req.value!.items[Math.max(0, Math.min(...selected.value) - 1)]; if (this.isListing) this.reload = true;
}
fileStore.preselect = nearbyItem?.path; },
},
fileStore.reload = true;
} catch (e) {
buttons.done("delete");
$showError?.(e);
if (isListing.value) fileStore.reload = true;
}
}; };
</script> </script>

View File

@@ -33,4 +33,8 @@ import { useI18n } from "vue-i18n";
const layoutStore = useLayoutStore(); const layoutStore = useLayoutStore();
const { t } = useI18n(); const { t } = useI18n();
// const emit = defineEmits<{
// (e: "confirm"): void;
// }>();
</script> </script>

View File

@@ -17,7 +17,7 @@
</button> </button>
<button <button
class="button button--flat button--blue" class="button button--flat button--blue"
@click="currentPrompt?.saveAction" @click="currentPrompt.saveAction"
:aria-label="$t('buttons.saveChanges')" :aria-label="$t('buttons.saveChanges')"
:title="$t('buttons.saveChanges')" :title="$t('buttons.saveChanges')"
tabindex="1" tabindex="1"
@@ -26,7 +26,7 @@
</button> </button>
<button <button
id="focus-prompt" id="focus-prompt"
@click="currentPrompt?.confirm" @click="currentPrompt.confirm"
class="button button--flat button--red" class="button button--flat button--red"
:aria-label="$t('buttons.discardChanges')" :aria-label="$t('buttons.discardChanges')"
:title="$t('buttons.discardChanges')" :title="$t('buttons.discardChanges')"
@@ -38,11 +38,17 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { mapActions, mapState } from "pinia";
const layoutStore = useLayoutStore(); export default {
const { currentPrompt } = storeToRefs(layoutStore); name: "discardEditorChanges",
const { closeHovers } = layoutStore; computed: {
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@@ -24,10 +24,8 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { ref, computed, inject, onMounted, onUnmounted } from "vue"; import { mapState, mapActions } from "pinia";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
@@ -36,162 +34,147 @@ import url from "@/utils/url";
import { files } from "@/api"; import { files } from "@/api";
import { StatusError } from "@/api/utils.js"; import { StatusError } from "@/api/utils.js";
const props = defineProps<{ export default {
exclude?: string[]; name: "file-list",
}>(); props: {
exclude: {
type: Array,
default: () => [],
},
},
data: function () {
return {
items: [],
touches: {
id: "",
count: 0,
},
selected: null,
current: window.location.pathname,
nextAbortController: new AbortController(),
};
},
inject: ["$showError"],
computed: {
...mapState(useAuthStore, ["user"]),
...mapState(useFileStore, ["req"]),
nav() {
return decodeURIComponent(this.current);
},
},
mounted() {
this.fillOptions(this.req);
},
unmounted() {
this.abortOngoingNext();
},
methods: {
...mapActions(useLayoutStore, ["showHover"]),
abortOngoingNext() {
this.nextAbortController.abort();
},
fillOptions(req) {
// Sets the current path and resets
// the current items.
this.current = req.url;
this.items = [];
const emit = defineEmits<{ this.$emit("update:selected", this.current);
"update:selected": [value: string];
}>();
const route = useRoute(); // If the path isn't the root path,
const $showError = inject<(error: unknown) => void>("$showError"); // show a button to navigate to the previous
// directory.
if (req.url !== "/files/") {
this.items.push({
name: "..",
url: url.removeLastDir(req.url) + "/",
});
}
const authStore = useAuthStore(); // If this folder is empty, finish here.
const fileStore = useFileStore(); if (req.items === null) return;
const layoutStore = useLayoutStore();
const { user } = storeToRefs(authStore); // Otherwise we add every directory to the
const { req } = storeToRefs(fileStore); // move options.
const { showHover } = layoutStore; for (const item of req.items) {
if (!item.isDir) continue;
if (this.exclude?.includes(item.url)) continue;
const items = ref<Array<{ name: string; url: string }>>([]); this.items.push({
const touches = ref({ name: item.name,
id: "", url: item.url,
count: 0, });
}); }
const selected = ref<string | null>(null); },
const current = ref(window.location.pathname); next: function (event) {
const nextAbortController = ref(new AbortController()); // Retrieves the URL of the directory the user
// just clicked in and fill the options with its
// content.
const uri = event.currentTarget.dataset.url;
this.abortOngoingNext();
this.nextAbortController = new AbortController();
files
.fetch(uri, this.nextAbortController.signal)
.then(this.fillOptions)
.catch((e) => {
if (e instanceof StatusError && e.is_canceled) {
return;
}
this.$showError(e);
});
},
touchstart(event) {
const url = event.currentTarget.dataset.url;
const nav = computed(() => { // In 300 milliseconds, we shall reset the count.
return decodeURIComponent(current.value); setTimeout(() => {
}); this.touches.count = 0;
}, 300);
const abortOngoingNext = () => { // If the element the user is touching
nextAbortController.value.abort(); // is different from the last one he touched,
}; // reset the count.
if (this.touches.id !== url) {
const fillOptions = (reqData: any) => { this.touches.id = url;
// Sets the current path and resets this.touches.count = 1;
// the current items.
current.value = reqData.url;
items.value = [];
emit("update:selected", current.value);
// If the path isn't the root path,
// show a button to navigate to the previous
// directory.
if (reqData.url !== "/files/") {
items.value.push({
name: "..",
url: url.removeLastDir(reqData.url) + "/",
});
}
// If this folder is empty, finish here.
if (reqData.items === null) return;
// Otherwise we add every directory to the
// move options.
for (const item of reqData.items) {
if (!item.isDir) continue;
if (props.exclude?.includes(item.url)) continue;
items.value.push({
name: item.name,
url: item.url,
});
}
};
const next = (event: Event) => {
// Retrieves the URL of the directory the user
// just clicked in and fill the options with its
// content.
const uri = (event.currentTarget as HTMLElement).dataset.url!;
abortOngoingNext();
nextAbortController.value = new AbortController();
files
.fetch(uri, nextAbortController.value.signal)
.then(fillOptions)
.catch((e) => {
if (e instanceof StatusError && e.is_canceled) {
return; return;
} }
$showError?.(e);
});
};
const touchstart = (event: Event) => { this.touches.count++;
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
// In 300 milliseconds, we shall reset the count. // If there is more than one touch already,
setTimeout(() => { // open the next screen.
touches.value.count = 0; if (this.touches.count > 1) {
}, 300); this.next(event);
}
// If the element the user is touching
// is different from the last one he touched,
// reset the count.
if (touches.value.id !== urlValue) {
touches.value.id = urlValue;
touches.value.count = 1;
return;
}
touches.value.count++;
// If there is more than one touch already,
// open the next screen.
if (touches.value.count > 1) {
next(event);
}
};
const itemClick = (event: Event) => {
if (user.value?.singleClick) next(event);
else select(event);
};
const select = (event: Event) => {
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
// If the element is already selected, unselect it.
if (selected.value === urlValue) {
selected.value = null;
emit("update:selected", current.value);
return;
}
// Otherwise select the element.
selected.value = urlValue;
emit("update:selected", selected.value);
};
const createDir = async () => {
showHover({
prompt: "newDir",
action: undefined,
confirm: undefined,
props: {
redirect: false,
base: current.value === route.path ? null : current.value,
}, },
}); itemClick: function (event) {
if (this.user.singleClick) this.next(event);
else this.select(event);
},
select: function (event) {
// If the element is already selected, unselect it.
if (this.selected === event.currentTarget.dataset.url) {
this.selected = null;
this.$emit("update:selected", this.current);
return;
}
// Otherwise select the element.
this.selected = event.currentTarget.dataset.url;
this.$emit("update:selected", this.selected);
},
createDir: async function () {
this.showHover({
prompt: "newDir",
action: null,
confirm: null,
props: {
redirect: false,
base: this.current === this.$route.path ? null : this.current,
},
});
},
},
}; };
onMounted(() => {
if (req.value) {
fillOptions(req.value);
}
});
onUnmounted(() => {
abortOngoingNext();
});
defineExpose({
createDir,
});
</script> </script>

View File

@@ -34,8 +34,14 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { mapActions } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
const { closeHovers } = useLayoutStore(); export default {
name: "help",
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@@ -29,10 +29,10 @@
<template v-if="dir && selected.length === 0"> <template v-if="dir && selected.length === 0">
<p> <p>
<strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req?.numFiles }} <strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req.numFiles }}
</p> </p>
<p> <p>
<strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req?.numDirs }} <strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req.numDirs }}
</p> </p>
</template> </template>
@@ -99,100 +99,98 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { computed, inject } from "vue"; import { mapActions, mapState } from "pinia";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { filesize } from "@/utils"; import { filesize } from "@/utils";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { files as api } from "@/api"; import { files as api } from "@/api";
const route = useRoute(); export default {
const $showError = inject<(error: unknown) => void>("$showError"); name: "info",
inject: ["$showError"],
computed: {
...mapState(useFileStore, [
"req",
"selected",
"selectedCount",
"isListing",
]),
humanSize: function () {
if (this.selectedCount === 0 || !this.isListing) {
return filesize(this.req.size);
}
const fileStore = useFileStore(); let sum = 0;
const layoutStore = useLayoutStore();
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore); for (const selected of this.selected) {
const { closeHovers } = layoutStore; sum += this.req.items[selected].size;
}
const humanSize = computed(() => { return filesize(sum);
if (selectedCount.value === 0 || !isListing.value) { },
return filesize(req.value?.size ?? 0); humanTime: function () {
} if (this.selectedCount === 0) {
return dayjs(this.req.modified).fromNow();
}
let sum = 0; return dayjs(this.req.items[this.selected[0]].modified).fromNow();
},
modTime: function () {
if (this.selectedCount === 0) {
return new Date(Date.parse(this.req.modified)).toLocaleString();
}
for (const selectedIdx of selected.value) { return new Date(
sum += req.value?.items[selectedIdx]?.size ?? 0; Date.parse(this.req.items[this.selected[0]].modified)
} ).toLocaleString();
},
name: function () {
return this.selectedCount === 0
? this.req.name
: this.req.items[this.selected[0]].name;
},
dir: function () {
return (
this.selectedCount > 1 ||
(this.selectedCount === 0
? this.req.isDir
: this.req.items[this.selected[0]].isDir)
);
},
resolution: function () {
if (this.selectedCount === 1) {
const selectedItem = this.req.items[this.selected[0]];
if (selectedItem && selectedItem.type === "image") {
return selectedItem.resolution;
}
} else if (this.req && this.req.type === "image") {
return this.req.resolution;
}
return null;
},
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
checksum: async function (event, algo) {
event.preventDefault();
return filesize(sum); let link;
});
const humanTime = computed(() => { if (this.selectedCount) {
if (selectedCount.value === 0) { link = this.req.items[this.selected[0]].url;
return dayjs(req.value?.modified).fromNow(); } else {
} link = this.$route.path;
}
return dayjs(req.value?.items[selected.value[0]]?.modified).fromNow(); try {
}); const hash = await api.checksum(link, algo);
event.target.textContent = hash;
const modTime = computed(() => { } catch (e) {
if (selectedCount.value === 0) { this.$showError(e);
return new Date(Date.parse(req.value?.modified ?? "")).toLocaleString(); }
} },
},
return new Date(
Date.parse(req.value?.items[selected.value[0]]?.modified ?? "")
).toLocaleString();
});
const name = computed(() => {
return selectedCount.value === 0
? (req.value?.name ?? "")
: (req.value?.items[selected.value[0]]?.name ?? "");
});
const dir = computed(() => {
return (
selectedCount.value > 1 ||
(selectedCount.value === 0
? (req.value?.isDir ?? false)
: (req.value?.items[selected.value[0]]?.isDir ?? false))
);
});
const resolution = computed(() => {
if (selectedCount.value === 1) {
const selectedItem = req.value?.items[selected.value[0]];
if (selectedItem && selectedItem.type === "image") {
return (selectedItem as any).resolution;
}
} else if (req.value && req.value.type === "image") {
return (req.value as any).resolution;
}
return null;
});
const checksum = async (event: Event, algo: string) => {
event.preventDefault();
let link;
if (selectedCount.value) {
link = req.value?.items[selected.value[0]]?.url ?? "";
} else {
link = route.path;
}
try {
const hash = await api.checksum(link, algo as any);
(event.target as HTMLElement).textContent = hash;
} catch (e) {
$showError?.(e);
}
}; };
</script> </script>

View File

@@ -5,9 +5,10 @@
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t("prompts.moveMessage") }}</p>
<file-list <file-list
ref="fileList" ref="fileList"
@update:selected="(val: string) => (dest = val)" @update:selected="(val) => (dest = val)"
:exclude="excludedFolders" :exclude="excludedFolders"
tabindex="1" tabindex="1"
/> />
@@ -17,10 +18,10 @@
class="card-action" class="card-action"
style="display: flex; align-items: center; justify-content: space-between" style="display: flex; align-items: center; justify-content: space-between"
> >
<template v-if="user?.perm.create"> <template v-if="user.perm.create">
<button <button
class="button button--flat" class="button button--flat"
@click="fileList?.createDir()" @click="$refs.fileList.createDir()"
:aria-label="$t('sidebar.newFolder')" :aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')"
style="justify-self: left" style="justify-self: left"
@@ -54,10 +55,8 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { ref, computed, inject } from "vue"; import { mapActions, mapState, mapWritableState } from "pinia";
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
@@ -67,76 +66,80 @@ import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
import { removePrefix } from "@/api/utils"; import { removePrefix } from "@/api/utils";
const router = useRouter(); export default {
const $showError = inject<(error: unknown) => void>("$showError"); name: "move",
components: { FileList },
data: function () {
return {
current: window.location.pathname,
dest: null,
};
},
inject: ["$showError"],
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["preselect"]),
excludedFolders() {
return this.selected
.filter((idx) => this.req.items[idx].isDir)
.map((idx) => this.req.items[idx].url);
},
},
methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
move: async function (event) {
event.preventDefault();
const items = [];
const fileStore = useFileStore(); for (const item of this.selected) {
const layoutStore = useLayoutStore(); items.push({
const authStore = useAuthStore(); from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name,
});
}
const { req, selected } = storeToRefs(fileStore); const action = async (overwrite, rename) => {
const { user } = storeToRefs(authStore); buttons.loading("move");
const { showHover, closeHovers } = layoutStore;
const fileList = ref<InstanceType<typeof FileList> | null>(null); await api
const dest = ref<string | null>(null); .move(items, overwrite, rename)
.then(() => {
buttons.success("move");
this.preselect = removePrefix(items[0].to);
this.$router.push({ path: this.dest });
})
.catch((e) => {
buttons.done("move");
this.$showError(e);
});
};
const excludedFolders = computed(() => { const dstItems = (await api.fetch(this.dest)).items;
return selected.value const conflict = upload.checkConflict(items, dstItems);
.filter((idx) => req.value!.items[idx].isDir)
.map((idx) => req.value!.items[idx].url);
});
const move = async (event: Event) => { let overwrite = false;
event.preventDefault(); let rename = false;
const items: Array<{ from: string; to: string; name: string }> = [];
for (const item of selected.value) { if (conflict) {
items.push({ this.showHover({
from: req.value!.items[item].url, prompt: "replace-rename",
to: dest.value! + encodeURIComponent(req.value!.items[item].name), confirm: (event, option) => {
name: req.value!.items[item].name, overwrite = option == "overwrite";
}); rename = option == "rename";
}
const action = async (overwrite: boolean, rename: boolean) => { event.preventDefault();
buttons.loading("move"); this.closeHovers();
action(overwrite, rename);
},
});
await api return;
.move(items, overwrite, rename) }
.then(() => {
buttons.success("move");
fileStore.preselect = removePrefix(items[0].to);
router.push({ path: dest.value! });
})
.catch((e) => {
buttons.done("move");
$showError?.(e);
});
};
const dstItems = (await api.fetch(dest.value!)).items; action(overwrite, rename);
const conflict = upload.checkConflict(items as any, dstItems); },
},
let overwrite = false;
let rename = false;
if (conflict) {
showHover({
prompt: "replace-rename",
confirm: (event: Event, option: string) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault();
closeHovers();
action(overwrite, rename);
},
});
return;
}
action(overwrite, rename);
}; };
</script> </script>

View File

@@ -40,74 +40,80 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { ref, onMounted, inject } from "vue"; import { mapActions, mapState, mapWritableState } from "pinia";
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url"; import url from "@/utils/url";
import { files as api } from "@/api"; import { files as api } from "@/api";
import { removePrefix } from "@/api/utils"; import { removePrefix } from "@/api/utils";
const router = useRouter(); export default {
const $showError = inject<(error: unknown) => void>("$showError"); name: "rename",
data: function () {
return {
name: "",
};
},
created() {
this.name = this.oldName();
},
inject: ["$showError"],
computed: {
...mapState(useFileStore, [
"req",
"selected",
"selectedCount",
"isListing",
]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
cancel: function () {
this.closeHovers();
},
oldName: function () {
if (!this.isListing) {
return this.req.name;
}
const fileStore = useFileStore(); if (this.selectedCount === 0 || this.selectedCount > 1) {
const layoutStore = useLayoutStore(); // This shouldn't happen.
return;
}
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore); return this.req.items[this.selected[0]].name;
const { closeHovers } = layoutStore; },
submit: async function () {
let oldLink = "";
let newLink = "";
const name = ref(""); if (!this.isListing) {
oldLink = this.req.url;
} else {
oldLink = this.req.items[this.selected[0]].url;
}
const oldName = (): string => { newLink =
if (!isListing.value) { url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
return req.value?.name ?? "";
}
if (selectedCount.value === 0 || selectedCount.value > 1) { try {
// This shouldn't happen. await api.move([{ from: oldLink, to: newLink }]);
return ""; if (!this.isListing) {
} this.$router.push({ path: newLink });
return;
}
return req.value?.items[selected.value[0]].name ?? ""; this.preselect = removePrefix(newLink);
};
onMounted(() => { this.reload = true;
name.value = oldName(); } catch (e) {
}); this.$showError(e);
}
const submit = async () => { this.closeHovers();
let oldLink = ""; },
let newLink = ""; },
if (!req.value) {
return;
}
if (!isListing.value) {
oldLink = req.value.url;
} else {
oldLink = req.value.items[selected.value[0]].url;
}
newLink = url.removeLastDir(oldLink) + "/" + encodeURIComponent(name.value);
try {
await api.move([{ from: oldLink, to: newLink }]);
if (!isListing.value) {
router.push({ path: newLink });
return;
}
fileStore.preselect = removePrefix(newLink);
fileStore.reload = true;
} catch (e) {
$showError?.(e);
}
closeHovers();
}; };
</script> </script>

View File

@@ -20,7 +20,7 @@
</button> </button>
<button <button
class="button button--flat button--blue" class="button button--flat button--blue"
@click="currentPrompt?.action" @click="currentPrompt.action"
:aria-label="$t('buttons.continue')" :aria-label="$t('buttons.continue')"
:title="$t('buttons.continue')" :title="$t('buttons.continue')"
tabindex="2" tabindex="2"
@@ -30,7 +30,7 @@
<button <button
id="focus-prompt" id="focus-prompt"
class="button button--flat button--red" class="button button--flat button--red"
@click="currentPrompt?.confirm" @click="currentPrompt.confirm"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')" :title="$t('buttons.replace')"
tabindex="1" tabindex="1"
@@ -41,11 +41,17 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { storeToRefs } from "pinia"; import { mapActions, mapState } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
const layoutStore = useLayoutStore(); export default {
const { currentPrompt } = storeToRefs(layoutStore); name: "replace",
const { closeHovers } = layoutStore; computed: {
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@@ -20,7 +20,7 @@
</button> </button>
<button <button
class="button button--flat button--blue" class="button button--flat button--blue"
@click="(event) => currentPrompt?.confirm(event, 'rename')" @click="(event) => currentPrompt.confirm(event, 'rename')"
:aria-label="$t('buttons.rename')" :aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')" :title="$t('buttons.rename')"
tabindex="2" tabindex="2"
@@ -30,7 +30,7 @@
<button <button
id="focus-prompt" id="focus-prompt"
class="button button--flat button--red" class="button button--flat button--red"
@click="(event) => currentPrompt?.confirm(event, 'overwrite')" @click="(event) => currentPrompt.confirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')" :title="$t('buttons.replace')"
tabindex="1" tabindex="1"
@@ -41,11 +41,17 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { storeToRefs } from "pinia"; import { mapActions, mapState } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
const layoutStore = useLayoutStore(); export default {
const { currentPrompt } = storeToRefs(layoutStore); name: "replace-rename",
const { closeHovers } = layoutStore; computed: {
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@@ -129,146 +129,138 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { ref, computed, inject, onBeforeMount } from "vue"; import { mapActions, mapState } from "pinia";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { share as api } from "@/api"; import { share as api } from "@/api";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { copy } from "@/utils/clipboard"; import { copy } from "@/utils/clipboard";
const route = useRoute(); export default {
const { t } = useI18n(); name: "share",
const $showError = inject<(error: unknown) => void>("$showError"); data: function () {
const $showSuccess = inject<(message: string) => void>("$showSuccess"); return {
time: 0,
unit: "hours",
links: [],
clip: null,
password: "",
listing: true,
};
},
inject: ["$showError", "$showSuccess"],
computed: {
...mapState(useFileStore, [
"req",
"selected",
"selectedCount",
"isListing",
]),
url() {
if (!this.isListing) {
return this.$route.path;
}
const fileStore = useFileStore(); if (this.selectedCount === 0 || this.selectedCount > 1) {
const layoutStore = useLayoutStore(); // This shouldn't happen.
return;
}
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore); return this.req.items[this.selected[0]].url;
const { closeHovers } = layoutStore;
const time = ref(0);
const unit = ref("hours");
const links = ref<any[]>([]);
const password = ref("");
const listing = ref(true);
const url = computed(() => {
if (!isListing.value) {
return route.path;
}
if (selectedCount.value === 0 || selectedCount.value > 1) {
// This shouldn't happen.
return "";
}
return req.value?.items[selected.value[0]].url ?? "";
});
const copyToClipboard = (text: string) => {
copy({ text }).then(
() => {
// clipboard successfully set
$showSuccess?.(t("success.linkCopied"));
}, },
() => { },
// clipboard write failed async beforeMount() {
copy({ text }, { permission: true }).then( try {
const links = await api.get(this.url);
this.links = links;
this.sort();
if (this.links.length == 0) {
this.listing = false;
}
} catch (e) {
this.$showError(e);
}
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
copyToClipboard: function (text) {
copy({ text }).then(
() => { () => {
// clipboard successfully set // clipboard successfully set
$showSuccess?.(t("success.linkCopied")); this.$showSuccess(this.$t("success.linkCopied"));
}, },
(e) => { () => {
// clipboard write failed // clipboard write failed
$showError?.(e); copy({ text }, { permission: true }).then(
() => {
// clipboard successfully set
this.$showSuccess(this.$t("success.linkCopied"));
},
(e) => {
// clipboard write failed
this.$showError(e);
}
);
} }
); );
} },
); submit: async function () {
try {
let res = null;
if (!this.time) {
res = await api.create(this.url, this.password);
} else {
res = await api.create(this.url, this.password, this.time, this.unit);
}
this.links.push(res);
this.sort();
this.time = 0;
this.unit = "hours";
this.password = "";
this.listing = true;
} catch (e) {
this.$showError(e);
}
},
deleteLink: async function (event, link) {
event.preventDefault();
try {
await api.remove(link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash);
if (this.links.length == 0) {
this.listing = false;
}
} catch (e) {
this.$showError(e);
}
},
humanTime(time) {
return dayjs(time * 1000).fromNow();
},
buildLink(share) {
return api.getShareURL(share);
},
sort() {
this.links = this.links.sort((a, b) => {
if (a.expire === 0) return -1;
if (b.expire === 0) return 1;
return new Date(a.expire) - new Date(b.expire);
});
},
switchListing() {
if (this.links.length == 0 && !this.listing) {
this.closeHovers();
}
this.listing = !this.listing;
},
},
}; };
const submit = async () => {
try {
let res = null;
if (!time.value) {
res = await api.create(url.value, password.value);
} else {
res = await api.create(
url.value,
password.value,
String(time.value),
unit.value
);
}
links.value.push(res);
sort();
time.value = 0;
unit.value = "hours";
password.value = "";
listing.value = true;
} catch (e) {
$showError?.(e);
}
};
const deleteLink = async (event: Event, link: any) => {
event.preventDefault();
try {
await api.remove(link.hash);
links.value = links.value.filter((item) => item.hash !== link.hash);
if (links.value.length == 0) {
listing.value = false;
}
} catch (e) {
$showError?.(e);
}
};
const humanTime = (time: number) => {
return dayjs(time * 1000).fromNow();
};
const buildLink = (share: any) => {
return api.getShareURL(share);
};
const sort = () => {
links.value = links.value.sort((a, b) => {
if (a.expire === 0) return -1;
if (b.expire === 0) return 1;
return new Date(a.expire).getTime() - new Date(b.expire).getTime();
});
};
const switchListing = () => {
if (links.value.length == 0 && !listing.value) {
closeHovers();
}
listing.value = !listing.value;
};
onBeforeMount(async () => {
try {
const fetchedLinks = await api.get(url.value);
links.value = Array.isArray(fetchedLinks) ? fetchedLinks : [fetchedLinks];
sort();
if (links.value.length == 0) {
listing.value = false;
}
} catch (e) {
$showError?.(e);
}
});
</script> </script>

View File

@@ -27,15 +27,20 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { storeToRefs } from "pinia"; import { mapActions, mapState } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
const layoutStore = useLayoutStore(); export default {
const { currentPrompt } = storeToRefs(layoutStore); name: "share-delete",
const { closeHovers } = layoutStore; computed: {
...mapState(useLayoutStore, ["currentPrompt"]),
const submit = () => { },
currentPrompt.value?.confirm(); methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
submit: function () {
this.currentPrompt?.confirm();
},
},
}; };
</script> </script>

View File

@@ -8,27 +8,23 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { computed } from "vue"; export default {
name: "permissions",
const props = defineProps<{ props: ["commands"],
commands: string[]; computed: {
}>(); raw: {
get() {
const emit = defineEmits<{ return this.commands.join(" ");
"update:commands": [commands: string[]]; },
}>(); set(value) {
if (value !== "") {
const raw = computed({ this.$emit("update:commands", value.split(" "));
get() { } else {
return props.commands.join(" "); this.$emit("update:commands", []);
}
},
},
}, },
set(value: string) { };
if (value !== "") {
emit("update:commands", value.split(" "));
} else {
emit("update:commands", []);
}
},
});
</script> </script>

View File

@@ -6,50 +6,61 @@
</select> </select>
</template> </template>
<script setup lang="ts"> <script>
import { markRaw } from "vue"; import { markRaw } from "vue";
defineProps<{ export default {
locale: string; name: "languages",
}>(); props: ["locale"],
data() {
const dataObj = {};
const locales = {
he: "עברית",
hr: "Hrvatski",
hu: "Magyar",
ar: "العربية",
ca: "Català",
cs: "Čeština",
de: "Deutsch",
el: "Ελληνικά",
en: "English",
es: "Español",
fr: "Français",
is: "Icelandic",
it: "Italiano",
ja: "日本語",
ko: "한국어",
"nl-be": "Dutch (Belgium)",
no: "Norsk",
pl: "Polski",
"pt-br": "Português",
pt: "Português (Brasil)",
ro: "Romanian",
ru: "Русский",
sk: "Slovenčina",
"sv-se": "Swedish (Sweden)",
tr: "Türkçe",
uk: "Українська",
vi: "Tiếng Việt",
"zh-cn": "中文 (简体)",
"zh-tw": "中文 (繁體)",
};
const emit = defineEmits<{ // Vue3 reactivity breaks with this configuration
"update:locale": [locale: string]; // so we need to use markRaw as a workaround
}>(); // https://github.com/vuejs/core/issues/3024
Object.defineProperty(dataObj, "locales", {
value: markRaw(locales),
configurable: false,
writable: false,
});
const locales = markRaw({ return dataObj;
he: "עברית", },
hr: "Hrvatski", methods: {
hu: "Magyar", change(event) {
ar: "العربية", this.$emit("update:locale", event.target.value);
ca: "Català", },
cs: "Čeština", },
de: "Deutsch",
el: "Ελληνικά",
en: "English",
es: "Español",
fr: "Français",
is: "Icelandic",
it: "Italiano",
ja: "日本語",
ko: "한국어",
"nl-be": "Dutch (Belgium)",
no: "Norsk",
pl: "Polski",
"pt-br": "Português",
pt: "Português (Brasil)",
ro: "Romanian",
ru: "Русский",
sk: "Slovenčina",
"sv-se": "Swedish (Sweden)",
tr: "Türkçe",
uk: "Українська",
vi: "Tiếng Việt",
"zh-cn": "中文 (简体)",
"zh-tw": "中文 (繁體)",
});
const change = (event: Event) => {
emit("update:locale", (event.target as HTMLSelectElement).value);
}; };
</script> </script>

View File

@@ -39,28 +39,27 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { computed } from "vue";
import { enableExec } from "@/utils/constants"; import { enableExec } from "@/utils/constants";
export default {
name: "permissions",
props: ["perm"],
computed: {
admin: {
get() {
return this.perm.admin;
},
set(value) {
if (value) {
for (const key in this.perm) {
this.perm[key] = true;
}
}
const props = defineProps<{ this.perm.admin = value;
perm: UserPermissions; },
}>(); },
isExecEnabled: () => enableExec,
const admin = computed({
get() {
return props.perm.admin;
}, },
set(value: boolean) { };
if (value) {
for (const key in props.perm) {
props.perm[key as keyof UserPermissions] = true;
}
}
props.perm.admin = value;
},
});
const isExecEnabled = enableExec;
</script> </script>

View File

@@ -32,44 +32,32 @@
</form> </form>
</template> </template>
<script setup lang="ts"> <script>
interface Rule { export default {
allow: boolean; name: "rules-textarea",
path: string; props: ["rules"],
regex: boolean; methods: {
regexp: { remove(event, index) {
raw: string; event.preventDefault();
}; const rules = [...this.rules];
} rules.splice(index, 1);
this.$emit("update:rules", [...rules]);
const props = defineProps<{
rules: Rule[];
}>();
const emit = defineEmits<{
"update:rules": [rules: Rule[]];
}>();
const remove = (event: Event, index: number) => {
event.preventDefault();
const rules = [...props.rules];
rules.splice(index, 1);
emit("update:rules", [...rules]);
};
const create = (event: Event) => {
event.preventDefault();
emit("update:rules", [
...props.rules,
{
allow: true,
path: "",
regex: false,
regexp: {
raw: "",
},
}, },
]); create(event) {
event.preventDefault();
this.$emit("update:rules", [
...this.rules,
{
allow: true,
path: "",
regex: false,
regexp: {
raw: "",
},
},
]);
},
},
}; };
</script> </script>

View File

@@ -80,20 +80,12 @@ const { t } = useI18n();
const createUserDirData = ref<boolean | null>(null); const createUserDirData = ref<boolean | null>(null);
const originalUserScope = ref<string | null>(null); const originalUserScope = ref<string | null>(null);
const props = defineProps< const props = defineProps<{
| { user: IUserForm;
user: IUserForm; isNew: boolean;
isNew: boolean; isDefault: boolean;
isDefault: false; createUserDir?: boolean;
createUserDir?: boolean; }>();
}
| {
user: SettingsDefaults;
isNew: boolean;
isDefault: true;
createUserDir?: boolean;
}
>();
onMounted(() => { onMounted(() => {
if (props.user.scope) { if (props.user.scope) {
@@ -116,7 +108,6 @@ watch(
() => props.user, () => props.user,
() => { () => {
if (!props.user?.perm?.admin) return; if (!props.user?.perm?.admin) return;
if (props.isDefault) return;
props.user.lockPassword = false; props.user.lockPassword = false;
} }
); );

View File

@@ -0,0 +1,21 @@
.context-menu {
position: absolute;
background: var(--surfacePrimary);
min-width: 180px;
max-width: 220px;
border: 1px solid var(--borderSecondary);
box-shadow: 0 2px 4px var(--borderPrimary);
z-index: 999;
}
.context-menu .action {
display: block;
width: 100%;
border-radius: 0;
display: flex;
align-items: center;
}
.context-menu .action .counter {
left: 1.75em;
}

View File

@@ -17,6 +17,7 @@
@import "./mobile.css"; @import "./mobile.css";
@import "./epubReader.css"; @import "./epubReader.css";
@import "./mdPreview.css"; @import "./mdPreview.css";
@import "./context-menu.css";
/* For testing only /* For testing only
:focus { :focus {
@@ -459,4 +460,4 @@ html[dir="rtl"] .card-content .small + input {
html[dir="rtl"] .card.floating .card-content .file-list { html[dir="rtl"] .card.floating .card-content .file-list {
direction: ltr; direction: ltr;
text-align: left; text-align: left;
} }

View File

@@ -166,6 +166,7 @@
"allowNew": "إنشاء ملفات و مجلدات جديدة", "allowNew": "إنشاء ملفات و مجلدات جديدة",
"allowPublish": "نشر مقالات و صفحات جديدة", "allowPublish": "نشر مقالات و صفحات جديدة",
"allowSignup": "اسمح للمستخدمين بالاشتراك", "allowSignup": "اسمح للمستخدمين بالاشتراك",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(أتركه فارغاً إن لم ترد تغييره)", "avoidChanges": "(أتركه فارغاً إن لم ترد تغييره)",
"branding": "الشعار", "branding": "الشعار",
"brandingDirectoryPath": "مسار مجلد الشعار", "brandingDirectoryPath": "مسار مجلد الشعار",

View File

@@ -166,6 +166,7 @@
"allowNew": "Crear nous fitxers i carpetes", "allowNew": "Crear nous fitxers i carpetes",
"allowPublish": "Publicar nous posts i pàgines", "allowPublish": "Publicar nous posts i pàgines",
"allowSignup": "Permetre registre d'usuaris", "allowSignup": "Permetre registre d'usuaris",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(deixar en blanc per evitar canvis)", "avoidChanges": "(deixar en blanc per evitar canvis)",
"branding": "Marca", "branding": "Marca",
"brandingDirectoryPath": "Ruta de la carpeta de personalització de marca", "brandingDirectoryPath": "Ruta de la carpeta de personalització de marca",

View File

@@ -166,6 +166,7 @@
"allowNew": "Vytvářet nové soubory a adresáře", "allowNew": "Vytvářet nové soubory a adresáře",
"allowPublish": "Publikovat nové příspěvky a stránky", "allowPublish": "Publikovat nové příspěvky a stránky",
"allowSignup": "Povolit uživatelům registraci", "allowSignup": "Povolit uživatelům registraci",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(ponechte prázdné pro zabránění změnám)", "avoidChanges": "(ponechte prázdné pro zabránění změnám)",
"branding": "Branding", "branding": "Branding",
"brandingDirectoryPath": "Cesta ke složce s brandingem", "brandingDirectoryPath": "Cesta ke složce s brandingem",

View File

@@ -166,6 +166,7 @@
"allowNew": "Erstellen neuer Dateien und Ordner", "allowNew": "Erstellen neuer Dateien und Ordner",
"allowPublish": "Veröffentlichen von neuen Beiträgen und Seiten", "allowPublish": "Veröffentlichen von neuen Beiträgen und Seiten",
"allowSignup": "Erlaube Benutzern sich zu registrieren", "allowSignup": "Erlaube Benutzern sich zu registrieren",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(leer lassen, um Änderungen zu vermeiden)", "avoidChanges": "(leer lassen, um Änderungen zu vermeiden)",
"branding": "Design", "branding": "Design",
"brandingDirectoryPath": "Designverzeichnispfad", "brandingDirectoryPath": "Designverzeichnispfad",

View File

@@ -166,6 +166,7 @@
"allowNew": "Δημιουργία νέων αρχείων και φακέλων", "allowNew": "Δημιουργία νέων αρχείων και φακέλων",
"allowPublish": "Δημοσίευση νέων αναρτήσεων και σελίδων", "allowPublish": "Δημοσίευση νέων αναρτήσεων και σελίδων",
"allowSignup": "Να επιτρέπεται η εγγραφή νέων χρηστών", "allowSignup": "Να επιτρέπεται η εγγραφή νέων χρηστών",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(αφήστε το κενό για αποφυγή αλλαγών)", "avoidChanges": "(αφήστε το κενό για αποφυγή αλλαγών)",
"branding": "Εξατομίκευση", "branding": "Εξατομίκευση",
"brandingDirectoryPath": "Διαδρομή φακέλου εξατομίκευσης", "brandingDirectoryPath": "Διαδρομή φακέλου εξατομίκευσης",

View File

@@ -166,6 +166,7 @@
"allowNew": "Create new files and directories", "allowNew": "Create new files and directories",
"allowPublish": "Publish new posts and pages", "allowPublish": "Publish new posts and pages",
"allowSignup": "Allow users to signup", "allowSignup": "Allow users to signup",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(leave blank to avoid changes)", "avoidChanges": "(leave blank to avoid changes)",
"branding": "Branding", "branding": "Branding",
"brandingDirectoryPath": "Branding directory path", "brandingDirectoryPath": "Branding directory path",

View File

@@ -166,6 +166,7 @@
"allowNew": "Crear nuevos archivos y carpetas", "allowNew": "Crear nuevos archivos y carpetas",
"allowPublish": "Publicar nuevos posts y páginas", "allowPublish": "Publicar nuevos posts y páginas",
"allowSignup": "Permitir registro de usuarios", "allowSignup": "Permitir registro de usuarios",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(dejar en blanco para evitar cambios)", "avoidChanges": "(dejar en blanco para evitar cambios)",
"branding": "Marca", "branding": "Marca",
"brandingDirectoryPath": "Ruta de la carpeta de personalizacion de marca", "brandingDirectoryPath": "Ruta de la carpeta de personalizacion de marca",

View File

@@ -166,6 +166,7 @@
"allowNew": "ایجاد فایلها و پوشه های جدید", "allowNew": "ایجاد فایلها و پوشه های جدید",
"allowPublish": "انتشار پست ها و صفحات جدید", "allowPublish": "انتشار پست ها و صفحات جدید",
"allowSignup": "اجاره دادن به کاربران برای ثبت نام", "allowSignup": "اجاره دادن به کاربران برای ثبت نام",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(خالی بگذارید تا تغییر ایجاد نشود)", "avoidChanges": "(خالی بگذارید تا تغییر ایجاد نشود)",
"branding": "برندسازی", "branding": "برندسازی",
"brandingDirectoryPath": "مسیر پوشه برند", "brandingDirectoryPath": "مسیر پوشه برند",

View File

@@ -166,6 +166,7 @@
"allowNew": "Créer de nouveaux fichiers et dossiers", "allowNew": "Créer de nouveaux fichiers et dossiers",
"allowPublish": "Publier de nouveaux posts et pages", "allowPublish": "Publier de nouveaux posts et pages",
"allowSignup": "Autoriser les utilisateur·ices à s'inscrire", "allowSignup": "Autoriser les utilisateur·ices à s'inscrire",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(Laisser vide pour conserver l'actuel)", "avoidChanges": "(Laisser vide pour conserver l'actuel)",
"branding": "Image de marque", "branding": "Image de marque",
"brandingDirectoryPath": "Chemin du dossier d'image de marque", "brandingDirectoryPath": "Chemin du dossier d'image de marque",

View File

@@ -166,6 +166,7 @@
"allowNew": "יצירת קבצים ותיקיות חדשות", "allowNew": "יצירת קבצים ותיקיות חדשות",
"allowPublish": "פרסום פוסטים ודפים חדשים", "allowPublish": "פרסום פוסטים ודפים חדשים",
"allowSignup": "אפשר למשתמשים חדשים להירשם", "allowSignup": "אפשר למשתמשים חדשים להירשם",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(השאר ריק כדי למנוע שינויים)", "avoidChanges": "(השאר ריק כדי למנוע שינויים)",
"branding": "מיתוג", "branding": "מיתוג",
"brandingDirectoryPath": "נתיב תיקיית מיתוג", "brandingDirectoryPath": "נתיב תיקיית מיתוג",

View File

@@ -166,6 +166,7 @@
"allowNew": "Stvori nove datoteke i mape", "allowNew": "Stvori nove datoteke i mape",
"allowPublish": "Objavi nove objave i stranice", "allowPublish": "Objavi nove objave i stranice",
"allowSignup": "Dopusti registraciju korisnicima", "allowSignup": "Dopusti registraciju korisnicima",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(ostavite prazno kako biste izbjegli promjene)", "avoidChanges": "(ostavite prazno kako biste izbjegli promjene)",
"branding": "Brendiranje", "branding": "Brendiranje",
"brandingDirectoryPath": "Put brendiranja", "brandingDirectoryPath": "Put brendiranja",

View File

@@ -166,6 +166,7 @@
"allowNew": "Új fájlok és mappák létrehozása", "allowNew": "Új fájlok és mappák létrehozása",
"allowPublish": "Új bejegyzések és oldalak létrehozása", "allowPublish": "Új bejegyzések és oldalak létrehozása",
"allowSignup": "Felhasználók regisztrációjának engedélyezése", "allowSignup": "Felhasználók regisztrációjának engedélyezése",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(üresen hagyva nincs változás)", "avoidChanges": "(üresen hagyva nincs változás)",
"branding": "Márkázás", "branding": "Márkázás",
"brandingDirectoryPath": "Márkázás mappaútvonala", "brandingDirectoryPath": "Márkázás mappaútvonala",

View File

@@ -166,6 +166,7 @@
"allowNew": "Búa til ný skjöl og möppur", "allowNew": "Búa til ný skjöl og möppur",
"allowPublish": "Gefa út nýjar færslur og síður", "allowPublish": "Gefa út nýjar færslur og síður",
"allowSignup": "Leyfa nýjum notendum að skrá sig", "allowSignup": "Leyfa nýjum notendum að skrá sig",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(engar breytingar ef ekkert er skrifað)", "avoidChanges": "(engar breytingar ef ekkert er skrifað)",
"branding": "Útlit", "branding": "Útlit",
"brandingDirectoryPath": "Mappa fyrir branding-skjöl", "brandingDirectoryPath": "Mappa fyrir branding-skjöl",

View File

@@ -166,6 +166,7 @@
"allowNew": "Crea nuovi files o cartelle", "allowNew": "Crea nuovi files o cartelle",
"allowPublish": "Pubblica nuovi post e pagine", "allowPublish": "Pubblica nuovi post e pagine",
"allowSignup": "Permetti agli utenti di registrarsi", "allowSignup": "Permetti agli utenti di registrarsi",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(lascia vuoto per evitare cambiamenti)", "avoidChanges": "(lascia vuoto per evitare cambiamenti)",
"branding": "Branding", "branding": "Branding",
"brandingDirectoryPath": "Directory del branding", "brandingDirectoryPath": "Directory del branding",

View File

@@ -166,6 +166,7 @@
"allowNew": "ファイルやフォルダーの新規作成", "allowNew": "ファイルやフォルダーの新規作成",
"allowPublish": "新しい投稿やページの公開", "allowPublish": "新しい投稿やページの公開",
"allowSignup": "ユーザーの新規登録を許可", "allowSignup": "ユーザーの新規登録を許可",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(変更しない場合は空白のままにしてください)", "avoidChanges": "(変更しない場合は空白のままにしてください)",
"branding": "ブランディング", "branding": "ブランディング",
"brandingDirectoryPath": "ブランディングのディレクトリへのパス", "brandingDirectoryPath": "ブランディングのディレクトリへのパス",

View File

@@ -166,6 +166,7 @@
"allowNew": "파일/디렉토리 생성 허용", "allowNew": "파일/디렉토리 생성 허용",
"allowPublish": "새 포스트/페이지 생성 허용", "allowPublish": "새 포스트/페이지 생성 허용",
"allowSignup": "사용자 가입 허용", "allowSignup": "사용자 가입 허용",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(수정하지 않으면 비워두세요)", "avoidChanges": "(수정하지 않으면 비워두세요)",
"branding": "브랜딩", "branding": "브랜딩",
"brandingDirectoryPath": "브랜드 디렉토리 경로", "brandingDirectoryPath": "브랜드 디렉토리 경로",

View File

@@ -166,6 +166,7 @@
"allowNew": "Nieuwe bestanden of mappen aanmaken", "allowNew": "Nieuwe bestanden of mappen aanmaken",
"allowPublish": "Publiceer nieuwe berichten en pagina's", "allowPublish": "Publiceer nieuwe berichten en pagina's",
"allowSignup": "Sta gebruikers toe om zich te registreren", "allowSignup": "Sta gebruikers toe om zich te registreren",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(laat leeg om wijzigingen te voorkomen)", "avoidChanges": "(laat leeg om wijzigingen te voorkomen)",
"branding": "Branding", "branding": "Branding",
"brandingDirectoryPath": "Branding directory path", "brandingDirectoryPath": "Branding directory path",

View File

@@ -166,6 +166,7 @@
"allowNew": "Opprett nye filer og direktorater", "allowNew": "Opprett nye filer og direktorater",
"allowPublish": "Publiser nye innlegg og sider", "allowPublish": "Publiser nye innlegg og sider",
"allowSignup": "Tilat brukere å registrere seg", "allowSignup": "Tilat brukere å registrere seg",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(la stå tomt for å unngå endringer)", "avoidChanges": "(la stå tomt for å unngå endringer)",
"branding": "Merkevarebygging", "branding": "Merkevarebygging",
"brandingDirectoryPath": "Bane for merkevarekatalog", "brandingDirectoryPath": "Bane for merkevarekatalog",

View File

@@ -166,6 +166,7 @@
"allowNew": "Tworzenie nowych plików lub folderów", "allowNew": "Tworzenie nowych plików lub folderów",
"allowPublish": "Tworzenie nowych wpisów i stron", "allowPublish": "Tworzenie nowych wpisów i stron",
"allowSignup": "Pozwól użytkownikom na rejestrację", "allowSignup": "Pozwól użytkownikom na rejestrację",
"hideLoginButton": "Ukryj przycisk logowania na stronach publicznych",
"avoidChanges": "(pozostaw puste, aby uniknąć zmian)", "avoidChanges": "(pozostaw puste, aby uniknąć zmian)",
"branding": "Personalizacja", "branding": "Personalizacja",
"brandingDirectoryPath": "Ścieżka do folderu personalizacji", "brandingDirectoryPath": "Ścieżka do folderu personalizacji",

View File

@@ -166,6 +166,7 @@
"allowNew": "Criar novos arquivos e pastas", "allowNew": "Criar novos arquivos e pastas",
"allowPublish": "Publicar novas páginas e conteúdos", "allowPublish": "Publicar novas páginas e conteúdos",
"allowSignup": "Permitir cadastro de usuários", "allowSignup": "Permitir cadastro de usuários",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(deixe em branco para manter)", "avoidChanges": "(deixe em branco para manter)",
"branding": "Customização", "branding": "Customização",
"brandingDirectoryPath": "Diretório de customização", "brandingDirectoryPath": "Diretório de customização",

View File

@@ -166,6 +166,7 @@
"allowNew": "Criar novos ficheiros e pastas", "allowNew": "Criar novos ficheiros e pastas",
"allowPublish": "Publicar novas páginas e conteúdos", "allowPublish": "Publicar novas páginas e conteúdos",
"allowSignup": "Permitir que os utilizadores criem contas", "allowSignup": "Permitir que os utilizadores criem contas",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(deixe em branco para manter)", "avoidChanges": "(deixe em branco para manter)",
"branding": "Marca", "branding": "Marca",
"brandingDirectoryPath": "Caminho da pasta de marca", "brandingDirectoryPath": "Caminho da pasta de marca",

View File

@@ -166,6 +166,7 @@
"allowNew": "Crează noi fișiere sau directoare", "allowNew": "Crează noi fișiere sau directoare",
"allowPublish": "Publică noi pagini și postări", "allowPublish": "Publică noi pagini și postări",
"allowSignup": "Permite utilizatorilor să se înregistreze", "allowSignup": "Permite utilizatorilor să se înregistreze",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(lasă gol pentru a nu schimba)", "avoidChanges": "(lasă gol pentru a nu schimba)",
"branding": "Branding", "branding": "Branding",
"brandingDirectoryPath": "Calea către directorul de branding", "brandingDirectoryPath": "Calea către directorul de branding",

View File

@@ -166,6 +166,7 @@
"allowNew": "Создание новых файлов или каталогов", "allowNew": "Создание новых файлов или каталогов",
"allowPublish": "Публикация новых записей и страниц", "allowPublish": "Публикация новых записей и страниц",
"allowSignup": "Разрешить пользователям регистрироваться", "allowSignup": "Разрешить пользователям регистрироваться",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(оставьте поле пустым, чтобы избежать изменений)", "avoidChanges": "(оставьте поле пустым, чтобы избежать изменений)",
"branding": "Брендинг", "branding": "Брендинг",
"brandingDirectoryPath": "Путь к каталогу брендов", "brandingDirectoryPath": "Путь к каталогу брендов",

View File

@@ -166,6 +166,7 @@
"allowNew": "Vytvárať nové súbory a priečinky", "allowNew": "Vytvárať nové súbory a priečinky",
"allowPublish": "Zverejňovať nové príspevky a stránky", "allowPublish": "Zverejňovať nové príspevky a stránky",
"allowSignup": "Povoliť registráciu používateľov", "allowSignup": "Povoliť registráciu používateľov",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(nechajte prázdne, aby sa nezmenilo)", "avoidChanges": "(nechajte prázdne, aby sa nezmenilo)",
"branding": "Vlastný vzhľad", "branding": "Vlastný vzhľad",
"brandingDirectoryPath": "Cesta k priečinku s vlastným vzhľadom", "brandingDirectoryPath": "Cesta k priečinku s vlastným vzhľadom",

View File

@@ -166,6 +166,7 @@
"allowNew": "Skapa nya filer eller mappar", "allowNew": "Skapa nya filer eller mappar",
"allowPublish": "Publicera nya inlägg och sidor", "allowPublish": "Publicera nya inlägg och sidor",
"allowSignup": "Tillåt användare att registrera sig", "allowSignup": "Tillåt användare att registrera sig",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(lämna blankt för att undvika ändringar)", "avoidChanges": "(lämna blankt för att undvika ändringar)",
"branding": "Varumärke", "branding": "Varumärke",
"brandingDirectoryPath": "Sökväg till varumärkes katalog", "brandingDirectoryPath": "Sökväg till varumärkes katalog",

View File

@@ -166,6 +166,7 @@
"allowNew": "Yeni dosyalar ve dizinler oluşturun", "allowNew": "Yeni dosyalar ve dizinler oluşturun",
"allowPublish": "Yeni linkler ve sayfaları yayınlayın", "allowPublish": "Yeni linkler ve sayfaları yayınlayın",
"allowSignup": "Kullanıcıların kaydolmasına izin ver", "allowSignup": "Kullanıcıların kaydolmasına izin ver",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(değişiklikleri önlemek için boş bırakın)", "avoidChanges": "(değişiklikleri önlemek için boş bırakın)",
"branding": "Marka", "branding": "Marka",
"brandingDirectoryPath": "Marka dizin yolu", "brandingDirectoryPath": "Marka dizin yolu",

View File

@@ -166,6 +166,7 @@
"allowNew": "Створення нових файлів або каталогів", "allowNew": "Створення нових файлів або каталогів",
"allowPublish": "Публікація нових записів та сторінок", "allowPublish": "Публікація нових записів та сторінок",
"allowSignup": "Дозволити користувачам реєструватися", "allowSignup": "Дозволити користувачам реєструватися",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(залишіть поле порожнім, щоб уникнути змін)", "avoidChanges": "(залишіть поле порожнім, щоб уникнути змін)",
"branding": "Брендинг", "branding": "Брендинг",
"brandingDirectoryPath": "Шлях до каталогу брендів", "brandingDirectoryPath": "Шлях до каталогу брендів",

View File

@@ -166,6 +166,7 @@
"allowNew": "Tạo tệp và thư mục mới", "allowNew": "Tạo tệp và thư mục mới",
"allowPublish": "Xuất bản bài viết và trang mới", "allowPublish": "Xuất bản bài viết và trang mới",
"allowSignup": "Cho phép người dùng đăng ký", "allowSignup": "Cho phép người dùng đăng ký",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(để trống để tránh thay đổi)", "avoidChanges": "(để trống để tránh thay đổi)",
"branding": "Thương hiệu", "branding": "Thương hiệu",
"brandingDirectoryPath": "Đường dẫn thư mục thương hiệu", "brandingDirectoryPath": "Đường dẫn thư mục thương hiệu",

View File

@@ -166,6 +166,7 @@
"allowNew": "创建新文件和文件夹", "allowNew": "创建新文件和文件夹",
"allowPublish": "发布新的帖子与页面", "allowPublish": "发布新的帖子与页面",
"allowSignup": "允许用户注册", "allowSignup": "允许用户注册",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(留空以避免更改)", "avoidChanges": "(留空以避免更改)",
"branding": "品牌", "branding": "品牌",
"brandingDirectoryPath": "品牌信息文件夹路径", "brandingDirectoryPath": "品牌信息文件夹路径",

View File

@@ -166,6 +166,7 @@
"allowNew": "建立新檔案和目錄", "allowNew": "建立新檔案和目錄",
"allowPublish": "發佈新的貼文與頁面", "allowPublish": "發佈新的貼文與頁面",
"allowSignup": "允許使用者註冊", "allowSignup": "允許使用者註冊",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(留空以避免更改)", "avoidChanges": "(留空以避免更改)",
"branding": "品牌", "branding": "品牌",
"brandingDirectoryPath": "品牌資訊資料夾路徑", "brandingDirectoryPath": "品牌資訊資料夾路徑",

View File

@@ -1,6 +1,7 @@
interface ISettings { interface ISettings {
signup: boolean; signup: boolean;
createUserDir: boolean; createUserDir: boolean;
hideLoginButton: boolean;
minimumPasswordLength: number; minimumPasswordLength: number;
userHomeBasePath: string; userHomeBasePath: string;
defaults: SettingsDefaults; defaults: SettingsDefaults;
@@ -17,7 +18,7 @@ interface SettingsDefaults {
viewMode: ViewModeType; viewMode: ViewModeType;
singleClick: boolean; singleClick: boolean;
sorting: Sorting; sorting: Sorting;
perm: UserPermissions; perm: Permissions;
commands: any[]; commands: any[];
hideDotfiles: boolean; hideDotfiles: boolean;
dateFormat: boolean; dateFormat: boolean;

View File

@@ -4,7 +4,7 @@ interface IUser {
password: string; password: string;
scope: string; scope: string;
locale: string; locale: string;
perm: UserPermissions; perm: Permissions;
commands: string[]; commands: string[];
rules: IRule[]; rules: IRule[];
lockPassword: boolean; lockPassword: boolean;
@@ -20,20 +20,20 @@ type ViewModeType = "list" | "mosaic" | "mosaic gallery";
interface IUserForm { interface IUserForm {
id?: number; id?: number;
username: string; username?: string;
password: string; password?: string;
scope: string; scope?: string;
locale: string; locale?: string;
perm: UserPermissions; perm?: Permissions;
commands: string[]; commands?: string[];
rules: IRule[]; rules?: IRule[];
lockPassword: boolean; lockPassword?: boolean;
hideDotfiles: boolean; hideDotfiles?: boolean;
singleClick: boolean; singleClick?: boolean;
dateFormat: boolean; dateFormat?: boolean;
} }
interface UserPermissions { interface Permissions {
admin: boolean; admin: boolean;
copy: boolean; copy: boolean;
create: boolean; create: boolean;

View File

@@ -18,6 +18,7 @@ const enableExec: boolean = window.FileBrowser.EnableExec;
const tusSettings = window.FileBrowser.TusSettings; const tusSettings = window.FileBrowser.TusSettings;
const origin = window.location.origin; const origin = window.location.origin;
const tusEndpoint = `/api/tus`; const tusEndpoint = `/api/tus`;
const hideLoginButton = window.FileBrowser.HideLoginButton;
export { export {
name, name,
@@ -39,4 +40,5 @@ export {
tusSettings, tusSettings,
origin, origin,
tusEndpoint, tusEndpoint,
hideLoginButton,
}; };

View File

@@ -207,7 +207,10 @@
<h2 v-if="fileStore.req?.numDirs ?? false"> <h2 v-if="fileStore.req?.numDirs ?? false">
{{ t("files.folders") }} {{ t("files.folders") }}
</h2> </h2>
<div v-if="fileStore.req?.numDirs ?? false"> <div
v-if="fileStore.req?.numDirs ?? false"
@contextmenu="showContextMenu"
>
<item <item
v-for="item in dirs" v-for="item in dirs"
:key="base64(item.name)" :key="base64(item.name)"
@@ -223,8 +226,13 @@
</item> </item>
</div> </div>
<h2 v-if="fileStore.req?.numFiles ?? false">{{ t("files.files") }}</h2> <h2 v-if="fileStore.req?.numFiles ?? false">
<div v-if="fileStore.req?.numFiles ?? false"> {{ t("files.files") }}
</h2>
<div
v-if="fileStore.req?.numFiles ?? false"
@contextmenu="showContextMenu"
>
<item <item
v-for="item in files" v-for="item in files"
:key="base64(item.name)" :key="base64(item.name)"
@@ -239,6 +247,53 @@
> >
</item> </item>
</div> </div>
<context-menu
:show="isContextMenuVisible"
:pos="contextMenuPos"
@hide="hideContextMenu"
>
<action
v-if="headerButtons.share"
icon="share"
:label="t('buttons.share')"
show="share"
/>
<action
v-if="headerButtons.rename"
icon="mode_edit"
:label="t('buttons.rename')"
show="rename"
/>
<action
v-if="headerButtons.copy"
id="copy-button"
icon="content_copy"
:label="t('buttons.copyFile')"
show="copy"
/>
<action
v-if="headerButtons.move"
id="move-button"
icon="forward"
:label="t('buttons.moveFile')"
show="move"
/>
<action
v-if="headerButtons.delete"
id="delete-button"
icon="delete"
:label="t('buttons.delete')"
show="delete"
/>
<action
v-if="headerButtons.download"
icon="file_download"
:label="t('buttons.download')"
@action="download"
:counter="fileStore.selectedCount"
/>
<action icon="info" :label="t('buttons.info')" show="info" />
</context-menu>
<input <input
style="display: none" style="display: none"
@@ -291,6 +346,7 @@ import HeaderBar from "@/components/header/HeaderBar.vue";
import Action from "@/components/header/Action.vue"; import Action from "@/components/header/Action.vue";
import Search from "@/components/Search.vue"; import Search from "@/components/Search.vue";
import Item from "@/components/files/ListingItem.vue"; import Item from "@/components/files/ListingItem.vue";
import ContextMenu from "@/components/ContextMenu.vue";
import { import {
computed, computed,
inject, inject,
@@ -310,6 +366,8 @@ const columnWidth = ref<number>(280);
const dragCounter = ref<number>(0); const dragCounter = ref<number>(0);
const width = ref<number>(window.innerWidth); const width = ref<number>(window.innerWidth);
const itemWeight = ref<number>(0); const itemWeight = ref<number>(0);
const isContextMenuVisible = ref<boolean>(false);
const contextMenuPos = ref<{ x: number; y: number }>({ x: 0, y: 0 });
const $showError = inject<IToastError>("$showError")!; const $showError = inject<IToastError>("$showError")!;
@@ -438,7 +496,7 @@ watch(req, () => {
onMounted(() => { onMounted(() => {
// Check the columns size for the first time. // Check the columns size for the first time.
colunmsResize(); columnsResize();
// How much every listing item affects the window height // How much every listing item affects the window height
setItemWeight(); setItemWeight();
@@ -642,7 +700,7 @@ const paste = (event: Event) => {
action(overwrite, rename); action(overwrite, rename);
}; };
const colunmsResize = () => { const columnsResize = () => {
// Update the columns size based on the window width. // Update the columns size based on the window width.
const items_ = css(["#listing.mosaic .item", ".mosaic#listing .item"]); const items_ = css(["#listing.mosaic .item", ".mosaic#listing .item"]);
if (items_ === null) return; if (items_ === null) return;
@@ -850,7 +908,7 @@ const toggleMultipleSelection = () => {
}; };
const windowsResize = throttle(() => { const windowsResize = throttle(() => {
colunmsResize(); columnsResize();
width.value = window.innerWidth; width.value = window.innerWidth;
// Listing element is not displayed // Listing element is not displayed
@@ -977,4 +1035,17 @@ const revealPreviousItem = () => {
return true; return true;
}; };
const showContextMenu = (event: MouseEvent) => {
event.preventDefault();
isContextMenuVisible.value = true;
contextMenuPos.value = {
x: event.clientX + 8,
y: event.clientY + Math.floor(window.scrollY),
};
};
const hideContextMenu = () => {
isContextMenuVisible.value = false;
};
</script> </script>

View File

@@ -18,6 +18,11 @@
{{ t("settings.createUserDir") }} {{ t("settings.createUserDir") }}
</p> </p>
<p>
<input type="checkbox" v-model="settings.hideLoginButton" />
{{ t("settings.hideLoginButton") }}
</p>
<p> <p>
<label class="small">{{ t("settings.userHomeBasePath") }}</label> <label class="small">{{ t("settings.userHomeBasePath") }}</label>
<input <input

2
go.mod
View File

@@ -8,7 +8,7 @@ require (
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/dsoprea/go-exif/v3 v3.0.1 github.com/dsoprea/go-exif/v3 v3.0.1
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568
github.com/golang-jwt/jwt/v4 v4.5.2 github.com/golang-jwt/jwt/v5 v5.3.0
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/jellydator/ttlcache/v3 v3.4.0 github.com/jellydator/ttlcache/v3 v3.4.0

4
go.sum
View File

@@ -97,8 +97,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=

View File

@@ -9,8 +9,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
"github.com/golang-jwt/jwt/v4/request" "github.com/golang-jwt/jwt/v5/request"
fbErrors "github.com/filebrowser/filebrowser/v2/errors" fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
@@ -69,15 +69,19 @@ func withUser(fn handleFunc) handleFunc {
var tk authToken var tk authToken
token, err := request.ParseFromRequest(r, &extractor{}, keyFunc, request.WithClaims(&tk)) token, err := request.ParseFromRequest(r, &extractor{}, keyFunc, request.WithClaims(&tk))
if err != nil || !token.Valid { if err != nil || !token.Valid {
return http.StatusUnauthorized, nil return http.StatusUnauthorized, nil
} }
expired := !tk.VerifyExpiresAt(time.Now().Add(time.Hour), true) err = jwt.NewValidator(jwt.WithExpirationRequired()).Validate(tk)
if err != nil {
return http.StatusUnauthorized, nil
}
expiresSoon := tk.ExpiresAt != nil && time.Until(tk.ExpiresAt.Time) < time.Hour
updated := tk.IssuedAt != nil && tk.IssuedAt.Unix() < d.store.Users.LastUpdate(tk.User.ID) updated := tk.IssuedAt != nil && tk.IssuedAt.Unix() < d.store.Users.LastUpdate(tk.User.ID)
if expired || updated { if expiresSoon || updated {
w.Header().Add("X-Renew-Token", "true") w.Header().Add("X-Renew-Token", "true")
} }

View File

@@ -1,14 +0,0 @@
//go:build dev
package http
// global headers to append to every response
// cross-origin headers are necessary to be able to
// access them from a different URL during development
var globalHeaders = map[string]string{
"Cache-Control": "no-cache, no-store, must-revalidate",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Credentials": "true",
}

View File

@@ -124,12 +124,12 @@ func createPreview(imgSvc ImgService, fileCache FileCache,
options []img.Option options []img.Option
) )
switch { switch previewSize {
case previewSize == PreviewSizeBig: case PreviewSizeBig:
width = 1080 width = 1080
height = 1080 height = 1080
options = append(options, img.WithMode(img.ResizeModeFit), img.WithQuality(img.QualityMedium)) options = append(options, img.WithMode(img.ResizeModeFit), img.WithQuality(img.QualityMedium))
case previewSize == PreviewSizeThumb: case PreviewSizeThumb:
width = 256 width = 256
height = 256 height = 256
options = append(options, img.WithMode(img.ResizeModeFill), img.WithQuality(img.QualityLow), img.WithFormat(img.FormatJpeg)) options = append(options, img.WithMode(img.ResizeModeFill), img.WithQuality(img.QualityLow), img.WithFormat(img.FormatJpeg))

View File

@@ -98,8 +98,8 @@ 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.Sorting = files.Sorting{By: "name", Asc: false}
file.Listing.ApplySort() file.ApplySort()
return renderJSON(w, r, file) return renderJSON(w, r, file)
} }

View File

@@ -32,7 +32,7 @@ func parseQueryFiles(r *http.Request, f *files.FileInfo, _ *users.User) ([]strin
fileSlice = append(fileSlice, f.Path) fileSlice = append(fileSlice, f.Path)
} else { } else {
for _, name := range names { for _, name := range names {
name, err := url.QueryUnescape(strings.Replace(name, "+", "%2B", -1)) //nolint:govet name, err := url.QueryUnescape(strings.ReplaceAll(name, "+", "%2B"))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -37,8 +37,8 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
} }
if file.IsDir { if file.IsDir {
file.Listing.Sorting = d.user.Sorting file.Sorting = d.user.Sorting
file.Listing.ApplySort() file.ApplySort()
return renderJSON(w, r, file) return renderJSON(w, r, file)
} }

View File

@@ -10,6 +10,7 @@ import (
type settingsData struct { type settingsData struct {
Signup bool `json:"signup"` Signup bool `json:"signup"`
HideLoginButton bool `json:"hideLoginButton"`
CreateUserDir bool `json:"createUserDir"` CreateUserDir bool `json:"createUserDir"`
MinimumPasswordLength uint `json:"minimumPasswordLength"` MinimumPasswordLength uint `json:"minimumPasswordLength"`
UserHomeBasePath string `json:"userHomeBasePath"` UserHomeBasePath string `json:"userHomeBasePath"`
@@ -24,6 +25,7 @@ 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: d.settings.Signup, Signup: d.settings.Signup,
HideLoginButton: d.settings.HideLoginButton,
CreateUserDir: d.settings.CreateUserDir, CreateUserDir: d.settings.CreateUserDir,
MinimumPasswordLength: d.settings.MinimumPasswordLength, MinimumPasswordLength: d.settings.MinimumPasswordLength,
UserHomeBasePath: d.settings.UserHomeBasePath, UserHomeBasePath: d.settings.UserHomeBasePath,
@@ -55,6 +57,7 @@ var settingsPutHandler = withAdmin(func(_ http.ResponseWriter, r *http.Request,
d.settings.Tus = req.Tus d.settings.Tus = req.Tus
d.settings.Shell = req.Shell d.settings.Shell = req.Shell
d.settings.Commands = req.Commands d.settings.Commands = req.Commands
d.settings.HideLoginButton = req.HideLoginButton
err = d.store.Settings.Save(d.settings) err = d.store.Settings.Save(d.settings)
return errToStatus(err), err return errToStatus(err), err

View File

@@ -46,6 +46,7 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
"ResizePreview": d.server.ResizePreview, "ResizePreview": d.server.ResizePreview,
"EnableExec": d.server.EnableExec, "EnableExec": d.server.EnableExec,
"TusSettings": d.settings.Tus, "TusSettings": d.settings.Tus,
"HideLoginButton": d.settings.HideLoginButton,
} }
if d.settings.Branding.Files != "" { if d.settings.Branding.Files != "" {

View File

@@ -48,8 +48,8 @@ func parseSearch(value string) *searchOptions {
} }
// removes the options from the value // removes the options from the value
value = strings.Replace(value, "case:insensitive", "", -1) value = strings.ReplaceAll(value, "case:insensitive", "")
value = strings.Replace(value, "case:sensitive", "", -1) value = strings.ReplaceAll(value, "case:sensitive", "")
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
types := typeRegexp.FindAllStringSubmatch(value, -1) types := typeRegexp.FindAllStringSubmatch(value, -1)

View File

@@ -42,7 +42,7 @@ func (s *Settings) MakeUserDir(username, userScope, serverRoot string) (string,
func cleanUsername(s string) string { func cleanUsername(s string) string {
// Remove any trailing space to avoid ending on - // Remove any trailing space to avoid ending on -
s = strings.Trim(s, " ") s = strings.Trim(s, " ")
s = strings.Replace(s, "..", "", -1) s = strings.ReplaceAll(s, "..", "")
// Replace all characters which not in the list `0-9A-Za-z@_\-.` with a dash // Replace all characters which not in the list `0-9A-Za-z@_\-.` with a dash
s = invalidFilenameChars.ReplaceAllString(s, "-") s = invalidFilenameChars.ReplaceAllString(s, "-")

View File

@@ -22,6 +22,7 @@ type AuthMethod string
type Settings struct { type Settings struct {
Key []byte `json:"key"` Key []byte `json:"key"`
Signup bool `json:"signup"` Signup bool `json:"signup"`
HideLoginButton bool `json:"hideLoginButton"`
CreateUserDir bool `json:"createUserDir"` CreateUserDir bool `json:"createUserDir"`
UserHomeBasePath string `json:"userHomeBasePath"` UserHomeBasePath string `json:"userHomeBasePath"`
Defaults UserDefaults `json:"defaults"` Defaults UserDefaults `json:"defaults"`
@@ -34,6 +35,7 @@ type Settings struct {
MinimumPasswordLength uint `json:"minimumPasswordLength"` MinimumPasswordLength uint `json:"minimumPasswordLength"`
FileMode fs.FileMode `json:"fileMode"` FileMode fs.FileMode `json:"fileMode"`
DirMode fs.FileMode `json:"dirMode"` DirMode fs.FileMode `json:"dirMode"`
HideDotfiles bool `json:"hideDotfiles"`
} }
// GetRules implements rules.Provider. // GetRules implements rules.Provider.

View File

@@ -1,27 +0,0 @@
include common.mk
# tools
TOOLS_DIR := $(BASE_PATH)/tools
TOOLS_GO_DEPS := $(TOOLS_DIR)/go.mod $(TOOLS_DIR)/go.sum
TOOLS_BIN := $(TOOLS_DIR)/bin
$(eval $(shell mkdir -p $(TOOLS_BIN)))
PATH := $(TOOLS_BIN):$(PATH)
export PATH
.PHONY: clean-tools
clean-tools:
$Q rm -rf $(TOOLS_BIN)
goimports=$(TOOLS_BIN)/goimports
$(goimports): $(TOOLS_GO_DEPS)
$Q cd $(TOOLS_DIR) && $(go) build -o $@ golang.org/x/tools/cmd/goimports
golangci-lint=$(TOOLS_BIN)/golangci-lint
$(golangci-lint): $(TOOLS_GO_DEPS)
$Q cd $(TOOLS_DIR) && $(go) build -o $@ github.com/golangci/golangci-lint/v2/cmd/golangci-lint
# js tools
TOOLS_JS_DEPS=$(TOOLS_DIR)/node_modules/.modified
$(TOOLS_JS_DEPS): $(TOOLS_DIR)/package.json $(TOOLS_DIR)/yarn.lock
$Q cd ${TOOLS_DIR} && yarn install
$Q touch -am $@

View File

@@ -1,216 +0,0 @@
module github.com/filebrowser/filebrowser/v2/tools
go 1.25
require (
github.com/golangci/golangci-lint/v2 v2.6.1
golang.org/x/tools v0.38.0
)
require (
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
4d63.com/gochecknoglobals v0.2.2 // indirect
codeberg.org/chavacava/garif v0.2.0 // indirect
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
dev.gaijin.team/go/golib v0.6.0 // indirect
github.com/4meepo/tagalign v1.4.3 // indirect
github.com/Abirdcfly/dupword v0.1.7 // indirect
github.com/AdminBenni/iota-mixing v1.0.0 // indirect
github.com/AlwxSin/noinlineerr v1.0.5 // indirect
github.com/Antonboom/errname v1.1.1 // indirect
github.com/Antonboom/nilnil v1.1.1 // indirect
github.com/Antonboom/testifylint v1.6.4 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/Djarvur/go-err113 v0.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/MirrexOne/unqueryvet v1.2.1 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
github.com/alexkohler/prealloc v1.0.0 // indirect
github.com/alfatraining/structtag v1.0.0 // indirect
github.com/alingse/asasalint v0.0.11 // indirect
github.com/alingse/nilnesserr v0.2.0 // indirect
github.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect
github.com/ashanbrown/makezero/v2 v2.1.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bkielbasa/cyclop v1.2.3 // indirect
github.com/blizzy78/varnamelen v0.8.0 // indirect
github.com/bombsimon/wsl/v4 v4.7.0 // indirect
github.com/bombsimon/wsl/v5 v5.3.0 // indirect
github.com/breml/bidichk v0.3.3 // indirect
github.com/breml/errchkjson v0.4.1 // indirect
github.com/butuzov/ireturn v0.4.0 // indirect
github.com/butuzov/mirror v1.3.0 // indirect
github.com/catenacyber/perfsprint v0.10.0 // indirect
github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charithe/durationcheck v0.0.11 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/ckaznocha/intrange v0.3.1 // indirect
github.com/curioswitch/go-reassign v0.3.0 // indirect
github.com/daixiang0/gci v0.13.7 // indirect
github.com/dave/dst v0.27.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denis-tingaikin/go-header v0.5.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/ettle/strcase v0.2.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/firefart/nonamedreturns v1.0.6 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/ghostiam/protogetter v0.3.17 // indirect
github.com/go-critic/go-critic v0.14.2 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.2.0 // indirect
github.com/go-toolsmith/astfmt v1.1.0 // indirect
github.com/go-toolsmith/astp v1.1.0 // indirect
github.com/go-toolsmith/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/godoc-lint/godoc-lint v0.10.1 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golangci/asciicheck v0.5.0 // indirect
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
github.com/golangci/go-printf-func-name v0.1.1 // indirect
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect
github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 // indirect
github.com/golangci/misspell v0.7.0 // indirect
github.com/golangci/plugin-module-register v0.1.2 // indirect
github.com/golangci/revgrep v0.8.0 // indirect
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/gordonklaus/ineffassign v0.2.0 // indirect
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
github.com/gostaticanalysis/comment v1.5.0 // indirect
github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
github.com/gostaticanalysis/nilerr v0.1.2 // indirect
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jgautheron/goconst v1.8.2 // indirect
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
github.com/jjti/go-spancheck v0.6.5 // indirect
github.com/julz/importas v0.2.0 // indirect
github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect
github.com/kisielk/errcheck v1.9.0 // indirect
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
github.com/kulti/thelper v0.7.1 // indirect
github.com/kunwardeep/paralleltest v1.0.15 // indirect
github.com/lasiar/canonicalheader v1.1.2 // indirect
github.com/ldez/exptostd v0.4.5 // indirect
github.com/ldez/gomoddirectives v0.7.1 // indirect
github.com/ldez/grignotin v0.10.1 // indirect
github.com/ldez/tagliatelle v0.7.2 // indirect
github.com/ldez/usetesting v0.5.0 // indirect
github.com/leonklingele/grouper v1.1.2 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/macabu/inamedparam v0.2.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect
github.com/manuelarte/funcorder v0.5.0 // indirect
github.com/maratori/testableexamples v1.0.0 // indirect
github.com/maratori/testpackage v1.1.1 // indirect
github.com/matoous/godox v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mgechev/revive v1.12.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moricho/tparallel v0.3.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/nakabonne/nestif v0.3.1 // indirect
github.com/nishanths/exhaustive v0.12.0 // indirect
github.com/nishanths/predeclared v0.2.2 // indirect
github.com/nunnatsa/ginkgolinter v0.21.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polyfloyd/go-errorlint v1.8.0 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/quasilyte/go-ruleguard v0.4.5 // indirect
github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
github.com/raeperd/recvcheck v0.2.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/ryancurrah/gomodguard v1.4.1 // indirect
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
github.com/securego/gosec/v2 v2.22.10 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sonatard/noctx v0.4.0 // indirect
github.com/sourcegraph/go-diff v0.7.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.10.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.12.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/tetafro/godot v1.5.4 // indirect
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect
github.com/timonwong/loggercheck v0.11.0 // indirect
github.com/tomarrell/wrapcheck/v2 v2.11.0 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
github.com/ultraware/funlen v0.2.0 // indirect
github.com/ultraware/whitespace v0.2.0 // indirect
github.com/uudashr/gocognit v1.2.0 // indirect
github.com/uudashr/iface v1.4.1 // indirect
github.com/xen0n/gosmopolitan v1.3.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yagipy/maintidx v1.0.0 // indirect
github.com/yeya24/promlinter v0.3.0 // indirect
github.com/ykadowak/zerologlint v0.1.5 // indirect
gitlab.com/bosi/decorder v0.4.2 // indirect
go-simpler.org/musttag v0.14.0 // indirect
go-simpler.org/sloglint v0.11.1 // indirect
go.augendre.info/arangolint v0.3.1 // indirect
go.augendre.info/fatcontext v0.9.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect
golang.org/x/text v0.30.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
honnef.co/go/tools v0.6.1 // indirect
mvdan.cc/gofumpt v0.9.2 // indirect
mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +0,0 @@
//go:build tools
// +build tools
package tools
// Manage tool dependencies via go.mod.
//
// https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module
// https://github.com/golang/go/issues/25922
//
// nolint
import (
_ "github.com/golangci/golangci-lint/v2/cmd/golangci-lint"
_ "golang.org/x/tools/cmd/goimports"
)

View File

@@ -2,6 +2,12 @@
Most of the configuration can be understood through the command line interface documentation. To access it, you need to install File Browser and run `filebrowser --help`. In this page, we cover some specific, more complex, topics. Most of the configuration can be understood through the command line interface documentation. To access it, you need to install File Browser and run `filebrowser --help`. In this page, we cover some specific, more complex, topics.
## Flags as Environment Variables
In some situations, it is easier to use environment variables instead of flags. For example, if you're using our provided Docker image, it's easier for you to use environment variables to customize the settings instead of flags.
All flags should be available as environment variables prefixed with `FB_`. For example, the flag `--disable-thumbnails` is available as `FB_DISABLE_THUMBNAILS`.
## Custom Branding ## Custom Branding
You can customize File Browser to use your own branding. This includes the following: You can customize File Browser to use your own branding. This includes the following:

View File

@@ -10,7 +10,7 @@
> [!WARNING] > [!WARNING]
> >
> This project is currently on **maintenance-only** mode. For more information, read the information on [GitHub](https://github.com/filebrowser/filebrowser#project-status). > This project is on **maintenance-only** mode. For more information, read the information on [GitHub](https://github.com/filebrowser/filebrowser#project-status).
![Preview](static/example.gif) ![Preview](static/example.gif)