Compare commits

..

33 Commits

Author SHA1 Message Date
Henrique Dias
fb8d41eb9a chore(release): 2.47.0 2025-11-16 14:28:15 +01:00
Henrique Dias
0fadaccaa2 chore(docs): update CLI documentation 2025-11-16 14:28:03 +01:00
Henrique Dias
e24e1f1aba feat: add TUS settings to the command line (#5556) 2025-11-16 14:13:58 +01:00
Henrique Dias
5de4099cba fix: exit 0 when gracefully shutting down (#5555) 2025-11-16 14:13:21 +01:00
Henrique Dias
d01493106d docs: improved config 2025-11-16 10:31:08 +01:00
Henrique Dias
2d9689dd6a docs: add CLI usage and integrate generation in release 2025-11-16 10:14:59 +01:00
Henrique Dias
c4c1cea230 docs: remove partially incorrect env variables info 2025-11-16 09:01:54 +01:00
Henrique Dias
ceb5e723f3 feat: remove importer of v1 config (#5550) 2025-11-16 07:52:53 +01:00
renovate[bot]
ebc7d2303d chore(deps): update dependency vue-tsc to v3.1.4 (#5551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-15 20:14:10 +01:00
Henrique Dias
23c4e4565b chore: remove 'nolint' comments 2025-11-15 09:01:21 +01:00
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
156 changed files with 3156 additions and 3635 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

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,41 @@
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.47.0](https://github.com/filebrowser/filebrowser/compare/v2.46.1...v2.47.0) (2025-11-16)
### Features
* add TUS settings to the command line ([#5556](https://github.com/filebrowser/filebrowser/issues/5556)) ([e24e1f1](https://github.com/filebrowser/filebrowser/commit/e24e1f1abae9e80add620c4ad65660ca1b575a49))
* remove importer of v1 config ([#5550](https://github.com/filebrowser/filebrowser/issues/5550)) ([ceb5e72](https://github.com/filebrowser/filebrowser/commit/ceb5e723f3ee2c966bb561a804015246450280ca))
### Bug Fixes
* exit 0 when gracefully shutting down ([#5555](https://github.com/filebrowser/filebrowser/issues/5555)) ([5de4099](https://github.com/filebrowser/filebrowser/commit/5de4099cba2cf012d4a213c8eb29c412fc72c151))
## [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

79
Taskfile.yml Normal file
View File

@@ -0,0 +1,79 @@
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: docs:cli:generate
- git add www/docs/cli
- task: release:dry-run
- task: release:make
docs:cli:generate:
cmds:
- rm -rf www/docs/cli
- mkdir -p www/docs/cli
- go run . docs
generates:
- www/docs/cli
docs:docker:generate:
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:docker:generate
- docker run --rm {{.SITE_DOCKER_FLAGS}} filebrowser.site build -d "public"
docs:serve:
desc: Serve documentation
cmds:
- task: docs:docker:generate
- docker run --rm -it -p 8000:8000 {{.SITE_DOCKER_FLAGS}} filebrowser.site

View File

@@ -103,7 +103,7 @@ func (a *HookAuth) RunCommand() (string, error) {
command[i] = os.Expand(arg, envMapping) command[i] = os.Expand(arg, envMapping)
} }
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec cmd := exec.Command(command[0], command[1:]...)
cmd.Env = append(os.Environ(), fmt.Sprintf("USERNAME=%s", a.Cred.Username)) cmd.Env = append(os.Environ(), fmt.Sprintf("USERNAME=%s", a.Cred.Username))
cmd.Env = append(cmd.Env, fmt.Sprintf("PASSWORD=%s", a.Cred.Password)) cmd.Env = append(cmd.Env, fmt.Sprintf("PASSWORD=%s", a.Cred.Password))
out, err := cmd.Output() out, err := cmd.Output()

View File

@@ -40,7 +40,7 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, s
// If ReCaptcha is enabled, check the code. // If ReCaptcha is enabled, check the code.
if a.ReCaptcha != nil && a.ReCaptcha.Secret != "" { if a.ReCaptcha != nil && a.ReCaptcha.Secret != "" {
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet ok, err := a.ReCaptcha.Ok(cred.ReCaptcha)
if err != nil { if err != nil {
return nil, err return nil, err

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")
@@ -51,8 +52,11 @@ func addConfigFlags(flags *pflag.FlagSet) {
flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph") flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph")
// NB: these are string so they can be presented as octal in the help text // NB: these are string so they can be presented as octal in the help text
// as that's the conventional representation for modes in Unix. // as that's the conventional representation for modes in Unix.
flags.String("file-mode", fmt.Sprintf("%O", settings.DefaultFileMode), "Mode bits that new files are created with") flags.String("file-mode", fmt.Sprintf("%O", settings.DefaultFileMode), "mode bits that new files are created with")
flags.String("dir-mode", fmt.Sprintf("%O", settings.DefaultDirMode), "Mode bits that new directories are created with") flags.String("dir-mode", fmt.Sprintf("%O", settings.DefaultDirMode), "mode bits that new directories are created with")
flags.Uint64("tus.chunkSize", settings.DefaultTusChunkSize, "the tus chunk size")
flags.Uint16("tus.retryCount", settings.DefaultTusRetryCount, "the tus retry count")
} }
func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) { func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) {
@@ -192,9 +196,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)
@@ -213,8 +218,12 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert) fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert)
fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey) fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey)
fmt.Fprintf(w, "\tExec Enabled:\t%t\n", ser.EnableExec) fmt.Fprintf(w, "\tExec Enabled:\t%t\n", ser.EnableExec)
fmt.Fprintln(w, "\nTUS:")
fmt.Fprintf(w, "\tChunk size:\t%d\n", set.Tus.ChunkSize)
fmt.Fprintf(w, "\tRetry count:\t%d\n", set.Tus.RetryCount)
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
@@ -81,9 +86,20 @@ override the options.`,
return err return err
} }
tusChunkSize, err := flags.GetUint64("tus.chunkSize")
if err != nil {
return err
}
tusRetryCount, err := flags.GetUint16("tus.retryCount")
if err != nil {
return err
}
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),
@@ -96,6 +112,10 @@ override the options.`,
Theme: brandingTheme, Theme: brandingTheme,
Files: brandingFiles, Files: brandingFiles,
}, },
Tus: settings.Tus{
ChunkSize: tusChunkSize,
RetryCount: tusRetryCount,
},
} }
s.FileMode, err = getMode(flags, "file-mode") s.FileMode, err = getMode(flags, "file-mode")

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":
@@ -78,6 +80,10 @@ you want to change. Other options will remain unchanged.`,
set.FileMode, err = getMode(flags, flag.Name) set.FileMode, err = getMode(flags, flag.Name)
case "dir-mode": case "dir-mode":
set.DirMode, err = getMode(flags, flag.Name) set.DirMode, err = getMode(flags, flag.Name)
case "tus.chunkSize":
set.Tus.ChunkSize, err = flags.GetUint64(flag.Name)
case "tus.retryCount":
set.Tus.RetryCount, err = flags.GetUint16(flag.Name)
} }
}) })

View File

@@ -3,36 +3,18 @@ package cmd
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path"
"sort" "regexp"
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/cobra/doc"
) )
func init() { func init() {
rootCmd.AddCommand(docsCmd) rootCmd.AddCommand(docsCmd)
docsCmd.Flags().StringP("path", "p", "./docs", "path to save the docs") docsCmd.Flags().String("out", "www/docs/cli", "directory to write the docs to")
}
func printToc(names []string) {
for i, name := range names {
name = strings.TrimSuffix(name, filepath.Ext(name))
name = strings.Replace(name, "-", " ", -1)
names[i] = name
}
sort.Strings(names)
toc := ""
for _, name := range names {
toc += "* [" + name + "](cli/" + strings.Replace(name, " ", "-", -1) + ".md)\n"
}
fmt.Println(toc)
} }
var docsCmd = &cobra.Command{ var docsCmd = &cobra.Command{
@@ -40,115 +22,61 @@ var docsCmd = &cobra.Command{
Hidden: true, Hidden: true,
Args: cobra.NoArgs, Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
dir, err := getString(cmd.Flags(), "path") outputDir, err := cmd.Flags().GetString("out")
if err != nil { if err != nil {
return err return err
} }
err = generateDocs(rootCmd, dir) tempDir, err := os.MkdirTemp(os.TempDir(), "filebrowser-docs-")
if err != nil { if err != nil {
return err return err
} }
names := []string{} defer os.RemoveAll(tempDir)
err = filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error { rootCmd.Root().DisableAutoGenTag = true
if err != nil || info.IsDir() {
return err
}
if !strings.HasPrefix(info.Name(), "filebrowser") { err = doc.GenMarkdownTreeCustom(cmd.Root(), tempDir, func(f string) string {
return nil return ""
} }, func(s string) string {
return s
names = append(names, info.Name())
return nil
}) })
if err != nil { if err != nil {
return err return err
} }
printToc(names) entries, err := os.ReadDir(tempDir)
if err != nil {
return err
}
headerRegex := regexp.MustCompile(`(?m)^(##)(.*)$`)
linkRegex := regexp.MustCompile(`\(filebrowser(.*)\.md\)`)
fmt.Println("Generated Documents:")
for _, entry := range entries {
srcPath := path.Join(tempDir, entry.Name())
dstPath := path.Join(outputDir, strings.ReplaceAll(entry.Name(), "_", "-"))
data, err := os.ReadFile(srcPath)
if err != nil {
return err
}
data = headerRegex.ReplaceAll(data, []byte("#$2"))
data = linkRegex.ReplaceAllFunc(data, func(b []byte) []byte {
return bytes.ReplaceAll(b, []byte("_"), []byte("-"))
})
data = bytes.ReplaceAll(data, []byte("## SEE ALSO"), []byte("## See Also"))
err = os.WriteFile(dstPath, data, 0666)
if err != nil {
return err
}
fmt.Println("- " + dstPath)
}
return nil return nil
}, },
} }
func generateDocs(cmd *cobra.Command, dir string) error {
for _, c := range cmd.Commands() {
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
continue
}
err := generateDocs(c, dir)
if err != nil {
return err
}
}
basename := strings.Replace(cmd.CommandPath(), " ", "-", -1) + ".md"
filename := filepath.Join(dir, basename)
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
return generateMarkdown(cmd, f)
}
func generateMarkdown(cmd *cobra.Command, w io.Writer) error {
cmd.InitDefaultHelpCmd()
cmd.InitDefaultHelpFlag()
buf := new(bytes.Buffer)
name := cmd.CommandPath()
short := cmd.Short
long := cmd.Long
if long == "" {
long = short
}
buf.WriteString("---\ndescription: " + short + "\n---\n\n")
buf.WriteString("# " + name + "\n\n")
buf.WriteString("## Synopsis\n\n")
buf.WriteString(long + "\n\n")
if cmd.Runnable() {
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine())
}
if cmd.Example != "" {
buf.WriteString("## Examples\n\n")
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example)
}
printOptions(buf, cmd)
_, err := buf.WriteTo(w)
return err
}
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {
_, _ = buf.WriteString("| Name | Shorthand | Usage |\n")
_, _ = buf.WriteString("|------|-----------|-------|\n")
fs.VisitAll(func(f *pflag.Flag) {
_, _ = buf.WriteString("|" + f.Name + "|" + f.Shorthand + "|" + f.Usage + "|\n")
})
}
func printOptions(buf *bytes.Buffer, cmd *cobra.Command) {
flags := cmd.NonInheritedFlags()
flags.SetOutput(buf)
if flags.HasAvailableFlags() {
buf.WriteString("## Options\n\n")
generateFlagsTable(flags, buf)
buf.WriteString("\n")
}
parentFlags := cmd.InheritedFlags()
parentFlags.SetOutput(buf)
if parentFlags.HasAvailableFlags() {
buf.WriteString("### Inherited\n\n")
generateFlagsTable(parentFlags, buf)
buf.WriteString("\n")
}
}

View File

@@ -26,7 +26,6 @@ import (
"github.com/filebrowser/filebrowser/v2/auth" "github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/diskcache" "github.com/filebrowser/filebrowser/v2/diskcache"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/frontend" "github.com/filebrowser/filebrowser/v2/frontend"
fbhttp "github.com/filebrowser/filebrowser/v2/http" fbhttp "github.com/filebrowser/filebrowser/v2/http"
"github.com/filebrowser/filebrowser/v2/img" "github.com/filebrowser/filebrowser/v2/img"
@@ -70,7 +69,7 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.StringP("baseurl", "b", "", "base url") flags.StringP("baseurl", "b", "", "base url")
flags.String("cache-dir", "", "file cache directory (disabled if empty)") flags.String("cache-dir", "", "file cache directory (disabled if empty)")
flags.String("token-expiration-time", "2h", "user session timeout") flags.String("token-expiration-time", "2h", "user session timeout")
flags.Int("img-processors", 4, "image processors count") //nolint:mnd flags.Int("img-processors", 4, "image processors count")
flags.Bool("disable-thumbnails", false, "disable image thumbnails") flags.Bool("disable-thumbnails", false, "disable image thumbnails")
flags.Bool("disable-preview-resize", false, "disable resize of image previews") flags.Bool("disable-preview-resize", false, "disable resize of image previews")
flags.Bool("disable-exec", true, "disables Command Runner feature") flags.Bool("disable-exec", true, "disables Command Runner feature")
@@ -141,7 +140,7 @@ user created with the credentials from options "username" and "password".`,
return err return err
} }
if cacheDir != "" { if cacheDir != "" {
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet if err := os.MkdirAll(cacheDir, 0700); err != nil {
return fmt.Errorf("can't make directory %s: %w", cacheDir, err) return fmt.Errorf("can't make directory %s: %w", cacheDir, err)
} }
fileCache = diskcache.New(afero.NewOsFs(), cacheDir) fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
@@ -169,7 +168,7 @@ user created with the credentials from options "username" and "password".`,
if err != nil { if err != nil {
return err return err
} }
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet socketPerm, err := cmd.Flags().GetUint32("socket-perm")
if err != nil { if err != nil {
return err return err
} }
@@ -178,7 +177,7 @@ user created with the credentials from options "username" and "password".`,
return err return err
} }
case server.TLSKey != "" && server.TLSCert != "": case server.TLSKey != "" && server.TLSCert != "":
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey)
if err != nil { if err != nil {
return err return err
} }
@@ -233,7 +232,7 @@ user created with the credentials from options "username" and "password".`,
sig := <-sigc sig := <-sigc
log.Println("Got signal:", sig) log.Println("Got signal:", sig)
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) //nolint:mnd shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownRelease() defer shutdownRelease()
if err := srv.Shutdown(shutdownCtx); err != nil { if err := srv.Shutdown(shutdownCtx); err != nil {
@@ -241,22 +240,10 @@ user created with the credentials from options "username" and "password".`,
} }
log.Println("Graceful shutdown complete.") log.Println("Graceful shutdown complete.")
switch sig { return nil
case syscall.SIGHUP:
d.err = fbErrors.ErrSighup
case syscall.SIGINT:
d.err = fbErrors.ErrSigint
case syscall.SIGQUIT:
d.err = fbErrors.ErrSigquit
case syscall.SIGTERM:
d.err = fbErrors.ErrSigTerm
}
return d.err
}, pythonConfig{allowNoDB: true}), }, pythonConfig{allowNoDB: true}),
} }
//nolint:gocyclo
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server, error) { func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server, error) {
server, err := st.Settings.GetServer() server, err := st.Settings.GetServer()
if err != nil { if err != nil {
@@ -420,6 +407,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 +521,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

@@ -1,40 +0,0 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/storage/bolt/importer"
)
func init() {
rootCmd.AddCommand(upgradeCmd)
upgradeCmd.Flags().String("old.database", "", "")
upgradeCmd.Flags().String("old.config", "", "")
_ = upgradeCmd.MarkFlagRequired("old.database")
}
var upgradeCmd = &cobra.Command{
Use: "upgrade",
Short: "Upgrades an old configuration",
Long: `Upgrades an old configuration. This command DOES NOT
import share links because they are incompatible with
this version.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
flags := cmd.Flags()
oldDB, err := getString(flags, "old.database")
if err != nil {
return err
}
oldConf, err := getString(flags, "old.config")
if err != nil {
return err
}
db, err := getString(flags, "database")
if err != nil {
return err
}
return importer.Import(oldDB, oldConf, db)
},
}

View File

@@ -94,7 +94,6 @@ func getViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
return viewMode, nil return viewMode, nil
} }
//nolint:gocyclo
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) error { func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) error {
var visitErr error var visitErr error
visit := func(flag *pflag.Flag) { visit := func(flag *pflag.Flag) {
@@ -135,6 +134,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

@@ -87,7 +87,7 @@ list or set it to 0.`,
// with the new username. If there is, print an error and cancel the // with the new username. If there is, print an error and cancel the
// operation // operation
if user.Username != onDB.Username { if user.Username != onDB.Username {
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet if conflictuous, err := d.store.Users.Get("", user.Username); err == nil {
return usernameConflictError(user.Username, conflictuous.ID, user.ID) return usernameConflictError(user.Username, conflictuous.ID, user.ID)
} }
} }

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 {
@@ -76,7 +66,6 @@ type pythonConfig struct {
type pythonData struct { type pythonData struct {
hadDB bool hadDB bool
store *storage.Storage store *storage.Storage
err error
} }
func dbExists(path string) (bool, error) { func dbExists(path string) (bool, error) {
@@ -89,7 +78,7 @@ func dbExists(path string) (bool, error) {
d := filepath.Dir(path) d := filepath.Dir(path)
_, err = os.Stat(d) _, err = os.Stat(d)
if os.IsNotExist(err) { if os.IsNotExist(err) {
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet if err := os.MkdirAll(d, 0700); err != nil {
return false, err return false, err
} }
return false, nil return false, nil

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

@@ -2,7 +2,7 @@ package diskcache
import ( import (
"context" "context"
"crypto/sha1" //nolint:gosec "crypto/sha1"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
@@ -103,7 +103,7 @@ func (f *FileCache) getScopedLocks(key string) (lock sync.Locker) {
} }
func (f *FileCache) getFileName(key string) string { func (f *FileCache) getFileName(key string) string {
hasher := sha1.New() //nolint:gosec hasher := sha1.New()
_, _ = hasher.Write([]byte(key)) _, _ = hasher.Write([]byte(key))
hash := hex.EncodeToString(hasher.Sum(nil)) hash := hex.EncodeToString(hasher.Sum(nil))
return fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash) return fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash)

View File

@@ -40,7 +40,7 @@ func TestFileCache(t *testing.T) {
require.False(t, exists) require.False(t, exists)
} }
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:revive func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) {
t.Helper() t.Helper()
// check actual file content // check actual file content
b, err := afero.ReadFile(fs, fileFullPath) b, err := afero.ReadFile(fs, fileFullPath)

View File

@@ -3,15 +3,6 @@ package errors
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"syscall"
)
const (
ExitCodeSigTerm = 128 + int(syscall.SIGTERM)
ExitCodeSighup = 128 + int(syscall.SIGHUP)
ExitCodeSigint = 128 + int(syscall.SIGINT)
ExitCodeSigquit = 128 + int(syscall.SIGQUIT)
) )
var ( var (
@@ -31,10 +22,6 @@ var (
ErrInvalidRequestParams = errors.New("invalid request params") ErrInvalidRequestParams = errors.New("invalid request params")
ErrSourceIsParent = errors.New("source is parent") ErrSourceIsParent = errors.New("source is parent")
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted") ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
ErrSigTerm = errors.New("exit on signal: sigterm")
ErrSighup = errors.New("exit on signal: sighup")
ErrSigint = errors.New("exit on signal: sigint")
ErrSigquit = errors.New("exit on signal: sigquit")
) )
type ErrShortPassword struct { type ErrShortPassword struct {
@@ -44,44 +31,3 @@ type ErrShortPassword struct {
func (e ErrShortPassword) Error() string { func (e ErrShortPassword) Error() string {
return fmt.Sprintf("password is too short, minimum length is %d", e.MinimumLength) return fmt.Sprintf("password is too short, minimum length is %d", e.MinimumLength)
} }
// GetExitCode returns the exit code for a given error.
func GetExitCode(err error) int {
if err == nil {
return 0
}
exitCodeMap := map[error]int{
ErrSigTerm: ExitCodeSigTerm,
ErrSighup: ExitCodeSighup,
ErrSigint: ExitCodeSigint,
ErrSigquit: ExitCodeSigquit,
}
for e, code := range exitCodeMap {
if errors.Is(err, e) {
return code
}
}
if exitErr, ok := err.(interface{ ExitCode() int }); ok {
return exitErr.ExitCode()
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
return 1
}
var syscallErr *os.SyscallError
if errors.As(err, &syscallErr) {
return 1
}
var errno syscall.Errno
if errors.As(err, &errno) {
return 1
}
return 1
}

View File

@@ -1,8 +1,8 @@
package files package files
import ( import (
"crypto/md5" //nolint:gosec "crypto/md5"
"crypto/sha1" //nolint:gosec "crypto/sha1"
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"encoding/hex" "encoding/hex"
@@ -90,7 +90,7 @@ func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
if opts.Expand { if opts.Expand {
if file.IsDir { if file.IsDir {
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil {
return nil, err return nil, err
} }
return file, nil return file, nil
@@ -183,7 +183,6 @@ func (i *FileInfo) Checksum(algo string) error {
var h hash.Hash var h hash.Hash
//nolint:gosec
switch algo { switch algo {
case "md5": case "md5":
h = md5.New() h = md5.New()

View File

@@ -600,7 +600,6 @@ var types = map[string]string{
".epub": "application/epub+zip", ".epub": "application/epub+zip",
} }
//nolint:gochecknoinits
func init() { func init() {
for ext, typ := range types { for ext, typ := range types {
// skip errors // skip errors

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

@@ -161,7 +161,7 @@ importers:
version: 2.3.1(rollup@4.52.5) version: 2.3.1(rollup@4.52.5)
vue-tsc: vue-tsc:
specifier: ^3.1.3 specifier: ^3.1.3
version: 3.1.3(typescript@5.9.3) version: 3.1.4(typescript@5.9.3)
packages: packages:
@@ -1303,8 +1303,8 @@ packages:
typescript: typescript:
optional: true optional: true
'@vue/language-core@3.1.3': '@vue/language-core@3.1.4':
resolution: {integrity: sha512-KpR1F/eGAG9D1RZ0/T6zWJs6dh/pRLfY5WupecyYKJ1fjVmDMgTPw9wXmKv2rBjo4zCJiOSiyB8BDP1OUwpMEA==} resolution: {integrity: sha512-n/58wm8SkmoxMWkUNUH/PwoovWe4hmdyPJU2ouldr3EPi1MLoS7iDN46je8CsP95SnVBs2axInzRglPNKvqMcg==}
peerDependencies: peerDependencies:
typescript: '*' typescript: '*'
peerDependenciesMeta: peerDependenciesMeta:
@@ -2484,8 +2484,8 @@ packages:
peerDependencies: peerDependencies:
vue: ^3.0.2 vue: ^3.0.2
vue-tsc@3.1.3: vue-tsc@3.1.4:
resolution: {integrity: sha512-StMNfZHwPIXQgY3KxPKM0Jsoc8b46mDV3Fn2UlHCBIwRJApjqrSwqeMYgWf0zpN+g857y74pv7GWuBm+UqQe1w==} resolution: {integrity: sha512-GsRJxttj4WkmXW/zDwYPGMJAN3np/4jTzoDFQTpTsI5Vg/JKMWamBwamlmLihgSVHO66y9P7GX+uoliYxeI4Hw==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
typescript: '>=5.0.0' typescript: '>=5.0.0'
@@ -3828,7 +3828,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@vue/language-core@3.1.3(typescript@5.9.3)': '@vue/language-core@3.1.4(typescript@5.9.3)':
dependencies: dependencies:
'@volar/language-core': 2.4.23 '@volar/language-core': 2.4.23
'@vue/compiler-dom': 3.5.24 '@vue/compiler-dom': 3.5.24
@@ -4966,10 +4966,10 @@ snapshots:
dependencies: dependencies:
vue: 3.5.24(typescript@5.9.3) vue: 3.5.24(typescript@5.9.3)
vue-tsc@3.1.3(typescript@5.9.3): vue-tsc@3.1.4(typescript@5.9.3):
dependencies: dependencies:
'@volar/typescript': 2.4.23 '@volar/typescript': 2.4.23
'@vue/language-core': 3.1.3(typescript@5.9.3) '@vue/language-core': 3.1.4(typescript@5.9.3)
typescript: 5.9.3 typescript: 5.9.3
vue@3.5.24(typescript@5.9.3): vue@3.5.24(typescript@5.9.3):

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,54 +44,75 @@
</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,
const pct = computed(() => { },
const pct = (props.val / props.max) * 100; text: {
const pctFixed = pct.toFixed(2); type: String,
return Math.min(parseFloat(pctFixed), props.max); default: "",
}); },
"text-align": {
const size_px = computed(() => { type: String,
switch (props.size) { 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": case "tiny":
return 2; return 2;
case "small": case "small":
@@ -105,11 +129,10 @@ const size_px = computed(() => {
return 64; return 64;
} }
return isNumber(props.size) ? (props.size as number) : 32; return isNumber(this.size) ? this.size : 32;
}); },
text_padding() {
const text_padding = computed(() => { switch (this.size) {
switch (props.size) {
case "tiny": case "tiny":
case "small": case "small":
case "medium": case "medium":
@@ -117,14 +140,13 @@ const text_padding = computed(() => {
case "big": case "big":
case "huge": case "huge":
case "massive": case "massive":
return Math.min(Math.max(Math.ceil(size_px.value / 8), 3), 12); return Math.min(Math.max(Math.ceil(this.size_px / 8), 3), 12);
} }
return isNumber(props.spacing) ? props.spacing : 4; return isNumber(this.spacing) ? this.spacing : 4;
}); },
text_font_size() {
const text_font_size = computed(() => { switch (this.size) {
switch (props.size) {
case "tiny": case "tiny":
case "small": case "small":
case "medium": case "medium":
@@ -132,73 +154,72 @@ const text_font_size = computed(() => {
case "big": case "big":
case "huge": case "huge":
case "massive": case "massive":
return Math.min(Math.max(Math.ceil(size_px.value * 1.4), 11), 32); return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32);
} }
return isNumber(props.fontSize) ? props.fontSize : 13; return isNumber(this.fontSize) ? this.fontSize : 13;
}); },
progress_style() {
const progress_style = computed(() => { const style = {
const style: Record<string, string> = { background: this.bgColor,
background: props.bgColor,
}; };
if (props.textPosition == "middle" || props.textPosition == "inside") { if (this.textPosition == "middle" || this.textPosition == "inside") {
style["position"] = "relative"; style["position"] = "relative";
style["min-height"] = size_px.value + "px"; style["min-height"] = this.size_px + "px";
style["z-index"] = "-2"; style["z-index"] = "-2";
} }
if (props.barBorderRadius > 0) { if (this.barBorderRadius > 0) {
style["border-radius"] = props.barBorderRadius + "px"; style["border-radius"] = this.barBorderRadius + "px";
} }
return style; return style;
}); },
bar_style() {
const bar_style = computed(() => { const style = {
const style: Record<string, string> = { background: this.barColor,
background: props.barColor, width: this.pct + "%",
width: pct.value + "%", height: this.size_px + "px",
height: size_px.value + "px", transition: this.barTransition,
transition: props.barTransition,
}; };
if (props.barBorderRadius > 0) { if (this.barBorderRadius > 0) {
style["border-radius"] = props.barBorderRadius + "px"; style["border-radius"] = this.barBorderRadius + "px";
} }
if (props.textPosition == "middle" || props.textPosition == "inside") { if (this.textPosition == "middle" || this.textPosition == "inside") {
style["position"] = "absolute"; style["position"] = "absolute";
style["top"] = "0"; style["top"] = "0";
style["height"] = "100%"; style["height"] = "100%";
style["min-height"] = size_px.value + "px"; ((style["min-height"] = this.size_px + "px"),
style["z-index"] = "-1"; (style["z-index"] = "-1"));
} }
return style; return style;
}); },
text_style() {
const text_style = computed(() => { const style = {
const style: Record<string, string> = { color: this.textFgColor,
color: props.textFgColor, "font-size": this.text_font_size + "px",
"font-size": text_font_size.value + "px", "text-align": this.textAlign,
"text-align": props.textAlign,
}; };
if ( if (
props.textPosition == "top" || this.textPosition == "top" ||
props.textPosition == "middle" || this.textPosition == "middle" ||
props.textPosition == "inside" this.textPosition == "inside"
) )
style["padding-bottom"] = text_padding.value + "px"; style["padding-bottom"] = this.text_padding + "px";
if ( if (
props.textPosition == "bottom" || this.textPosition == "bottom" ||
props.textPosition == "middle" || this.textPosition == "middle" ||
props.textPosition == "inside" this.textPosition == "inside"
) )
style["padding-top"] = text_padding.value + "px"; style["padding-top"] = this.text_padding + "px";
return style; 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",
const fileStore = useFileStore(); computed: {
const layoutStore = useLayoutStore(); ...mapState(useLayoutStore, ["showShell"]),
...mapState(useFileStore, ["isFiles"]),
const { showShell } = storeToRefs(layoutStore); path: function () {
const { isFiles } = storeToRefs(fileStore); if (this.isFiles) {
const { toggleShell } = layoutStore; return this.$route.path;
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 = () => { return "";
},
},
data: () => ({
content: [],
history: [],
historyPos: 0,
canInput: true,
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") { if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)"; return "rgba(255, 255, 255, 0.4)";
} }
return "rgba(127, 127, 127, 0.4)"; return "rgba(127, 127, 127, 0.4)";
}; },
startDrag() {
const scroll = () => { document.addEventListener("pointermove", this.handleDrag);
if (scrollable.value) { this.shellDrag = true;
scrollable.value.scrollTop = scrollable.value.scrollHeight; },
} stopDrag() {
}; document.removeEventListener("pointermove", this.handleDrag);
this.shellDrag = false;
const focus = () => { },
input.value?.focus(); handleDrag: throttle(function (event) {
}; const top = window.innerHeight / this.fontsize - 4;
const userPos = (window.innerHeight - event.clientY) / this.fontsize;
const handleDrag = throttle((event: PointerEvent) => { const bottom =
const top = window.innerHeight / fontsize.value - 4; 2.25 +
const userPos = (window.innerHeight - event.clientY) / fontsize.value; document.querySelector(".shell__divider").offsetHeight / this.fontsize;
const divider = document.querySelector(".shell__divider") as HTMLElement;
const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value;
if (userPos <= top && userPos >= bottom) { if (userPos <= top && userPos >= bottom) {
shellHeight.value = parseFloat(userPos.toFixed(2)); this.shellHeight = userPos.toFixed(2);
} }
}, 32); }, 32),
resize: throttle(function () {
const top = window.innerHeight / this.fontsize - 4;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
const resize = throttle(() => { if (this.shellHeight > top) {
const top = window.innerHeight / fontsize.value - 4; this.shellHeight = top;
const divider = document.querySelector(".shell__divider") as HTMLElement; } else if (this.shellHeight < bottom) {
const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value; this.shellHeight = bottom;
if (shellHeight.value > top) {
shellHeight.value = top;
} else if (shellHeight.value < bottom) {
shellHeight.value = bottom;
} }
}, 32); }, 32),
scroll: function () {
const startDrag = () => { this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
document.addEventListener("pointermove", handleDrag as any); },
shellDrag.value = true; focus: function () {
}; this.$refs.input.focus();
},
const stopDrag = () => { historyUp() {
document.removeEventListener("pointermove", handleDrag as any); if (this.historyPos > 0) {
shellDrag.value = false; this.$refs.input.innerText = this.history[--this.historyPos];
}; this.focus();
const historyUp = () => {
if (historyPos.value > 0 && input.value) {
historyPos.value--;
input.value.innerText = history.value[historyPos.value];
focus();
} }
}; },
historyDown() {
const historyDown = () => { if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
if ( this.$refs.input.innerText = this.history[++this.historyPos];
historyPos.value >= 0 && this.focus();
historyPos.value < history.value.length - 1 &&
input.value
) {
historyPos.value++;
input.value.innerText = history.value[historyPos.value];
focus();
} else { } else {
historyPos.value = history.value.length; this.historyPos = this.history.length;
if (input.value) { this.$refs.input.innerText = "";
input.value.innerText = "";
} }
} },
}; submit: function (event) {
const cmd = event.target.innerText.trim();
const submit = (event: Event) => {
const target = event.target as HTMLElement;
const cmd = target.innerText.trim();
if (cmd === "") { if (cmd === "") {
return; return;
} }
if (cmd === "clear") { if (cmd === "clear") {
content.value = []; this.content = [];
target.innerHTML = ""; event.target.innerHTML = "";
return; return;
} }
if (cmd === "exit") { if (cmd === "exit") {
target.innerHTML = ""; event.target.innerHTML = "";
toggleShell(); this.toggleShell();
return; return;
} }
canInput.value = false; this.canInput = false;
target.innerHTML = ""; event.target.innerHTML = "";
const results = { const results = {
text: `${cmd}\n\n`, text: `${cmd}\n\n`,
}; };
history.value.push(cmd); this.history.push(cmd);
historyPos.value = history.value.length; this.historyPos = this.history.length;
content.value.push(results); this.content.push(results);
commands( commands(
path.value, this.path,
cmd, cmd,
(event: MessageEvent) => { (event) => {
results.text += `${event.data}\n`; results.text += `${event.data}\n`;
scroll(); this.scroll();
}, },
() => { () => {
results.text = results.text results.text = results.text
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now .replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
.trimEnd(); .trimEnd();
canInput.value = true; this.canInput = true;
input.value?.focus(); this.$refs.input.focus();
scroll(); 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(); },
components: {
const { user, isLoggedIn } = storeToRefs(authStore); ProgressBar,
const { isFiles } = storeToRefs(fileStore); },
const { currentPromptName } = storeToRefs(layoutStore); inject: ["$showError"],
const { closeHovers, showHover } = layoutStore; computed: {
...mapState(useAuthStore, ["user", "isLoggedIn"]),
const usage = reactive(USAGE_DEFAULT); ...mapState(useFileStore, ["isFiles", "reload"]),
const usageAbortController = ref(new AbortController()); ...mapState(useLayoutStore, ["currentPromptName"]),
active() {
const active = computed(() => { return this.currentPromptName === "sidebar";
return currentPromptName.value === "sidebar"; },
}); signup: () => signup,
hideLoginButton: () => hideLoginButton,
const canLogout = !noAuth && loginPage; version: () => version,
disableExternal: () => disableExternal,
const abortOngoingFetchUsage = () => { disableUsedPercentage: () => disableUsedPercentage,
usageAbortController.value.abort(); canLogout: () => !noAuth && loginPage,
}; },
methods: {
const fetchUsage = async () => { ...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
const path = route.path.endsWith("/") ? route.path : route.path + "/"; abortOngoingFetchUsage() {
this.usageAbortController.abort();
},
async fetchUsage() {
const path = this.$route.path.endsWith("/")
? this.$route.path
: this.$route.path + "/";
let usageStats = USAGE_DEFAULT; let usageStats = USAGE_DEFAULT;
if (disableUsedPercentage) { if (this.disableUsedPercentage) {
return Object.assign(usage, usageStats); return Object.assign(this.usage, usageStats);
} }
try { try {
abortOngoingFetchUsage(); this.abortOngoingFetchUsage();
usageAbortController.value = new AbortController(); this.usageAbortController = new AbortController();
const usageData = await api.usage(path, usageAbortController.value.signal); const usage = await api.usage(path, this.usageAbortController.signal);
usageStats = { usageStats = {
used: prettyBytes(usageData.used, { binary: true }), used: prettyBytes(usage.used, { binary: true }),
total: prettyBytes(usageData.total, { binary: true }), total: prettyBytes(usage.total, { binary: true }),
usedPercentage: Math.round((usageData.used / usageData.total) * 100), usedPercentage: Math.round((usage.used / usage.total) * 100),
}; };
} finally { } finally {
return Object.assign(usage, usageStats); return Object.assign(this.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 } toRoot() {
); this.$router.push({ path: "/files" });
this.closeHovers();
onUnmounted(() => { },
abortOngoingFetchUsage(); 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,77 +64,81 @@ 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 () {
const fileStore = useFileStore(); return {
const layoutStore = useLayoutStore(); current: window.location.pathname,
const authStore = useAuthStore(); dest: null,
};
const { req, selected } = storeToRefs(fileStore); },
const { user } = storeToRefs(authStore); inject: ["$showError"],
const { showHover, closeHovers } = layoutStore; computed: {
...mapState(useFileStore, ["req", "selected"]),
const fileList = ref<InstanceType<typeof FileList> | null>(null); ...mapState(useAuthStore, ["user"]),
const dest = ref<string | null>(null); ...mapWritableState(useFileStore, ["reload", "preselect"]),
},
const copy = async (event: Event) => { methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
copy: async function (event) {
event.preventDefault(); event.preventDefault();
const items: Array<{ from: string; to: string; name: string }> = []; const items = [];
// Create a new promise for each file. // Create a new promise for each file.
for (const item of selected.value) { for (const item of this.selected) {
items.push({ items.push({
from: req.value!.items[item].url, from: this.req.items[item].url,
to: dest.value! + encodeURIComponent(req.value!.items[item].name), to: this.dest + encodeURIComponent(this.req.items[item].name),
name: req.value!.items[item].name, name: this.req.items[item].name,
}); });
} }
const action = async (overwrite: boolean, rename: boolean) => { const action = async (overwrite, rename) => {
buttons.loading("copy"); buttons.loading("copy");
await api await api
.copy(items, overwrite, rename) .copy(items, overwrite, rename)
.then(() => { .then(() => {
buttons.success("copy"); buttons.success("copy");
fileStore.preselect = removePrefix(items[0].to); this.preselect = removePrefix(items[0].to);
if (this.$route.path === this.dest) {
this.reload = true;
if (route.path === dest.value) {
fileStore.reload = true;
return; return;
} }
router.push({ path: dest.value! }); this.$router.push({ path: this.dest });
}) })
.catch((e) => { .catch((e) => {
buttons.done("copy"); buttons.done("copy");
$showError?.(e); this.$showError(e);
}); });
}; };
if (route.path === dest.value) { if (this.$route.path === this.dest) {
closeHovers(); this.closeHovers();
action(false, true); action(false, true);
return; return;
} }
const dstItems = (await api.fetch(dest.value!)).items; const dstItems = (await api.fetch(this.dest)).items;
const conflict = upload.checkConflict(items as any, dstItems); const conflict = upload.checkConflict(items, dstItems);
let overwrite = false; let overwrite = false;
let rename = false; let rename = false;
if (conflict) { if (conflict) {
showHover({ this.showHover({
prompt: "replace-rename", prompt: "replace-rename",
confirm: (event: Event, option: string) => { confirm: (event, option) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
closeHovers(); this.closeHovers();
action(overwrite, rename); action(overwrite, rename);
}, },
}); });
@@ -145,5 +147,7 @@ const copy = async (event: Event) => {
} }
action(overwrite, rename); 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"],
const fileStore = useFileStore(); computed: {
const layoutStore = useLayoutStore(); ...mapState(useFileStore, [
"isListing",
const { isListing, selectedCount, req, selected } = storeToRefs(fileStore); "selectedCount",
const { currentPrompt } = storeToRefs(layoutStore); "req",
const { closeHovers } = layoutStore; "selected",
]),
const submit = async () => { ...mapState(useLayoutStore, ["currentPrompt"]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
submit: async function () {
buttons.loading("delete"); buttons.loading("delete");
try { try {
if (!isListing.value) { if (!this.isListing) {
await api.remove(route.path); await api.remove(this.$route.path);
buttons.success("delete"); buttons.success("delete");
currentPrompt.value?.confirm(); this.currentPrompt?.confirm();
closeHovers(); this.closeHovers();
return; return;
} }
closeHovers(); this.closeHovers();
if (selectedCount.value === 0) { if (this.selectedCount === 0) {
return; return;
} }
const promises = []; const promises = [];
for (const index of selected.value) { for (const index of this.selected) {
promises.push(api.remove(req.value!.items[index].url)); promises.push(api.remove(this.req.items[index].url));
} }
await Promise.all(promises); await Promise.all(promises);
buttons.success("delete"); buttons.success("delete");
const nearbyItem = const nearbyItem =
req.value!.items[Math.max(0, Math.min(...selected.value) - 1)]; this.req.items[Math.max(0, Math.min(this.selected) - 1)];
fileStore.preselect = nearbyItem?.path; this.preselect = nearbyItem?.path;
fileStore.reload = true; this.reload = true;
} catch (e) { } catch (e) {
buttons.done("delete"); buttons.done("delete");
$showError?.(e); this.$showError(e);
if (isListing.value) fileStore.reload = true; if (this.isListing) this.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: {
const emit = defineEmits<{ type: Array,
"update:selected": [value: string]; default: () => [],
}>(); },
},
const route = useRoute(); data: function () {
const $showError = inject<(error: unknown) => void>("$showError"); return {
items: [],
const authStore = useAuthStore(); touches: {
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const { user } = storeToRefs(authStore);
const { req } = storeToRefs(fileStore);
const { showHover } = layoutStore;
const items = ref<Array<{ name: string; url: string }>>([]);
const touches = ref({
id: "", id: "",
count: 0, count: 0,
}); },
const selected = ref<string | null>(null); selected: null,
const current = ref(window.location.pathname); current: window.location.pathname,
const nextAbortController = ref(new AbortController()); nextAbortController: new AbortController(),
};
const nav = computed(() => { },
return decodeURIComponent(current.value); inject: ["$showError"],
}); computed: {
...mapState(useAuthStore, ["user"]),
const abortOngoingNext = () => { ...mapState(useFileStore, ["req"]),
nextAbortController.value.abort(); nav() {
}; return decodeURIComponent(this.current);
},
const fillOptions = (reqData: any) => { },
mounted() {
this.fillOptions(this.req);
},
unmounted() {
this.abortOngoingNext();
},
methods: {
...mapActions(useLayoutStore, ["showHover"]),
abortOngoingNext() {
this.nextAbortController.abort();
},
fillOptions(req) {
// Sets the current path and resets // Sets the current path and resets
// the current items. // the current items.
current.value = reqData.url; this.current = req.url;
items.value = []; this.items = [];
emit("update:selected", current.value); this.$emit("update:selected", this.current);
// If the path isn't the root path, // If the path isn't the root path,
// show a button to navigate to the previous // show a button to navigate to the previous
// directory. // directory.
if (reqData.url !== "/files/") { if (req.url !== "/files/") {
items.value.push({ this.items.push({
name: "..", name: "..",
url: url.removeLastDir(reqData.url) + "/", url: url.removeLastDir(req.url) + "/",
}); });
} }
// If this folder is empty, finish here. // If this folder is empty, finish here.
if (reqData.items === null) return; if (req.items === null) return;
// Otherwise we add every directory to the // Otherwise we add every directory to the
// move options. // move options.
for (const item of reqData.items) { for (const item of req.items) {
if (!item.isDir) continue; if (!item.isDir) continue;
if (props.exclude?.includes(item.url)) continue; if (this.exclude?.includes(item.url)) continue;
items.value.push({ this.items.push({
name: item.name, name: item.name,
url: item.url, url: item.url,
}); });
} }
}; },
next: function (event) {
const next = (event: Event) => {
// Retrieves the URL of the directory the user // Retrieves the URL of the directory the user
// just clicked in and fill the options with its // just clicked in and fill the options with its
// content. // content.
const uri = (event.currentTarget as HTMLElement).dataset.url!; const uri = event.currentTarget.dataset.url;
abortOngoingNext(); this.abortOngoingNext();
nextAbortController.value = new AbortController(); this.nextAbortController = new AbortController();
files files
.fetch(uri, nextAbortController.value.signal) .fetch(uri, this.nextAbortController.signal)
.then(fillOptions) .then(this.fillOptions)
.catch((e) => { .catch((e) => {
if (e instanceof StatusError && e.is_canceled) { if (e instanceof StatusError && e.is_canceled) {
return; return;
} }
$showError?.(e); this.$showError(e);
}); });
}; },
touchstart(event) {
const touchstart = (event: Event) => { const url = event.currentTarget.dataset.url;
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
// In 300 milliseconds, we shall reset the count. // In 300 milliseconds, we shall reset the count.
setTimeout(() => { setTimeout(() => {
touches.value.count = 0; this.touches.count = 0;
}, 300); }, 300);
// If the element the user is touching // If the element the user is touching
// is different from the last one he touched, // is different from the last one he touched,
// reset the count. // reset the count.
if (touches.value.id !== urlValue) { if (this.touches.id !== url) {
touches.value.id = urlValue; this.touches.id = url;
touches.value.count = 1; this.touches.count = 1;
return; return;
} }
touches.value.count++; this.touches.count++;
// If there is more than one touch already, // If there is more than one touch already,
// open the next screen. // open the next screen.
if (touches.value.count > 1) { if (this.touches.count > 1) {
next(event); this.next(event);
} }
}; },
itemClick: function (event) {
const itemClick = (event: Event) => { if (this.user.singleClick) this.next(event);
if (user.value?.singleClick) next(event); else this.select(event);
else select(event); },
}; select: function (event) {
const select = (event: Event) => {
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
// If the element is already selected, unselect it. // If the element is already selected, unselect it.
if (selected.value === urlValue) { if (this.selected === event.currentTarget.dataset.url) {
selected.value = null; this.selected = null;
emit("update:selected", current.value); this.$emit("update:selected", this.current);
return; return;
} }
// Otherwise select the element. // Otherwise select the element.
selected.value = urlValue; this.selected = event.currentTarget.dataset.url;
emit("update:selected", selected.value); this.$emit("update:selected", this.selected);
}; },
createDir: async function () {
const createDir = async () => { this.showHover({
showHover({
prompt: "newDir", prompt: "newDir",
action: undefined, action: null,
confirm: undefined, confirm: null,
props: { props: {
redirect: false, redirect: false,
base: current.value === route.path ? null : current.value, 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"],
const fileStore = useFileStore(); computed: {
const layoutStore = useLayoutStore(); ...mapState(useFileStore, [
"req",
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore); "selected",
const { closeHovers } = layoutStore; "selectedCount",
"isListing",
const humanSize = computed(() => { ]),
if (selectedCount.value === 0 || !isListing.value) { humanSize: function () {
return filesize(req.value?.size ?? 0); if (this.selectedCount === 0 || !this.isListing) {
return filesize(this.req.size);
} }
let sum = 0; let sum = 0;
for (const selectedIdx of selected.value) { for (const selected of this.selected) {
sum += req.value?.items[selectedIdx]?.size ?? 0; sum += this.req.items[selected].size;
} }
return filesize(sum); return filesize(sum);
}); },
humanTime: function () {
const humanTime = computed(() => { if (this.selectedCount === 0) {
if (selectedCount.value === 0) { return dayjs(this.req.modified).fromNow();
return dayjs(req.value?.modified).fromNow();
} }
return dayjs(req.value?.items[selected.value[0]]?.modified).fromNow(); return dayjs(this.req.items[this.selected[0]].modified).fromNow();
}); },
modTime: function () {
const modTime = computed(() => { if (this.selectedCount === 0) {
if (selectedCount.value === 0) { return new Date(Date.parse(this.req.modified)).toLocaleString();
return new Date(Date.parse(req.value?.modified ?? "")).toLocaleString();
} }
return new Date( return new Date(
Date.parse(req.value?.items[selected.value[0]]?.modified ?? "") Date.parse(this.req.items[this.selected[0]].modified)
).toLocaleString(); ).toLocaleString();
}); },
name: function () {
const name = computed(() => { return this.selectedCount === 0
return selectedCount.value === 0 ? this.req.name
? (req.value?.name ?? "") : this.req.items[this.selected[0]].name;
: (req.value?.items[selected.value[0]]?.name ?? ""); },
}); dir: function () {
const dir = computed(() => {
return ( return (
selectedCount.value > 1 || this.selectedCount > 1 ||
(selectedCount.value === 0 (this.selectedCount === 0
? (req.value?.isDir ?? false) ? this.req.isDir
: (req.value?.items[selected.value[0]]?.isDir ?? false)) : this.req.items[this.selected[0]].isDir)
); );
}); },
resolution: function () {
const resolution = computed(() => { if (this.selectedCount === 1) {
if (selectedCount.value === 1) { const selectedItem = this.req.items[this.selected[0]];
const selectedItem = req.value?.items[selected.value[0]];
if (selectedItem && selectedItem.type === "image") { if (selectedItem && selectedItem.type === "image") {
return (selectedItem as any).resolution; return selectedItem.resolution;
} }
} else if (req.value && req.value.type === "image") { } else if (this.req && this.req.type === "image") {
return (req.value as any).resolution; return this.req.resolution;
} }
return null; return null;
}); },
},
const checksum = async (event: Event, algo: string) => { methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
checksum: async function (event, algo) {
event.preventDefault(); event.preventDefault();
let link; let link;
if (selectedCount.value) { if (this.selectedCount) {
link = req.value?.items[selected.value[0]]?.url ?? ""; link = this.req.items[this.selected[0]].url;
} else { } else {
link = route.path; link = this.$route.path;
} }
try { try {
const hash = await api.checksum(link, algo as any); const hash = await api.checksum(link, algo);
(event.target as HTMLElement).textContent = hash; event.target.textContent = hash;
} catch (e) { } catch (e) {
$showError?.(e); this.$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,69 +66,71 @@ 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 },
const fileStore = useFileStore(); data: function () {
const layoutStore = useLayoutStore(); return {
const authStore = useAuthStore(); current: window.location.pathname,
dest: null,
const { req, selected } = storeToRefs(fileStore); };
const { user } = storeToRefs(authStore); },
const { showHover, closeHovers } = layoutStore; inject: ["$showError"],
computed: {
const fileList = ref<InstanceType<typeof FileList> | null>(null); ...mapState(useFileStore, ["req", "selected"]),
const dest = ref<string | null>(null); ...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["preselect"]),
const excludedFolders = computed(() => { excludedFolders() {
return selected.value return this.selected
.filter((idx) => req.value!.items[idx].isDir) .filter((idx) => this.req.items[idx].isDir)
.map((idx) => req.value!.items[idx].url); .map((idx) => this.req.items[idx].url);
}); },
},
const move = async (event: Event) => { methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
move: async function (event) {
event.preventDefault(); event.preventDefault();
const items: Array<{ from: string; to: string; name: string }> = []; const items = [];
for (const item of selected.value) { for (const item of this.selected) {
items.push({ items.push({
from: req.value!.items[item].url, from: this.req.items[item].url,
to: dest.value! + encodeURIComponent(req.value!.items[item].name), to: this.dest + encodeURIComponent(this.req.items[item].name),
name: req.value!.items[item].name, name: this.req.items[item].name,
}); });
} }
const action = async (overwrite: boolean, rename: boolean) => { const action = async (overwrite, rename) => {
buttons.loading("move"); buttons.loading("move");
await api await api
.move(items, overwrite, rename) .move(items, overwrite, rename)
.then(() => { .then(() => {
buttons.success("move"); buttons.success("move");
fileStore.preselect = removePrefix(items[0].to); this.preselect = removePrefix(items[0].to);
router.push({ path: dest.value! }); this.$router.push({ path: this.dest });
}) })
.catch((e) => { .catch((e) => {
buttons.done("move"); buttons.done("move");
$showError?.(e); this.$showError(e);
}); });
}; };
const dstItems = (await api.fetch(dest.value!)).items; const dstItems = (await api.fetch(this.dest)).items;
const conflict = upload.checkConflict(items as any, dstItems); const conflict = upload.checkConflict(items, dstItems);
let overwrite = false; let overwrite = false;
let rename = false; let rename = false;
if (conflict) { if (conflict) {
showHover({ this.showHover({
prompt: "replace-rename", prompt: "replace-rename",
confirm: (event: Event, option: string) => { confirm: (event, option) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
closeHovers(); this.closeHovers();
action(overwrite, rename); action(overwrite, rename);
}, },
}); });
@@ -138,5 +139,7 @@ const move = async (event: Event) => {
} }
action(overwrite, rename); 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 () {
const fileStore = useFileStore(); return {
const layoutStore = useLayoutStore(); name: "",
};
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore); },
const { closeHovers } = layoutStore; created() {
this.name = this.oldName();
const name = ref(""); },
inject: ["$showError"],
const oldName = (): string => { computed: {
if (!isListing.value) { ...mapState(useFileStore, [
return req.value?.name ?? ""; "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;
} }
if (selectedCount.value === 0 || selectedCount.value > 1) { if (this.selectedCount === 0 || this.selectedCount > 1) {
// This shouldn't happen. // This shouldn't happen.
return ""; return;
} }
return req.value?.items[selected.value[0]].name ?? ""; return this.req.items[this.selected[0]].name;
}; },
submit: async function () {
onMounted(() => {
name.value = oldName();
});
const submit = async () => {
let oldLink = ""; let oldLink = "";
let newLink = ""; let newLink = "";
if (!req.value) { if (!this.isListing) {
return; oldLink = this.req.url;
}
if (!isListing.value) {
oldLink = req.value.url;
} else { } else {
oldLink = req.value.items[selected.value[0]].url; oldLink = this.req.items[this.selected[0]].url;
} }
newLink = url.removeLastDir(oldLink) + "/" + encodeURIComponent(name.value); newLink =
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
try { try {
await api.move([{ from: oldLink, to: newLink }]); await api.move([{ from: oldLink, to: newLink }]);
if (!isListing.value) { if (!this.isListing) {
router.push({ path: newLink }); this.$router.push({ path: newLink });
return; return;
} }
fileStore.preselect = removePrefix(newLink); this.preselect = removePrefix(newLink);
fileStore.reload = true; this.reload = true;
} catch (e) { } catch (e) {
$showError?.(e); this.$showError(e);
} }
closeHovers(); this.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,
const fileStore = useFileStore(); unit: "hours",
const layoutStore = useLayoutStore(); links: [],
clip: null,
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore); password: "",
const { closeHovers } = layoutStore; listing: true,
};
const time = ref(0); },
const unit = ref("hours"); inject: ["$showError", "$showSuccess"],
const links = ref<any[]>([]); computed: {
const password = ref(""); ...mapState(useFileStore, [
const listing = ref(true); "req",
"selected",
const url = computed(() => { "selectedCount",
if (!isListing.value) { "isListing",
return route.path; ]),
url() {
if (!this.isListing) {
return this.$route.path;
} }
if (selectedCount.value === 0 || selectedCount.value > 1) { if (this.selectedCount === 0 || this.selectedCount > 1) {
// This shouldn't happen. // This shouldn't happen.
return ""; return;
} }
return req.value?.items[selected.value[0]].url ?? ""; return this.req.items[this.selected[0]].url;
}); },
},
async beforeMount() {
try {
const links = await api.get(this.url);
this.links = links;
this.sort();
const copyToClipboard = (text: string) => { if (this.links.length == 0) {
this.listing = false;
}
} catch (e) {
this.$showError(e);
}
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
copyToClipboard: function (text) {
copy({ text }).then( copy({ text }).then(
() => { () => {
// clipboard successfully set // clipboard successfully set
$showSuccess?.(t("success.linkCopied")); this.$showSuccess(this.$t("success.linkCopied"));
}, },
() => { () => {
// clipboard write failed // clipboard write failed
copy({ text }, { permission: true }).then( copy({ text }, { permission: true }).then(
() => { () => {
// clipboard successfully set // clipboard successfully set
$showSuccess?.(t("success.linkCopied")); this.$showSuccess(this.$t("success.linkCopied"));
}, },
(e) => { (e) => {
// clipboard write failed // clipboard write failed
$showError?.(e); this.$showError(e);
} }
); );
} }
); );
}; },
submit: async function () {
const submit = async () => {
try { try {
let res = null; let res = null;
if (!time.value) { if (!this.time) {
res = await api.create(url.value, password.value); res = await api.create(this.url, this.password);
} else { } else {
res = await api.create( res = await api.create(this.url, this.password, this.time, this.unit);
url.value,
password.value,
String(time.value),
unit.value
);
} }
links.value.push(res); this.links.push(res);
sort(); this.sort();
time.value = 0; this.time = 0;
unit.value = "hours"; this.unit = "hours";
password.value = ""; this.password = "";
listing.value = true; this.listing = true;
} catch (e) { } catch (e) {
$showError?.(e); this.$showError(e);
} }
}; },
deleteLink: async function (event, link) {
const deleteLink = async (event: Event, link: any) => {
event.preventDefault(); event.preventDefault();
try { try {
await api.remove(link.hash); await api.remove(link.hash);
links.value = links.value.filter((item) => item.hash !== link.hash); this.links = this.links.filter((item) => item.hash !== link.hash);
if (links.value.length == 0) { if (this.links.length == 0) {
listing.value = false; this.listing = false;
} }
} catch (e) { } catch (e) {
$showError?.(e); this.$showError(e);
} }
}; },
humanTime(time) {
const humanTime = (time: number) => {
return dayjs(time * 1000).fromNow(); return dayjs(time * 1000).fromNow();
}; },
buildLink(share) {
const buildLink = (share: any) => {
return api.getShareURL(share); return api.getShareURL(share);
}; },
sort() {
const sort = () => { this.links = this.links.sort((a, b) => {
links.value = links.value.sort((a, b) => {
if (a.expire === 0) return -1; if (a.expire === 0) return -1;
if (b.expire === 0) return 1; if (b.expire === 0) return 1;
return new Date(a.expire).getTime() - new Date(b.expire).getTime(); return new Date(a.expire) - new Date(b.expire);
}); });
},
switchListing() {
if (this.links.length == 0 && !this.listing) {
this.closeHovers();
}
this.listing = !this.listing;
},
},
}; };
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: {
const emit = defineEmits<{
"update:commands": [commands: string[]];
}>();
const raw = computed({
get() { get() {
return props.commands.join(" "); return this.commands.join(" ");
}, },
set(value: string) { set(value) {
if (value !== "") { if (value !== "") {
emit("update:commands", value.split(" ")); this.$emit("update:commands", value.split(" "));
} else { } else {
emit("update:commands", []); this.$emit("update:commands", []);
} }
}, },
}); },
},
};
</script> </script>

View File

@@ -6,18 +6,15 @@
</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 emit = defineEmits<{ const dataObj = {};
"update:locale": [locale: string]; const locales = {
}>();
const locales = markRaw({
he: "עברית", he: "עברית",
hr: "Hrvatski", hr: "Hrvatski",
hu: "Magyar", hu: "Magyar",
@@ -47,9 +44,23 @@ const locales = markRaw({
vi: "Tiếng Việt", vi: "Tiếng Việt",
"zh-cn": "中文 (简体)", "zh-cn": "中文 (简体)",
"zh-tw": "中文 (繁體)", "zh-tw": "中文 (繁體)",
}); };
const change = (event: Event) => { // Vue3 reactivity breaks with this configuration
emit("update:locale", (event.target as HTMLSelectElement).value); // 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,
});
return dataObj;
},
methods: {
change(event) {
this.$emit("update:locale", event.target.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 {
const props = defineProps<{ name: "permissions",
perm: UserPermissions; props: ["perm"],
}>(); computed: {
admin: {
const admin = computed({
get() { get() {
return props.perm.admin; return this.perm.admin;
}, },
set(value: boolean) { set(value) {
if (value) { if (value) {
for (const key in props.perm) { for (const key in this.perm) {
props.perm[key as keyof UserPermissions] = true; this.perm[key] = true;
} }
} }
props.perm.admin = value; this.perm.admin = value;
}, },
}); },
isExecEnabled: () => enableExec,
const isExecEnabled = enableExec; },
};
</script> </script>

View File

@@ -32,36 +32,22 @@
</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;
};
}
const props = defineProps<{
rules: Rule[];
}>();
const emit = defineEmits<{
"update:rules": [rules: Rule[]];
}>();
const remove = (event: Event, index: number) => {
event.preventDefault(); event.preventDefault();
const rules = [...props.rules]; const rules = [...this.rules];
rules.splice(index, 1); rules.splice(index, 1);
emit("update:rules", [...rules]); this.$emit("update:rules", [...rules]);
}; },
create(event) {
const create = (event: Event) => {
event.preventDefault(); event.preventDefault();
emit("update:rules", [ this.$emit("update:rules", [
...props.rules, ...this.rules,
{ {
allow: true, allow: true,
path: "", path: "",
@@ -71,5 +57,7 @@ const create = (event: Event) => {
}, },
}, },
]); ]);
},
},
}; };
</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: false; isDefault: boolean;
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 {

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

8
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
@@ -16,7 +16,6 @@ require (
github.com/marusama/semaphore/v2 v2.5.0 github.com/marusama/semaphore/v2 v2.5.0
github.com/mholt/archives v0.1.5 github.com/mholt/archives v0.1.5
github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir v1.1.0
github.com/pelletier/go-toml/v2 v2.2.4
github.com/shirou/gopsutil/v4 v4.25.10 github.com/shirou/gopsutil/v4 v4.25.10
github.com/spf13/afero v1.15.0 github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.1 github.com/spf13/cobra v1.10.1
@@ -24,7 +23,6 @@ require (
github.com/spf13/viper v1.21.0 github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
go.etcd.io/bbolt v1.4.3
golang.org/x/crypto v0.44.0 golang.org/x/crypto v0.44.0
golang.org/x/image v0.33.0 golang.org/x/image v0.33.0
golang.org/x/text v0.31.0 golang.org/x/text v0.31.0
@@ -40,6 +38,7 @@ require (
github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.1 // indirect github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect github.com/bodgit/windows v1.0.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
@@ -58,9 +57,11 @@ require (
github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect github.com/minio/minlz v1.0.1 // indirect
github.com/nwaples/rardecode/v2 v2.2.0 // indirect github.com/nwaples/rardecode/v2 v2.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
@@ -68,6 +69,7 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect github.com/ulikunitz/xz v0.5.15 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/net v0.46.0 // indirect golang.org/x/net v0.46.0 // indirect

6
go.sum
View File

@@ -47,6 +47,7 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -97,8 +98,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=
@@ -196,6 +197,7 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=

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

@@ -28,7 +28,6 @@ var (
cmdNotAllowed = []byte("Command not allowed.") cmdNotAllowed = []byte("Command not allowed.")
) )
//nolint:unparam
func wsErr(ws *websocket.Conn, r *http.Request, status int, err error) { func wsErr(ws *websocket.Conn, r *http.Request, status int, err error) {
txt := http.StatusText(status) txt := http.StatusText(status)
if err != nil || status >= 400 { if err != nil || status >= 400 {
@@ -49,7 +48,7 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
var raw string var raw string
for { for {
_, msg, err := conn.ReadMessage() //nolint:govet _, msg, err := conn.ReadMessage()
if err != nil { if err != nil {
wsErr(conn, r, http.StatusInternalServerError, err) wsErr(conn, r, http.StatusInternalServerError, err)
return 0, nil return 0, nil
@@ -63,7 +62,7 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
// Fail fast // Fail fast
if !d.server.EnableExec || !d.user.Perm.Execute { if !d.server.EnableExec || !d.user.Perm.Execute {
if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil { //nolint:govet if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil {
wsErr(conn, r, http.StatusInternalServerError, err) wsErr(conn, r, http.StatusInternalServerError, err)
} }
@@ -72,21 +71,21 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
command, name, err := runner.ParseCommand(d.settings, raw) command, name, err := runner.ParseCommand(d.settings, raw)
if err != nil { if err != nil {
if err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error())); err != nil { //nolint:govet if err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error())); err != nil {
wsErr(conn, r, http.StatusInternalServerError, err) wsErr(conn, r, http.StatusInternalServerError, err)
} }
return 0, nil return 0, nil
} }
if !slices.Contains(d.user.Commands, name) { if !slices.Contains(d.user.Commands, name) {
if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil { //nolint:govet if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil {
wsErr(conn, r, http.StatusInternalServerError, err) wsErr(conn, r, http.StatusInternalServerError, err)
} }
return 0, nil return 0, nil
} }
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec cmd := exec.Command(command[0], command[1:]...)
cmd.Dir = d.user.FullPath(r.URL.Path) cmd.Dir = d.user.FullPath(r.URL.Path)
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()

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

@@ -19,7 +19,7 @@ import (
func TestPublicShareHandlerAuthentication(t *testing.T) { func TestPublicShareHandlerAuthentication(t *testing.T) {
t.Parallel() t.Parallel()
const passwordBcrypt = "$2y$10$TFAmdCbyd/mEZDe5fUeZJu.MaJQXRTwdqb/IQV.eTn6dWrF58gCSe" //nolint:gosec const passwordBcrypt = "$2y$10$TFAmdCbyd/mEZDe5fUeZJu.MaJQXRTwdqb/IQV.eTn6dWrF58gCSe"
testCases := map[string]struct { testCases := map[string]struct {
share *share.Link share *share.Link
req *http.Request req *http.Request
@@ -70,7 +70,7 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
} }
t.Cleanup(func() { t.Cleanup(func() {
if err := db.Close(); err != nil { //nolint:govet if err := db.Close(); err != nil {
t.Errorf("failed to close db: %v", err) t.Errorf("failed to close db: %v", err)
} }
}) })

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)
} }

Some files were not shown because too many files have changed in this diff Show More