mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-13 12:41:59 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7524b20dca | ||
|
|
2ded6a96c8 | ||
|
|
5cad9f0f0d | ||
|
|
9ed714923f | ||
|
|
6ae2f6feb8 | ||
|
|
e10634078a | ||
|
|
8ee0418dd8 | ||
|
|
b1094e178a | ||
|
|
a2a97bc7ef | ||
|
|
aa39d634e9 | ||
|
|
0fa2cc24b9 | ||
|
|
dba3268237 | ||
|
|
0338f9ede4 | ||
|
|
761451e9c1 | ||
|
|
e5b13e465d | ||
|
|
2e2c8204bb | ||
|
|
505921004b | ||
|
|
259594ebce | ||
|
|
eaeeba3d9e | ||
|
|
db1fdd2e93 | ||
|
|
5992d28e9e | ||
|
|
fb23ea6add | ||
|
|
693f7965f1 | ||
|
|
e6edaf76c8 | ||
|
|
8c2e824da7 | ||
|
|
1ab6e9fb7c | ||
|
|
2d372e1b00 | ||
|
|
dfb181f801 | ||
|
|
29b180ea1c | ||
|
|
92ef5916fe | ||
|
|
dca9097d6c | ||
|
|
1c462b8e96 | ||
|
|
e59c9e327d | ||
|
|
8546fa294f | ||
|
|
0a195b9f95 |
No files matched your search
@@ -0,0 +1,79 @@
|
||||
name: dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'dev_v*'
|
||||
|
||||
jobs:
|
||||
test-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go test -race -v ./...
|
||||
lint-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.59
|
||||
working-directory: backend
|
||||
format-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go fmt ./...
|
||||
lint-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i eslint && npm run lint
|
||||
|
||||
push_dev_to_registry:
|
||||
name: Push dev image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
# Workaround to fix error:
|
||||
# failed to push: failed to copy: io: read/write on closed pipe
|
||||
# See https://github.com/docker/build-push-action/issues/761
|
||||
with:
|
||||
driver-opts: |
|
||||
image=moby/buildkit:v0.10.6
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: gtstef/filebrowser
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
+47
-13
@@ -1,12 +1,50 @@
|
||||
name: main release
|
||||
name: main job
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
test-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go test -race -v ./...
|
||||
lint-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.59
|
||||
working-directory: backend
|
||||
format-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go fmt ./...
|
||||
lint-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i eslint && npm run lint
|
||||
|
||||
push_latest_to_registry:
|
||||
needs: [lint-frontend, lint-backend, test-backend, format-backend]
|
||||
name: Push latest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -16,6 +54,12 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
# Workaround to fix error:
|
||||
# failed to push: failed to copy: io: read/write on closed pipe
|
||||
# See https://github.com/docker/build-push-action/issues/761
|
||||
with:
|
||||
driver-opts: |
|
||||
image=moby/buildkit:v0.10.6
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -26,22 +70,12 @@ jobs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: gtstef/filebrowser
|
||||
- name: Get latest release tag and commit SHA
|
||||
id: get_tag_and_sha
|
||||
run: |
|
||||
latest_tag=$(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
latest_commit=$(git rev-list -n 1 $latest_tag)
|
||||
echo "latest_tag=${latest_tag}" >> $GITHUB_ENV
|
||||
echo "latest_commit=${latest_commit}" >> $GITHUB_ENV
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
build-args: |
|
||||
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
||||
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: 'gtstef/filebrowser:latest'
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -0,0 +1,81 @@
|
||||
name: pr-merge
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'dev_v*'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
|
||||
jobs:
|
||||
test-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go test -race -v ./...
|
||||
lint-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.59
|
||||
working-directory: backend
|
||||
format-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go fmt ./...
|
||||
lint-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i eslint && npm run lint
|
||||
|
||||
push_pr_to_registry:
|
||||
name: Push PR
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
# Workaround to fix error:
|
||||
# failed to push: failed to copy: io: read/write on closed pipe
|
||||
# See https://github.com/docker/build-push-action/issues/761
|
||||
with:
|
||||
driver-opts: |
|
||||
image=moby/buildkit:v0.10.6
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: gtstef/filebrowser
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -1,56 +0,0 @@
|
||||
name: pr-request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
jobs:
|
||||
test_frontend:
|
||||
name: Push release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.playwright
|
||||
push: false
|
||||
push_pr_to_registry:
|
||||
name: Push PR
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: gtstef/filebrowser
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
version=${{ steps.meta.outputs.version }}
|
||||
commitSHA=${{ steps.meta.outputs.revision }}
|
||||
@@ -1,44 +0,0 @@
|
||||
name: dev tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
test-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go test -race -v ./...
|
||||
lint-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22.5'
|
||||
- uses: golangci/golangci-lint-action@v5
|
||||
with:
|
||||
version: v1.60
|
||||
working-directory: backend
|
||||
format-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go fmt ./...
|
||||
lint-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i eslint && npm run lint
|
||||
@@ -1,9 +1,9 @@
|
||||
name: version release
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -14,28 +14,63 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
uses: actions/checkout@v2
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
|
||||
id: extract_branch
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ steps.extract_branch.outputs.branch }}
|
||||
release_name: ${{ steps.extract_branch.outputs.branch }}
|
||||
body: |
|
||||
Please see usage on [readme](https://github.com/gtsteffaniak/filebrowser).
|
||||
Docker repo: https://hub.docker.com/r/gtstef/filebrowser/tags
|
||||
draft: false
|
||||
prerelease: false
|
||||
test-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22.2
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go test -race -v ./...
|
||||
lint-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
target_commitish: ${{ steps.extract_branch.outputs.branch }}
|
||||
token: ${{ secrets.PAT }}
|
||||
tag_name: ${{ steps.extract_branch.outputs.branch }}
|
||||
prerelease: false
|
||||
draft: false
|
||||
generate_release_notes: true
|
||||
name: ${{ steps.extract_branch.outputs.branch }}
|
||||
go-version: 'stable'
|
||||
- uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.59
|
||||
working-directory: backend
|
||||
format-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go fmt ./...
|
||||
lint-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i eslint && npm run lint
|
||||
|
||||
push_release_to_registry:
|
||||
needs: [lint-frontend, lint-backend, test-backend, format-backend]
|
||||
name: Push release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -45,7 +80,14 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
# Workaround to fix error:
|
||||
# failed to push: failed to copy: io: read/write on closed pipe
|
||||
# See https://github.com/docker/build-push-action/issues/761
|
||||
with:
|
||||
driver-opts: |
|
||||
image=moby/buildkit:v0.10.6
|
||||
- name: Login to Docker Hub
|
||||
# Only push to Docker Hub when making a release
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
@@ -63,14 +105,11 @@ jobs:
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
|
||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
build-args: |
|
||||
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
||||
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -1,50 +0,0 @@
|
||||
name: dev release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "dev_v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
push_release_to_registry:
|
||||
name: Push dev release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: gtstef/filebrowser
|
||||
- name: Strip v from version number
|
||||
id: modify-json
|
||||
run: |
|
||||
JSON="${{ steps.meta.outputs.tags }}"
|
||||
# Use jq to remove 'v' from the version field
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
|
||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
build-args: |
|
||||
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
||||
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -0,0 +1,24 @@
|
||||
name: 'Close stale issues and PRs'
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
|
||||
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||
close-issue-message: 'This issue was closed because it has been stalled for 30 days with no activity.'
|
||||
days-before-stale: 30
|
||||
days-before-close: 30
|
||||
exempt-issue-labels: 'feature ☘,enhancement ⚙,bug 🐞'
|
||||
exempt-pr-labels: 'need-help,wip'
|
||||
operations-per-run: 100
|
||||
@@ -1,31 +0,0 @@
|
||||
name: tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update_tag:
|
||||
name: Update Release tag
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Install dependencies and build frontend
|
||||
run: npm i && npm run build
|
||||
working-directory: frontend
|
||||
- name: Install UPX
|
||||
run: sudo apt-get install -y upx
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
args: release --clean
|
||||
workdir: backend
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -7,12 +7,9 @@ rice-box.go
|
||||
/filebrowser.exe
|
||||
/frontend/dist
|
||||
/frontend/pkg
|
||||
/frontend/test-results
|
||||
/frontend/package-lock.json
|
||||
/backend/vendor
|
||||
/backend/*.cov
|
||||
/backend/test_config.yaml
|
||||
/backend/srv
|
||||
|
||||
.DS_Store
|
||||
node_modules
|
||||
|
||||
@@ -2,24 +2,6 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
|
||||
|
||||
## v0.2.8
|
||||
|
||||
- **Feature**: New gallary view scaling options (closes [#141](https://github.com/gtsteffaniak/filebrowser/issues/141))
|
||||
- **Change**: Refactored backend files functions
|
||||
- **Change**: Improved UI response to filesystem changes
|
||||
- **Change**: Added frontend tests for deployment integrity
|
||||
- **Fix**: move/replace file prompt issue
|
||||
- **Fix**: opening files from search
|
||||
- **Fix**: Display count issue when hideDotFile is enabled.
|
||||
|
||||
## v0.2.7
|
||||
|
||||
- **Change**: New sidebar style and behavior
|
||||
- **Change**: make search view and button behavior more consistent.
|
||||
- **Fix**: [upload file bug](https://github.com/gtsteffaniak/filebrowser/issues/153)
|
||||
- **Fix**: user lock out bug introduced in 0.2.6
|
||||
- **Fix**: many minor state related issues.
|
||||
|
||||
## v0.2.6
|
||||
|
||||
This change focuses on minimizing and simplifying build process.
|
||||
|
||||
+9
-14
@@ -1,22 +1,17 @@
|
||||
FROM golang:1.22-alpine AS base
|
||||
ARG VERSION
|
||||
ARG REVISION
|
||||
FROM node:slim as nbuild
|
||||
WORKDIR /app
|
||||
COPY ./backend ./
|
||||
RUN go build -ldflags="-w -s \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/version.Version=${VERSION}' \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/version.CommitSHA=${REVISION}'" \
|
||||
-o filebrowser .
|
||||
|
||||
FROM node:slim AS nbuild
|
||||
WORKDIR /app
|
||||
COPY ./frontend/package.json ./
|
||||
COPY ./frontend/package*.json ./
|
||||
RUN npm i --maxsockets 1
|
||||
COPY ./frontend/ ./
|
||||
RUN npm run build-docker
|
||||
RUN npm run build
|
||||
|
||||
FROM golang:1.22-alpine as base
|
||||
WORKDIR /app
|
||||
COPY ./backend ./
|
||||
RUN go build -ldflags="-w -s" -o filebrowser .
|
||||
|
||||
FROM alpine:latest
|
||||
ENV FILEBROWSER_NO_EMBEDED="true"
|
||||
ARG app="/app/filebrowser"
|
||||
RUN apk --no-cache add ca-certificates mailcap
|
||||
COPY --from=base /app/filebrowser* ./
|
||||
COPY --from=nbuild /app/dist/ ./frontend/dist/
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
FROM golang:1.22-alpine AS base
|
||||
WORKDIR /app
|
||||
COPY ./backend ./
|
||||
RUN go build -ldflags="-w -s" -o filebrowser .
|
||||
|
||||
FROM node:slim
|
||||
WORKDIR /app
|
||||
COPY ./frontend/package.json ./
|
||||
RUN npm i --maxsockets 1
|
||||
RUN npx playwright install --with-deps firefox
|
||||
COPY [ "backend/filebrowser.yaml", "./" ]
|
||||
COPY ./frontend/ ./frontend
|
||||
WORKDIR /app/frontend
|
||||
RUN npm run build-docker
|
||||
WORKDIR /app
|
||||
COPY --from=base /app/filebrowser* ./
|
||||
RUN cp -R frontend/tests/ srv
|
||||
ENV FILEBROWSER_NO_EMBEDED="true"
|
||||
RUN ./filebrowser & sleep 2 && cd frontend && npx playwright test
|
||||
@@ -2,76 +2,70 @@
|
||||
<a href="https://opensource.org/license/apache-2-0/"><img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg" alt="License: Apache-2.0"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="frontend/public/img/icons/favicon-256x256.png" width="100" title="Login With Custom URL">
|
||||
<img src="frontend/public/img/icons/favicon-256x256.png" width="100" title="Login With Custom URL">
|
||||
</p>
|
||||
<h3 align="center">Filebrowser Quantum - A modern web-based file manager</h3>
|
||||
<h3 align="center">Filebrowser - A modern web-based file manager</h3>
|
||||
<p align="center">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/8ba93582-aba2-4996-8ac3-25f763a2e596" title="Main Screenshot">
|
||||
<img width="800" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/899152cf-3e69-4179-aa82-752af2df3fc6" title="Main Screenshot">
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
> Starting with v0.2.0, *ALL* configuration is done via `filebrowser.yaml`
|
||||
> Configuration file.
|
||||
> Starting with v0.2.4 *ALL* share links need to be re-created (due to
|
||||
> security fix).
|
||||
> Starting with v0.2.0, *ALL* configuration is done via `filebrowser.yaml` configuration file.
|
||||
> Starting with v0.2.4 *ALL* share links need to be re-created (due to security fix).
|
||||
|
||||
Filebrowser Quantum is a fork of the filebrowser opensource project with the
|
||||
following changes:
|
||||
This fork makes the following significant changes to filebrowser for origin:
|
||||
|
||||
1. [x] Enhanced lightning fast indexed search
|
||||
- Real-time results as you type
|
||||
1. [x] Better search
|
||||
- Lightning fast
|
||||
- realtime results as you type
|
||||
- Works with more type filters
|
||||
- Enhanced interactive results page.
|
||||
- interactive results page.
|
||||
2. [x] Revamped and simplified GUI navbar and sidebar menu.
|
||||
- Additional compact view mode as well as refreshed view mode
|
||||
styles.
|
||||
3. [x] Revamped and simplified configuration via `filebrowser.yml` config file.
|
||||
4. [x] Faster listing browsing
|
||||
- Switching view modes is instant
|
||||
- Changing Sort order is instant
|
||||
- The entire directory is loaded in 1/3 the time
|
||||
- Additional compact view mode as well as refreshed view mode styles.
|
||||
3. [x] Revamped configuration via `filebrowser.yml` config file.
|
||||
- More configurations possible at a per-user level
|
||||
- <img width="450" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/625bd7c4-5ee9-4011-aaae-2a388ab0813b">
|
||||
|
||||
## About
|
||||
|
||||
Filebrowser Quantum provides a file managing interface within a specified directory
|
||||
and can be used to upload, delete, preview, rename, and edit your files.
|
||||
It allows the creation of multiple users and each user can have its
|
||||
Filebrowser provides a file managing interface within a specified directory
|
||||
and it can be used to upload, delete, preview, rename and edit your files.
|
||||
It allows the creation of multiple users and each user can have its own
|
||||
directory.
|
||||
|
||||
This repository is a fork of the original [filebrowser](https://github.com/filebrowser/filebrowser)
|
||||
with a collection of changes that make this program work better in terms of
|
||||
aesthetics and performance. Improved search, simplified ui
|
||||
(without removing features) and more secure and up-to-date
|
||||
This repository is a fork, a collection of changes that make this program
|
||||
work better in terms of asthetics and performance. Improved search,
|
||||
simplified ui (without removing features) and more secure and up-to-date
|
||||
build are just a few examples.
|
||||
|
||||
Filebrowser Quantum differs significantly to the original.
|
||||
There are hundreds of thousands of lines changed and they are generally
|
||||
no longer compatible with each other. This has been intentional -- the
|
||||
This Implementation of filebrowser differs significantly to the original.
|
||||
There are hundereds of thousands of lines changed and they are generally
|
||||
no longer compatible with eachother. This has been intentional -- the
|
||||
focus of this fork is on a few key principles:
|
||||
- Simplicity and improved user experience
|
||||
- Improving performance and faster feedback when making changes.
|
||||
- Minimize external dependencies and standard library usage.
|
||||
- Of course -- adding much-needed features.
|
||||
- Efficiency of operations and performance
|
||||
- Minimizing external dependancies and usage of standard libraries.
|
||||
- Of course -- adding much needed features.
|
||||
|
||||
## Look
|
||||
|
||||
One way you can observe the improved user experience is how I changed
|
||||
the UI. The Navbar is simplified to a three-component system :
|
||||
One way you can observe the improved user experience is how I changed the UI.
|
||||
The Navbar is simplified to a three component system :
|
||||
|
||||
1. (Left) The slide-out action panel button
|
||||
2. (Middle) The powerful search bar.
|
||||
3. (Right) The view change toggle.
|
||||
|
||||
All other functions are moved either into the action menu or popup menus.
|
||||
If the action does not depend on context, it will exist in the slide-out
|
||||
action panel. If the action is available based on context, it will show up as
|
||||
a popup menu.
|
||||
|
||||
All other functions are moved either into the action menu or popup menus.
|
||||
If the action is does not depend on context, it will exist in the slide-out action panel.
|
||||
If the action is available based on context, it will showup as a popup menu.
|
||||
|
||||
<p align="center">
|
||||
<img width="800" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/899152cf-3e69-4179-aa82-752af2df3fc6" title="Main Screenshot">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/18c02d03-5c60-4e15-9c32-3cfe058a0c49" title="Main Screenshot">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/75226dc4-9802-46f0-9e3c-e4403d3275da" title="Main Screenshot">
|
||||
|
||||
<img width="500" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/35cdeb3b-ab79-4b04-8001-8f51f6ea06bb" title="Dark mode">
|
||||
<img width="500" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/55fa4f5c-440e-4a97-b711-96139208a163">
|
||||
<img width="500" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/c76f4100-949b-4e17-a3e6-e410fb8ec08f">
|
||||
<img width="500" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/0bde26f3-fa90-411e-bd0b-abaa47506d62">
|
||||
<img width="560" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/71d8f2b8-6fe6-4fdc-8aac-503d08c28d86">
|
||||
</p>
|
||||
|
||||
## Install
|
||||
@@ -84,7 +78,7 @@ Using docker:
|
||||
docker run -it -v /path/to/folder:/srv -p 80:80 gtstef/filebrowser
|
||||
```
|
||||
|
||||
1. docker compose:
|
||||
1. docker-compose:
|
||||
|
||||
- with local storage
|
||||
|
||||
@@ -125,96 +119,43 @@ volumes:
|
||||
|
||||
```
|
||||
|
||||
Not using docker (not recommended), download your binary from releases and run with your custom config file:
|
||||
Not using docker (not recommended) (Must donwload asset with frontend directory next to filebrowser binary)
|
||||
|
||||
```
|
||||
./filebrowser -c <filebrowser.yml or other /path/to/config.yaml>
|
||||
./filebrowser -f <filebrowser.yml or other /path/to/config.yaml>
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration is now done via a single configuration file:
|
||||
`filebrowser.yaml`, here is an example of minimal [configuration
|
||||
file](./backend/filebrowser.yaml).
|
||||
All configuration is now done via a single configuration file: `filebrowser.yaml`, here is an example minimal [configuration file](./backend/filebrowser.yaml).
|
||||
|
||||
View the [Configuration Help Page](./configuration.md) for available
|
||||
configuration options and other help.
|
||||
View the [Configuration Help Page](./configuration.md) for available configuration options and other help.
|
||||
|
||||
|
||||
## Migration from filebrowser/filebrowser
|
||||
|
||||
If you currently use the original opensource filebrowser
|
||||
but want to try using this. I recommend you start fresh without
|
||||
reusing the database, but there are a few things you'll need to do if you
|
||||
must migrate:
|
||||
If you are currently using filebrowser from the filebrowser/filebrowser repo, but want to try using this. I recommend you start fresh without reusing the database, but there are a few things you'll need to do if you must migrate:
|
||||
|
||||
1. Create a configuration file as mentioned above.
|
||||
2. Copy your database file from the original filebrowser to the path of
|
||||
the new one.
|
||||
3. Update the configuration file to use the database (under server in
|
||||
filebrowser.yml)
|
||||
4. If you are using docker, update the docker-compose file or docker run
|
||||
command to use the config file as described in the install section
|
||||
above.
|
||||
5. If you are not using docker, just make sure you run filebrowser -c
|
||||
filebrowser.yml and have a valid filebrowser config.
|
||||
2. Copy your database file from the original filebrowser to the path of the new one.
|
||||
3. Update the configuration file to use the database (under server in filebrowser.yml)
|
||||
4. If you are using docker, update the docker-compose file or docker run command to use the config file as described in the install section above.
|
||||
5. If you are not using docker, just make sure you run filebrowser -f filebrowser.yml and have valid filebrowser config.
|
||||
|
||||
|
||||
The filebrowser Quantum application should run with the same user and rules that
|
||||
you have from the original. But keep in mind the differences that are
|
||||
mentioned at the top of this readme.
|
||||
The filebrowser application should run with the same user and rules that you have from the original. But keep in mind the differences that are mentioned at the top of this readme.
|
||||
|
||||
### background & help
|
||||
|
||||
## Comparison Chart
|
||||
The original project filebrowser/filebrowser used multiple different ways to configure the server.
|
||||
This was confusing and difficult to work with from a user and from a developer's perspective.
|
||||
So I completely redesigned the program to use one single human-readable config file.
|
||||
|
||||
Application Name | <img width="48" src="frontend/public/img/icons/favicon-256x256.png" > Quantum | <img width="48" src="https://github.com/filebrowser/filebrowser/blob/master/frontend/public/img/logo.svg" > Filebrowser | <img width="48" src="https://github.com/mickael-kerjean/filestash/blob/master/public/assets/logo/app_icon.png?raw=true" > Filestash | <img width="48" src="https://avatars.githubusercontent.com/u/19211038?s=200&v=4" > Nextcloud | <img width="48" src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/da/Google_Drive_logo.png/480px-Google_Drive_logo.png" > Google_Drive | <img width="48" src="https://avatars.githubusercontent.com/u/6422152?v=4" > FileRun
|
||||
--- | --- | --- | --- | --- | --- | --- |
|
||||
Filesystem support | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
Linux | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
Windows | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
Mac | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
Self hostable | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
Has Stable Release? | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
S3 support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
webdav support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
ftp support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
Dedicated docs site? | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
Multiple sources at once | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
Docker image size | 22 MB | 31 MB | 240 MB (main image) | 250 MB | ❌ | > 2 GB |
|
||||
Min. Memory Requirements | 128 MB | 128 MB | 128 MB (main image) | 128 MB | ❌ | 4 GB |
|
||||
has standalone binary | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
price | free | free | free | free tier | free tier | $99+ |
|
||||
rich media preview | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
upload files from the web? | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
Advanced Search? | ✅ | ❌ | ❌ | configurable | ✅ | ✅ |
|
||||
Indexed Search? | ✅ | ❌ | ❌ | configurable | ✅ | ✅ |
|
||||
Content-aware search? | ❌ | ❌ | ❌ | configurable | ✅ | ✅ |
|
||||
Custom job support | ❌ | ✅ | ❌ | ✅ | ❌ | ✅ |
|
||||
Multiple users | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
Single sign-on support | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||
LDAP sign-on support | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||
2FA sign-on support | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||
Long-live API key support | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||
Mobile App | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
|
||||
open source? | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
tags support | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ |
|
||||
sharable web links? | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
Event-based notifications | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
Metrics | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
file space quotas | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||
text-based files editor | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
office file support | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||
Themes | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
|
||||
Branding support | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
|
||||
activity log | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||
Comments support | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||
collaboration on same file | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||
trash support | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||
Starred/pinned files | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||
Content preview icons | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ |
|
||||
Plugins support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
Chromecast support | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
I understand many coming from the original fork may notice differences which make using this improved version more difficult. If you notice issues that you believe should be fixed, please open an issue here and it will very likely be addressed with a PR within a few weeks.
|
||||
|
||||
This version of filebrowser is going through a configuration overhaul as mentioned above. Certain features related to rules and commands may not work as they do on the original filebrowser. The purpose of this is to create a more consistent experience where configuration is done via files rather than running commands, so that it's very clear what the current state of the configuration is. When running commands its not clear what the configuration is.
|
||||
|
||||
## Roadmap
|
||||
|
||||
see [Roadmap Page](./roadmap.md)
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
# This code is licensed under the terms of the MIT license https://opensource.org/license/mit
|
||||
# Copyright (c) 2021 Marat Reymers
|
||||
|
||||
## Golden config for golangci-lint v1.59.1
|
||||
#
|
||||
# This is the best config for golangci-lint based on my experience and opinion.
|
||||
# It is very strict, but not extremely strict.
|
||||
# Feel free to adapt and change it for your needs.
|
||||
|
||||
run:
|
||||
# Timeout for analysis, e.g. 30s, 5m.
|
||||
# Default: 1m
|
||||
timeout: 3m
|
||||
|
||||
|
||||
# This file contains only configs which differ from defaults.
|
||||
# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml
|
||||
linters-settings:
|
||||
cyclop:
|
||||
# The maximal code complexity to report.
|
||||
# Default: 10
|
||||
max-complexity: 30
|
||||
# The maximal average package complexity.
|
||||
# If it's higher than 0.0 (float) the check is enabled
|
||||
# Default: 0.0
|
||||
package-average: 10.0
|
||||
|
||||
errcheck:
|
||||
# Report about not checking of errors in type assertions: `a := b.(MyStruct)`.
|
||||
# Such cases aren't reported by default.
|
||||
# Default: false
|
||||
check-type-assertions: true
|
||||
|
||||
exhaustive:
|
||||
# Program elements to check for exhaustiveness.
|
||||
# Default: [ switch ]
|
||||
check:
|
||||
- switch
|
||||
- map
|
||||
|
||||
exhaustruct:
|
||||
# List of regular expressions to exclude struct packages and their names from checks.
|
||||
# Regular expressions must match complete canonical struct package/name/structname.
|
||||
# Default: []
|
||||
exclude:
|
||||
# std libs
|
||||
- "^net/http.Client$"
|
||||
- "^net/http.Cookie$"
|
||||
- "^net/http.Request$"
|
||||
- "^net/http.Response$"
|
||||
- "^net/http.Server$"
|
||||
- "^net/http.Transport$"
|
||||
- "^net/url.URL$"
|
||||
- "^os/exec.Cmd$"
|
||||
- "^reflect.StructField$"
|
||||
# public libs
|
||||
- "^github.com/Shopify/sarama.Config$"
|
||||
- "^github.com/Shopify/sarama.ProducerMessage$"
|
||||
- "^github.com/mitchellh/mapstructure.DecoderConfig$"
|
||||
- "^github.com/prometheus/client_golang/.+Opts$"
|
||||
- "^github.com/spf13/cobra.Command$"
|
||||
- "^github.com/spf13/cobra.CompletionOptions$"
|
||||
- "^github.com/stretchr/testify/mock.Mock$"
|
||||
- "^github.com/testcontainers/testcontainers-go.+Request$"
|
||||
- "^github.com/testcontainers/testcontainers-go.FromDockerfile$"
|
||||
- "^golang.org/x/tools/go/analysis.Analyzer$"
|
||||
- "^google.golang.org/protobuf/.+Options$"
|
||||
- "^gopkg.in/yaml.v3.Node$"
|
||||
|
||||
funlen:
|
||||
# Checks the number of lines in a function.
|
||||
# If lower than 0, disable the check.
|
||||
# Default: 60
|
||||
lines: 100
|
||||
# Checks the number of statements in a function.
|
||||
# If lower than 0, disable the check.
|
||||
# Default: 40
|
||||
statements: 50
|
||||
# Ignore comments when counting lines.
|
||||
# Default false
|
||||
ignore-comments: true
|
||||
|
||||
gocognit:
|
||||
# Minimal code complexity to report.
|
||||
# Default: 30 (but we recommend 10-20)
|
||||
min-complexity: 20
|
||||
|
||||
gocritic:
|
||||
# Settings passed to gocritic.
|
||||
# The settings key is the name of a supported gocritic checker.
|
||||
# The list of supported checkers can be find in https://go-critic.github.io/overview.
|
||||
settings:
|
||||
captLocal:
|
||||
# Whether to restrict checker to params only.
|
||||
# Default: true
|
||||
paramsOnly: false
|
||||
underef:
|
||||
# Whether to skip (*x).method() calls where x is a pointer receiver.
|
||||
# Default: true
|
||||
skipRecvDeref: false
|
||||
|
||||
gomodguard:
|
||||
blocked:
|
||||
# List of blocked modules.
|
||||
# Default: []
|
||||
modules:
|
||||
- github.com/golang/protobuf:
|
||||
recommendations:
|
||||
- google.golang.org/protobuf
|
||||
reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules"
|
||||
- github.com/satori/go.uuid:
|
||||
recommendations:
|
||||
- github.com/google/uuid
|
||||
reason: "satori's package is not maintained"
|
||||
- github.com/gofrs/uuid:
|
||||
recommendations:
|
||||
- github.com/gofrs/uuid/v5
|
||||
reason: "gofrs' package was not go module before v5"
|
||||
|
||||
govet:
|
||||
# Enable all analyzers.
|
||||
# Default: false
|
||||
enable-all: true
|
||||
# Disable analyzers by name.
|
||||
# Run `go tool vet help` to see all analyzers.
|
||||
# Default: []
|
||||
disable:
|
||||
- fieldalignment # too strict
|
||||
# Settings per analyzer.
|
||||
settings:
|
||||
shadow:
|
||||
# Whether to be strict about shadowing; can be noisy.
|
||||
# Default: false
|
||||
strict: false
|
||||
|
||||
inamedparam:
|
||||
# Skips check for interface methods with only a single parameter.
|
||||
# Default: false
|
||||
skip-single-param: true
|
||||
|
||||
mnd:
|
||||
# List of function patterns to exclude from analysis.
|
||||
# Values always ignored: `time.Date`,
|
||||
# `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`,
|
||||
# `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`.
|
||||
# Default: []
|
||||
ignored-functions:
|
||||
- args.Error
|
||||
- flag.Arg
|
||||
- flag.Duration.*
|
||||
- flag.Float.*
|
||||
- flag.Int.*
|
||||
- flag.Uint.*
|
||||
- os.Chmod
|
||||
- os.Mkdir.*
|
||||
- os.OpenFile
|
||||
- os.WriteFile
|
||||
- prometheus.ExponentialBuckets.*
|
||||
- prometheus.LinearBuckets
|
||||
|
||||
nakedret:
|
||||
# Make an issue if func has more lines of code than this setting, and it has naked returns.
|
||||
# Default: 30
|
||||
max-func-lines: 0
|
||||
|
||||
nolintlint:
|
||||
# Exclude following linters from requiring an explanation.
|
||||
# Default: []
|
||||
allow-no-explanation: [ funlen, gocognit, lll ]
|
||||
# Enable to require an explanation of nonzero length after each nolint directive.
|
||||
# Default: false
|
||||
require-explanation: true
|
||||
# Enable to require nolint directives to mention the specific linter being suppressed.
|
||||
# Default: false
|
||||
require-specific: true
|
||||
|
||||
perfsprint:
|
||||
# Optimizes into strings concatenation.
|
||||
# Default: true
|
||||
strconcat: false
|
||||
|
||||
rowserrcheck:
|
||||
# database/sql is always checked
|
||||
# Default: []
|
||||
packages:
|
||||
- github.com/jmoiron/sqlx
|
||||
|
||||
sloglint:
|
||||
# Enforce not using global loggers.
|
||||
# Values:
|
||||
# - "": disabled
|
||||
# - "all": report all global loggers
|
||||
# - "default": report only the default slog logger
|
||||
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global
|
||||
# Default: ""
|
||||
no-global: "all"
|
||||
# Enforce using methods that accept a context.
|
||||
# Values:
|
||||
# - "": disabled
|
||||
# - "all": report all contextless calls
|
||||
# - "scope": report only if a context exists in the scope of the outermost function
|
||||
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#context-only
|
||||
# Default: ""
|
||||
context: "scope"
|
||||
|
||||
tenv:
|
||||
# The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures.
|
||||
# Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked.
|
||||
# Default: false
|
||||
all: true
|
||||
|
||||
|
||||
issues:
|
||||
# Maximum count of issues with the same text.
|
||||
# Set to 0 to disable.
|
||||
# Default: 3
|
||||
max-same-issues: 50
|
||||
|
||||
exclude-rules:
|
||||
- source: "(noinspection|TODO)"
|
||||
linters: [ godot ]
|
||||
- source: "//noinspection"
|
||||
linters: [ gocritic ]
|
||||
- path: "_test\\.go"
|
||||
linters:
|
||||
- bodyclose
|
||||
- dupl
|
||||
- funlen
|
||||
- goconst
|
||||
- gosec
|
||||
- noctx
|
||||
- wrapcheck
|
||||
@@ -1,43 +0,0 @@
|
||||
# .goreleaser.yaml
|
||||
project_name: filebrowser
|
||||
version: 2
|
||||
|
||||
builds:
|
||||
# Build configuration for darwin and linux
|
||||
- id: default
|
||||
ldflags: &ldflags
|
||||
- -s -w -X github.com/gtsteffaniak/filebrowser/version.Version={{ .Version }} -X github.com/gtsteffaniak/filebrowser/version.CommitSHA={{ .ShortCommit }}
|
||||
main: main.go
|
||||
binary: filebrowser
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
hooks:
|
||||
post:
|
||||
- upx {{ .Path }} # Compress the binary with UPX
|
||||
|
||||
# Build configuration for windows without arm
|
||||
- id: windows
|
||||
ldflags: *ldflags
|
||||
main: main.go
|
||||
binary: filebrowser
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
|
||||
archives:
|
||||
- name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
|
||||
format: binary
|
||||
|
||||
checksum:
|
||||
disable: true
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
||||
+13
-43
@@ -13,10 +13,9 @@ import (
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"embed"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/auth"
|
||||
@@ -28,11 +27,6 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
//go:embed dist/*
|
||||
var assets embed.FS
|
||||
|
||||
var nonEmbededFS = os.Getenv("FILEBROWSER_NO_EMBEDED") == "true"
|
||||
|
||||
type dirFS struct {
|
||||
http.Dir
|
||||
}
|
||||
@@ -48,7 +42,6 @@ func init() {
|
||||
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
|
||||
pflag.Parse()
|
||||
log.Println("Initializing with config file:", *configFlag)
|
||||
log.Println("Embeded Frontend:", !nonEmbededFS)
|
||||
settings.Initialize(*configFlag)
|
||||
}
|
||||
|
||||
@@ -63,20 +56,13 @@ var rootCmd = &cobra.Command{
|
||||
log.Fatal("Image resize workers count could not be < 1")
|
||||
}
|
||||
imgSvc := img.New(serverConfig.NumImageProcessors)
|
||||
|
||||
var fileCache diskcache.Interface = diskcache.NewNoOp()
|
||||
cacheDir := "/tmp"
|
||||
var fileCache diskcache.Interface
|
||||
|
||||
// Use file cache if cacheDir is specified
|
||||
if cacheDir != "" {
|
||||
var err error
|
||||
fileCache, err = diskcache.NewFileCache(cacheDir)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create file cache: %v", err)
|
||||
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet,gomnd
|
||||
log.Fatalf("can't make directory %s: %s", cacheDir, err)
|
||||
}
|
||||
} else {
|
||||
// No-op cache if no cacheDir is specified
|
||||
fileCache = diskcache.NewNoOp()
|
||||
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
|
||||
}
|
||||
// initialize indexing and schedule indexing ever n minutes (default 5)
|
||||
go files.InitializeIndex(serverConfig.IndexingInterval, serverConfig.Indexing)
|
||||
@@ -107,31 +93,15 @@ var rootCmd = &cobra.Command{
|
||||
sigc := make(chan os.Signal, 1)
|
||||
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
||||
go cleanupHandler(listener, sigc)
|
||||
if !nonEmbededFS {
|
||||
assetsFs, err := fs.Sub(assets, "dist")
|
||||
if err != nil {
|
||||
log.Fatal("Could not embed frontend. Does backend/cmd/dist exist? Must be built and exist first")
|
||||
}
|
||||
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, &serverConfig, assetsFs)
|
||||
checkErr("fbhttp.NewHandler", err)
|
||||
defer listener.Close()
|
||||
log.Println("Listening on", listener.Addr().String())
|
||||
//nolint: gosec
|
||||
if err := http.Serve(listener, handler); err != nil {
|
||||
log.Fatalf("Could not start server on port %d: %v", serverConfig.Port, err)
|
||||
}
|
||||
} else {
|
||||
assetsFs := dirFS{Dir: http.Dir("frontend/dist")}
|
||||
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, &serverConfig, assetsFs)
|
||||
checkErr("fbhttp.NewHandler", err)
|
||||
defer listener.Close()
|
||||
log.Println("Listening on", listener.Addr().String())
|
||||
//nolint: gosec
|
||||
if err := http.Serve(listener, handler); err != nil {
|
||||
log.Fatalf("Could not start server on port %d: %v", serverConfig.Port, err)
|
||||
}
|
||||
assetsFs := dirFS{Dir: http.Dir("frontend/dist")}
|
||||
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, &serverConfig, assetsFs)
|
||||
checkErr("fbhttp.NewHandler", err)
|
||||
defer listener.Close()
|
||||
log.Println("Listening on", listener.Addr().String())
|
||||
//nolint: gosec
|
||||
if err := http.Serve(listener, handler); err != nil {
|
||||
log.Fatalf("Could not start server on port %d: %v", serverConfig.Port, err)
|
||||
}
|
||||
|
||||
}, pythonConfig{allowNoDB: true}),
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,11 @@ list or set it to 0.`,
|
||||
err = unmarshal(args[0], &list)
|
||||
checkErr("unmarshal", err)
|
||||
|
||||
for _, user := range list {
|
||||
err = user.Clean("")
|
||||
checkErr("Clean", err)
|
||||
}
|
||||
|
||||
if mustGetBool(cmd.Flags(), "replace") {
|
||||
oldUsers, err := d.store.Users.Gets("")
|
||||
checkErr("d.store.Users.Gets", err)
|
||||
|
||||
@@ -10,17 +10,13 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// Cache interface for caching operations
|
||||
type Cache interface {
|
||||
Get(key string) ([]byte, error)
|
||||
Set(key string, value []byte) error
|
||||
}
|
||||
|
||||
// FileCache struct for file-based caching
|
||||
type FileCache struct {
|
||||
dir string
|
||||
fs afero.Fs
|
||||
|
||||
// granular locks
|
||||
scopedLocks struct {
|
||||
sync.Mutex
|
||||
@@ -29,12 +25,10 @@ type FileCache struct {
|
||||
}
|
||||
}
|
||||
|
||||
// NewFileCache creates a new FileCache
|
||||
func NewFileCache(dir string) (*FileCache, error) {
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return nil, fmt.Errorf("can't make directory %s: %v", dir, err)
|
||||
func New(fs afero.Fs, root string) *FileCache {
|
||||
return &FileCache{
|
||||
fs: afero.NewBasePathFs(fs, root),
|
||||
}
|
||||
return &FileCache{dir: dir}, nil
|
||||
}
|
||||
|
||||
func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
|
||||
@@ -43,11 +37,11 @@ func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
|
||||
defer mu.Unlock()
|
||||
|
||||
fileName := f.getFileName(key)
|
||||
if err := os.MkdirAll(filepath.Dir(fileName), 0700); err != nil {
|
||||
if err := f.fs.MkdirAll(filepath.Dir(fileName), 0700); err != nil { //nolint:gomnd
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(fileName, value, 0600); err != nil {
|
||||
if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil { //nolint:gomnd
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -74,15 +68,15 @@ func (f *FileCache) Delete(ctx context.Context, key string) error {
|
||||
defer mu.Unlock()
|
||||
|
||||
fileName := f.getFileName(key)
|
||||
if err := os.Remove(fileName); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
if err := f.fs.Remove(fileName); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileCache) open(key string) (*os.File, bool, error) {
|
||||
func (f *FileCache) open(key string) (afero.File, bool, error) {
|
||||
fileName := f.getFileName(key)
|
||||
file, err := os.Open(fileName)
|
||||
file, err := f.fs.Open(fileName)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, false, nil
|
||||
@@ -112,5 +106,5 @@ func (f *FileCache) getFileName(key string) string {
|
||||
hasher := sha1.New() //nolint:gosec
|
||||
_, _ = hasher.Write([]byte(key))
|
||||
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||
return filepath.Join(f.dir, fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash))
|
||||
return fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash)
|
||||
}
|
||||
@@ -2,10 +2,10 @@ package diskcache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -15,39 +15,35 @@ func TestFileCache(t *testing.T) {
|
||||
key = "key"
|
||||
value = "some text"
|
||||
newValue = "new text"
|
||||
cacheRoot = "cache"
|
||||
cacheRoot = "/cache"
|
||||
cachedFilePath = "a/62/a62f2225bf70bfaccbc7f1ef2a397836717377de"
|
||||
)
|
||||
|
||||
// Create temporary directory for the cache
|
||||
cacheDir, err := os.MkdirTemp("", cacheRoot)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(cacheDir) // Clean up
|
||||
|
||||
cache, err := NewFileCache(cacheDir)
|
||||
require.NoError(t, err)
|
||||
fs := afero.NewMemMapFs()
|
||||
cache := New(fs, "/cache")
|
||||
|
||||
// store new key
|
||||
err = cache.Store(ctx, key, []byte(value))
|
||||
err := cache.Store(ctx, key, []byte(value))
|
||||
require.NoError(t, err)
|
||||
checkValue(t, ctx, cache, filepath.Join(cacheDir, cachedFilePath), key, value)
|
||||
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, value)
|
||||
|
||||
// update existing key
|
||||
err = cache.Store(ctx, key, []byte(newValue))
|
||||
require.NoError(t, err)
|
||||
checkValue(t, ctx, cache, filepath.Join(cacheDir, cachedFilePath), key, newValue)
|
||||
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, newValue)
|
||||
|
||||
// delete key
|
||||
err = cache.Delete(ctx, key)
|
||||
require.NoError(t, err)
|
||||
exists := fileExists(filepath.Join(cacheDir, cachedFilePath))
|
||||
exists, err := afero.Exists(fs, filepath.Join(cacheRoot, cachedFilePath))
|
||||
require.NoError(t, err)
|
||||
require.False(t, exists)
|
||||
}
|
||||
|
||||
func checkValue(t *testing.T, ctx context.Context, cache *FileCache, fileFullPath string, key, wantValue string) {
|
||||
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:golint
|
||||
t.Helper()
|
||||
// check actual file content
|
||||
b, err := os.ReadFile(fileFullPath)
|
||||
b, err := afero.ReadFile(fs, fileFullPath)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, wantValue, string(b))
|
||||
|
||||
@@ -57,11 +53,3 @@ func checkValue(t *testing.T, ctx context.Context, cache *FileCache, fileFullPat
|
||||
require.True(t, ok)
|
||||
require.Equal(t, wantValue, string(b))
|
||||
}
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
info, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
server:
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
root: "./srv"
|
||||
root: "/srv"
|
||||
auth:
|
||||
method: password
|
||||
signup: false
|
||||
|
||||
+73
-171
@@ -6,21 +6,21 @@ import (
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
filepath "path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/rules"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
@@ -33,6 +33,7 @@ var (
|
||||
// FileInfo describes a file.
|
||||
type FileInfo struct {
|
||||
*Listing
|
||||
Fs afero.Fs `json:"-"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
@@ -51,7 +52,8 @@ type FileInfo struct {
|
||||
|
||||
// FileOptions are the options when getting a file info.
|
||||
type FileOptions struct {
|
||||
Path string // realpath
|
||||
Fs afero.Fs
|
||||
Path string
|
||||
Modify bool
|
||||
Expand bool
|
||||
ReadHeader bool
|
||||
@@ -89,7 +91,7 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
|
||||
}
|
||||
if opts.Expand {
|
||||
if file.IsDir {
|
||||
if err = file.readListing(opts.Path, opts.Checker, opts.ReadHeader); err != nil {
|
||||
if err := file.readListing(opts.Path, opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
|
||||
return nil, err
|
||||
}
|
||||
return file, nil
|
||||
@@ -164,37 +166,60 @@ func RefreshFileInfo(opts FileOptions) bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
//_, exists := index.GetFileMetadata(adjustedPath)
|
||||
|
||||
return index.UpdateFileMetadata(adjustedPath, *file)
|
||||
} else {
|
||||
//_, exists := index.GetFileMetadata(adjustedPath)
|
||||
return index.UpdateFileMetadata(adjustedPath, *file)
|
||||
}
|
||||
}
|
||||
|
||||
func stat(path string, opts FileOptions) (*FileInfo, error) {
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file := &FileInfo{
|
||||
Path: opts.Path,
|
||||
Name: info.Name(),
|
||||
ModTime: info.ModTime(),
|
||||
Mode: info.Mode(),
|
||||
Size: info.Size(),
|
||||
Extension: filepath.Ext(info.Name()),
|
||||
Token: opts.Token,
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
file.IsDir = true
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
file.IsSymlink = true
|
||||
targetInfo, err := os.Stat(path)
|
||||
var file *FileInfo
|
||||
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
|
||||
info, _, err := lstaterFs.LstatIfPossible(path)
|
||||
if err == nil {
|
||||
file.Size = targetInfo.Size()
|
||||
file.IsDir = targetInfo.IsDir()
|
||||
file = &FileInfo{
|
||||
Fs: opts.Fs,
|
||||
Path: opts.Path,
|
||||
Name: info.Name(),
|
||||
ModTime: info.ModTime(),
|
||||
Mode: info.Mode(),
|
||||
Size: info.Size(),
|
||||
Extension: filepath.Ext(info.Name()),
|
||||
Token: opts.Token,
|
||||
}
|
||||
if info.IsDir() {
|
||||
file.IsDir = true
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
file.IsSymlink = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if file == nil || file.IsSymlink {
|
||||
info, err := opts.Fs.Stat(opts.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if file != nil && file.IsSymlink {
|
||||
file.Size = info.Size()
|
||||
file.IsDir = info.IsDir()
|
||||
return file, nil
|
||||
}
|
||||
|
||||
file = &FileInfo{
|
||||
Fs: opts.Fs,
|
||||
Path: opts.Path,
|
||||
Name: info.Name(),
|
||||
ModTime: info.ModTime(),
|
||||
Mode: info.Mode(),
|
||||
IsDir: info.IsDir(),
|
||||
Size: info.Size(),
|
||||
Extension: filepath.Ext(info.Name()),
|
||||
Token: opts.Token,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +237,7 @@ func (i *FileInfo) Checksum(algo string) error {
|
||||
i.Checksums = map[string]string{}
|
||||
}
|
||||
|
||||
reader, err := os.Open(i.Path)
|
||||
reader, err := i.Fs.Open(i.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -241,126 +266,23 @@ func (i *FileInfo) Checksum(algo string) error {
|
||||
|
||||
// RealPath gets the real path for the file, resolving symlinks if supported.
|
||||
func (i *FileInfo) RealPath() string {
|
||||
realPath, err := filepath.EvalSymlinks(i.Path)
|
||||
if err == nil {
|
||||
return realPath
|
||||
if realPathFs, ok := i.Fs.(interface {
|
||||
RealPath(name string) (fPath string, err error)
|
||||
}); ok {
|
||||
realPath, err := realPathFs.RealPath(i.Path)
|
||||
if err == nil {
|
||||
return realPath
|
||||
}
|
||||
}
|
||||
|
||||
return i.Path
|
||||
}
|
||||
|
||||
func GetRealPath(relativePath ...string) (string, error) {
|
||||
combined := []string{settings.Config.Server.Root}
|
||||
for _, path := range relativePath {
|
||||
combined = append(combined, strings.TrimPrefix(path, settings.Config.Server.Root))
|
||||
}
|
||||
joinedPath := filepath.Join(combined...)
|
||||
|
||||
// Convert relative path to absolute path
|
||||
absolutePath, err := filepath.Abs(joinedPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !Exists(absolutePath) {
|
||||
return absolutePath, nil // return without error
|
||||
}
|
||||
// Resolve symlinks and get the real path
|
||||
return resolveSymlinks(absolutePath)
|
||||
}
|
||||
|
||||
func DeleteFiles(absPath string, opts FileOptions) error {
|
||||
err := os.RemoveAll(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parentDir := filepath.Dir(absPath)
|
||||
opts.Path = parentDir
|
||||
updated := RefreshFileInfo(opts)
|
||||
if !updated {
|
||||
return errors.ErrEmptyKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteDirectory(opts FileOptions) error {
|
||||
// Ensure the parent directories exist
|
||||
err := os.MkdirAll(opts.Path, 0775)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Path = filepath.Dir(opts.Path)
|
||||
updated := RefreshFileInfo(opts)
|
||||
if !updated {
|
||||
return errors.ErrEmptyKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteFile(opts FileOptions, in io.Reader) error {
|
||||
fmt.Println("writing file", opts.Path)
|
||||
dst := opts.Path
|
||||
parentDir := filepath.Dir(dst)
|
||||
// Split the directory from the destination path
|
||||
dir := filepath.Dir(dst)
|
||||
|
||||
// Create the directory and all necessary parents
|
||||
err := os.MkdirAll(dir, 0775)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Open the file for writing (create if it doesn't exist, truncate if it does)
|
||||
file, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Copy the contents from the reader to the file
|
||||
_, err = io.Copy(file, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("refreshing info for ", parentDir)
|
||||
opts.Path = parentDir
|
||||
updated := RefreshFileInfo(opts)
|
||||
if !updated {
|
||||
return errors.ErrEmptyKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveSymlinks resolves symlinks in the given path
|
||||
func resolveSymlinks(path string) (string, error) {
|
||||
for {
|
||||
// Get the file info
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Check if it's a symlink
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
// Read the symlink target
|
||||
target, err := os.Readlink(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Resolve the target relative to the symlink's directory
|
||||
path = filepath.Join(filepath.Dir(path), target)
|
||||
} else {
|
||||
// Not a symlink, so we are done
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addContent reads and sets content based on the file type.
|
||||
func (i *FileInfo) addContent(path string) error {
|
||||
if !i.IsDir {
|
||||
content, err := os.ReadFile(path)
|
||||
afs := &afero.Afero{Fs: i.Fs}
|
||||
content, err := afs.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -379,9 +301,6 @@ func (i *FileInfo) addContent(path string) error {
|
||||
|
||||
// detectType detects the file type.
|
||||
func (i *FileInfo) detectType(path string, modify, saveContent, readHeader bool) error {
|
||||
if i.IsDir {
|
||||
return nil
|
||||
}
|
||||
if IsNamedPipe(i.Mode) {
|
||||
i.Type = "blob"
|
||||
if saveContent {
|
||||
@@ -426,6 +345,7 @@ func (i *FileInfo) detectType(path string, modify, saveContent, readHeader bool)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if i.Type == "" {
|
||||
i.Type = "blob"
|
||||
if saveContent {
|
||||
@@ -438,15 +358,15 @@ func (i *FileInfo) detectType(path string, modify, saveContent, readHeader bool)
|
||||
|
||||
// readFirstBytes reads the first bytes of the file.
|
||||
func (i *FileInfo) readFirstBytes() []byte {
|
||||
file, err := os.Open(i.Path)
|
||||
reader, err := i.Fs.Open(i.Path)
|
||||
if err != nil {
|
||||
i.Type = "blob"
|
||||
return nil
|
||||
}
|
||||
defer file.Close()
|
||||
defer reader.Close()
|
||||
|
||||
buffer := make([]byte, 512) //nolint:gomnd
|
||||
n, err := file.Read(buffer)
|
||||
n, err := reader.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
i.Type = "blob"
|
||||
return nil
|
||||
@@ -467,8 +387,7 @@ func (i *FileInfo) detectSubtitles(parentDir string) {
|
||||
// Directory must have been deleted, remove it from the index
|
||||
return
|
||||
}
|
||||
defer dir.Close() // Ensure directory handle is closed
|
||||
|
||||
// Read the directory contents
|
||||
files, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -493,13 +412,8 @@ func (i *FileInfo) detectSubtitles(parentDir string) {
|
||||
|
||||
// readListing reads the contents of a directory and fills the listing.
|
||||
func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bool) error {
|
||||
dir, err := os.Open(i.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
files, err := dir.Readdir(-1)
|
||||
afs := &afero.Afero{Fs: i.Fs}
|
||||
dir, err := afs.ReadDir(i.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -511,7 +425,7 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
|
||||
NumFiles: 0,
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
for _, f := range dir {
|
||||
name := f.Name()
|
||||
fPath := filepath.Join(i.Path, name)
|
||||
|
||||
@@ -522,7 +436,7 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
|
||||
isSymlink, isInvalidLink := false, false
|
||||
if IsSymlink(f.Mode()) {
|
||||
isSymlink = true
|
||||
info, err := os.Stat(fPath)
|
||||
info, err := i.Fs.Stat(fPath)
|
||||
if err == nil {
|
||||
f = info
|
||||
} else {
|
||||
@@ -564,7 +478,6 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
|
||||
i.Listing = listing
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsNamedPipe(mode os.FileMode) bool {
|
||||
return mode&os.ModeNamedPipe != 0
|
||||
}
|
||||
@@ -585,14 +498,3 @@ func getMutex(path string) *sync.Mutex {
|
||||
|
||||
return pathMutexes[path]
|
||||
}
|
||||
|
||||
func Exists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
|
||||
if count > maxSearchResults {
|
||||
break
|
||||
}
|
||||
fullName := strings.TrimLeft(pathName+file, "/")
|
||||
fullName := pathName + file
|
||||
fileTypes := map[string]bool{}
|
||||
|
||||
matches, fileType := containsSearchTerm(fullName, searchTerm, *searchOptions, isDir, fileTypes)
|
||||
|
||||
+10
-10
@@ -2,23 +2,23 @@ package fileutils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"path"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// Copy copies a file or folder from one place to another.
|
||||
func Copy(src, dst string) error {
|
||||
src = filepath.Clean(src)
|
||||
if src == "" {
|
||||
func Copy(fs afero.Fs, src, dst string) error {
|
||||
if src = path.Clean("/" + src); src == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
dst = filepath.Clean(dst)
|
||||
if dst == "" {
|
||||
if dst = path.Clean("/" + dst); dst == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
if src == "/" || dst == "/" {
|
||||
// Prohibit copying from or to the root directory.
|
||||
// Prohibit copying from or to the virtual root directory.
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
@@ -26,14 +26,14 @@ func Copy(src, dst string) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
info, err := os.Stat(src)
|
||||
info, err := fs.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return CopyDir(src, dst)
|
||||
return CopyDir(fs, src, dst)
|
||||
}
|
||||
|
||||
return CopyFile(src, dst)
|
||||
return CopyFile(fs, src, dst)
|
||||
}
|
||||
+10
-15
@@ -2,32 +2,27 @@ package fileutils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// CopyDir copies a directory from source to dest and all
|
||||
// of its sub-directories. It doesn't stop if it finds an error
|
||||
// during the copy. Returns an error if any.
|
||||
func CopyDir(source, dest string) error {
|
||||
func CopyDir(fs afero.Fs, source, dest string) error {
|
||||
// Get properties of source.
|
||||
srcinfo, err := os.Stat(source)
|
||||
srcinfo, err := fs.Stat(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the destination directory.
|
||||
err = os.MkdirAll(dest, srcinfo.Mode())
|
||||
err = fs.MkdirAll(dest, srcinfo.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dir, err := os.Open(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
dir, _ := fs.Open(source)
|
||||
obs, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -36,18 +31,18 @@ func CopyDir(source, dest string) error {
|
||||
var errs []error
|
||||
|
||||
for _, obj := range obs {
|
||||
fsource := filepath.Join(source, obj.Name())
|
||||
fdest := filepath.Join(dest, obj.Name())
|
||||
fsource := source + "/" + obj.Name()
|
||||
fdest := dest + "/" + obj.Name()
|
||||
|
||||
if obj.IsDir() {
|
||||
// Create sub-directories, recursively.
|
||||
err = CopyDir(fsource, fdest)
|
||||
err = CopyDir(fs, fsource, fdest)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
} else {
|
||||
// Perform the file copy.
|
||||
err = CopyFile(fsource, fdest)
|
||||
err = CopyFile(fs, fsource, fdest)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
+38
-24
@@ -5,22 +5,24 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// MoveFile moves a file from src to dst.
|
||||
// By default, the rename system call is used. If src and dst point to different volumes,
|
||||
// the file copy is used as a fallback.
|
||||
func MoveFile(src, dst string) error {
|
||||
if os.Rename(src, dst) == nil {
|
||||
// MoveFile moves file from src to dst.
|
||||
// By default the rename filesystem system call is used. If src and dst point to different volumes
|
||||
// the file copy is used as a fallback
|
||||
func MoveFile(fs afero.Fs, src, dst string) error {
|
||||
if fs.Rename(src, dst) == nil {
|
||||
return nil
|
||||
}
|
||||
// fallback
|
||||
err := CopyFile(src, dst)
|
||||
err := CopyFile(fs, src, dst)
|
||||
if err != nil {
|
||||
_ = os.Remove(dst)
|
||||
_ = fs.Remove(dst)
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(src); err != nil {
|
||||
if err := fs.Remove(src); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -28,22 +30,23 @@ func MoveFile(src, dst string) error {
|
||||
|
||||
// CopyFile copies a file from source to dest and returns
|
||||
// an error if any.
|
||||
func CopyFile(source, dest string) error {
|
||||
func CopyFile(fs afero.Fs, source, dest string) error {
|
||||
// Open the source file.
|
||||
src, err := os.Open(source)
|
||||
src, err := fs.Open(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// Makes the directory needed to create the dst file.
|
||||
err = os.MkdirAll(filepath.Dir(dest), 0775) //nolint:gomnd
|
||||
// Makes the directory needed to create the dst
|
||||
// file.
|
||||
err = fs.MkdirAll(filepath.Dir(dest), 0666) //nolint:gomnd
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the destination file.
|
||||
dst, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) //nolint:gomnd
|
||||
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) //nolint:gomnd
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -55,12 +58,12 @@ func CopyFile(source, dest string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy the mode.
|
||||
info, err := os.Stat(source)
|
||||
// Copy the mode
|
||||
info, err := fs.Stat(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Chmod(dest, info.Mode())
|
||||
err = fs.Chmod(dest, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -68,7 +71,7 @@ func CopyFile(source, dest string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommonPrefix returns the common directory path of provided files.
|
||||
// CommonPrefix returns common directory path of provided files
|
||||
func CommonPrefix(sep byte, paths ...string) string {
|
||||
// Handle special cases.
|
||||
switch len(paths) {
|
||||
@@ -78,19 +81,30 @@ func CommonPrefix(sep byte, paths ...string) string {
|
||||
return path.Clean(paths[0])
|
||||
}
|
||||
|
||||
// Treat string as []byte, not []rune as is often done in Go.
|
||||
// Note, we treat string as []byte, not []rune as is often
|
||||
// done in Go. (And sep as byte, not rune). This is because
|
||||
// most/all supported OS' treat paths as string of non-zero
|
||||
// bytes. A filename may be displayed as a sequence of Unicode
|
||||
// runes (typically encoded as UTF-8) but paths are
|
||||
// not required to be valid UTF-8 or in any normalized form
|
||||
// (e.g. "é" (U+00C9) and "é" (U+0065,U+0301) are different
|
||||
// file names.
|
||||
c := []byte(path.Clean(paths[0]))
|
||||
|
||||
// Add a trailing sep to handle the case where the common prefix directory
|
||||
// is included in the path list.
|
||||
// We add a trailing sep to handle the case where the
|
||||
// common prefix directory is included in the path list
|
||||
// (e.g. /home/user1, /home/user1/foo, /home/user1/bar).
|
||||
// path.Clean will have cleaned off trailing / separators with
|
||||
// the exception of the root directory, "/" (in which case we
|
||||
// make it "//", but this will get fixed up to "/" bellow).
|
||||
c = append(c, sep)
|
||||
|
||||
// Ignore the first path since it's already in c.
|
||||
// Ignore the first path since it's already in c
|
||||
for _, v := range paths[1:] {
|
||||
// Clean up each path before testing it.
|
||||
// Clean up each path before testing it
|
||||
v = path.Clean(v) + string(sep)
|
||||
|
||||
// Find the first non-common byte and truncate c.
|
||||
// Find the first non-common byte and truncate c
|
||||
if len(v) < len(c) {
|
||||
c = c[:len(v)]
|
||||
}
|
||||
@@ -102,7 +116,7 @@ func CommonPrefix(sep byte, paths ...string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove trailing non-separator characters and the final separator.
|
||||
// Remove trailing non-separator characters and the final separator
|
||||
for i := len(c) - 1; i >= 0; i-- {
|
||||
if c[i] == sep {
|
||||
c = c[:i]
|
||||
|
||||
+9
-9
@@ -1,13 +1,13 @@
|
||||
module github.com/gtsteffaniak/filebrowser
|
||||
|
||||
go 1.22.5
|
||||
go 1.22.0
|
||||
|
||||
require (
|
||||
github.com/asdine/storm/v3 v3.2.1
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dsoprea/go-exif/v3 v3.0.1
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568
|
||||
github.com/goccy/go-yaml v1.12.0
|
||||
github.com/goccy/go-yaml v1.11.3
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
@@ -15,13 +15,13 @@ require (
|
||||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
github.com/spf13/afero v1.11.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
|
||||
golang.org/x/crypto v0.26.0
|
||||
golang.org/x/image v0.19.0
|
||||
golang.org/x/text v0.17.0
|
||||
golang.org/x/crypto v0.24.0
|
||||
golang.org/x/image v0.17.0
|
||||
golang.org/x/text v0.16.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -48,9 +48,9 @@ require (
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.3.10 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
+17
-48
@@ -2,13 +2,12 @@ github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
|
||||
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
|
||||
github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc=
|
||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
|
||||
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
@@ -33,8 +32,6 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003142440-7a1927d49d9d/go.mod h1:LV
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU=
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw=
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
|
||||
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
@@ -42,11 +39,9 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
@@ -56,13 +51,12 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM=
|
||||
github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
|
||||
github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I=
|
||||
github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
|
||||
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
|
||||
@@ -71,7 +65,6 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
|
||||
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
@@ -85,12 +78,10 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU=
|
||||
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
@@ -103,29 +94,21 @@ github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/marusama/semaphore/v2 v2.5.0 h1:o/1QJD9DBYOWRnDhPwDVAXQn6mQYD0gZaS1Tpx6DJGM=
|
||||
github.com/marusama/semaphore/v2 v2.5.0/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
|
||||
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
||||
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
|
||||
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
|
||||
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM=
|
||||
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
@@ -133,8 +116,8 @@ github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/i
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
@@ -143,7 +126,6 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
|
||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
@@ -153,20 +135,15 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofm
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
|
||||
golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
|
||||
golang.org/x/image v0.17.0 h1:nTRVVdajgB8zCMZVsViyzhnMKPwYeroEERRC64JuLco=
|
||||
golang.org/x/image v0.17.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -174,15 +151,13 @@ golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -192,24 +167,18 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk=
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
@@ -47,6 +46,7 @@ func previewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, re
|
||||
}
|
||||
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
Path: "/" + vars["path"],
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: true,
|
||||
@@ -110,7 +110,7 @@ func handleImagePreview(
|
||||
|
||||
func createPreview(imgSvc ImgService, fileCache FileCache,
|
||||
file *files.FileInfo, previewSize PreviewSize) ([]byte, error) {
|
||||
fd, err := os.Open(file.Path)
|
||||
fd, err := file.Fs.Open(file.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+22
-20
@@ -2,12 +2,13 @@ package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
@@ -19,27 +20,33 @@ import (
|
||||
var withHashFile = func(fn handleFunc) handleFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
id, path := ifPathWithName(r)
|
||||
fmt.Println(id, path)
|
||||
link, err := d.store.Share.GetByHash(id)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
if link.Hash != "" {
|
||||
var status int
|
||||
status, err = authenticateShareRequest(r, link) // Assign to the existing `err` variable
|
||||
if err != nil || status != 0 {
|
||||
status, err := authenticateShareRequest(r, link)
|
||||
if status != 0 || err != nil {
|
||||
return status, err
|
||||
}
|
||||
}
|
||||
d.user = &users.PublicUser
|
||||
realPath, err := files.GetRealPath(d.user.Scope, link.Path, path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
if path == "/" {
|
||||
path = link.Path
|
||||
} else if strings.HasPrefix("/"+path, link.Path) {
|
||||
path = "/" + path
|
||||
} else {
|
||||
path = link.Path + "/" + path
|
||||
}
|
||||
sharePath := settings.Config.Server.Root + path
|
||||
lastComponent := filepath.Base(sharePath)
|
||||
basePath := filepath.Dir(sharePath)
|
||||
fsPath := afero.NewBasePathFs(afero.NewOsFs(), basePath)
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: realPath,
|
||||
Fs: fsPath,
|
||||
Path: lastComponent,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: true,
|
||||
Expand: false,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
Checker: d,
|
||||
Token: link.Token,
|
||||
@@ -56,15 +63,14 @@ func ifPathWithName(r *http.Request) (id, filePath string) {
|
||||
pathElements := strings.Split(r.URL.Path, "/")
|
||||
id = pathElements[0]
|
||||
allButFirst := path.Join(pathElements[1:]...)
|
||||
if len(pathElements) == 1 {
|
||||
allButFirst = "/"
|
||||
}
|
||||
return id, allButFirst
|
||||
}
|
||||
|
||||
var publicShareHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
file, ok := d.raw.(*files.FileInfo)
|
||||
if !ok {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type *files.FileInfo")
|
||||
}
|
||||
|
||||
file := d.raw.(*files.FileInfo)
|
||||
file.Path = strings.TrimPrefix(file.Path, settings.Config.Server.Root)
|
||||
if file.IsDir {
|
||||
return renderJSON(w, r, file)
|
||||
@@ -80,11 +86,7 @@ var publicUserGetHandler = func(w http.ResponseWriter, r *http.Request, d *data)
|
||||
}
|
||||
|
||||
var publicDlHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
file, ok := d.raw.(*files.FileInfo)
|
||||
if !ok {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type *files.FileInfo")
|
||||
}
|
||||
|
||||
file := d.raw.(*files.FileInfo)
|
||||
if !file.IsDir {
|
||||
return rawFileHandler(w, r, file)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/share"
|
||||
@@ -91,6 +92,7 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
|
||||
|
||||
storage.Users = &customFSUser{
|
||||
Store: storage.Users,
|
||||
fs: &afero.MemMapFs{},
|
||||
}
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
@@ -120,6 +122,7 @@ func newHTTPRequest(t *testing.T, requestModifiers ...func(*http.Request)) *http
|
||||
|
||||
type customFSUser struct {
|
||||
users.Store
|
||||
fs afero.Fs
|
||||
}
|
||||
|
||||
func (cu *customFSUser) Get(baseScope string, id interface{}) (*users.User, error) {
|
||||
@@ -127,5 +130,7 @@ func (cu *customFSUser) Get(baseScope string, id interface{}) (*users.User, erro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.Fs = cu.fs
|
||||
|
||||
return user, nil
|
||||
}
|
||||
+6
-9
@@ -5,7 +5,6 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
gopath "path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -81,12 +80,10 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
|
||||
if !d.user.Perm.Download {
|
||||
return http.StatusAccepted, nil
|
||||
}
|
||||
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: realPath,
|
||||
Fs: d.user.Fs,
|
||||
Path: r.URL.Path,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
@@ -111,7 +108,7 @@ func addFile(ar archiver.Writer, d *data, path, commonPath string) error {
|
||||
if !d.Check(path) {
|
||||
return nil
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
info, err := d.user.Fs.Stat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -120,7 +117,7 @@ func addFile(ar archiver.Writer, d *data, path, commonPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
file, err := d.user.Fs.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -201,7 +198,7 @@ func rawDirHandler(w http.ResponseWriter, r *http.Request, d *data, file *files.
|
||||
}
|
||||
|
||||
func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo) (int, error) {
|
||||
fd, err := os.Open(file.Path)
|
||||
fd, err := file.Fs.Open(file.Path)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
+122
-63
@@ -3,6 +3,7 @@ package http
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
@@ -18,13 +20,9 @@ import (
|
||||
)
|
||||
|
||||
var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
if err != nil {
|
||||
fmt.Println("unable to get real path", d.user.Scope, r.URL.Path)
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: realPath,
|
||||
Fs: d.user.Fs,
|
||||
Path: r.URL.Path,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: true,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
@@ -55,18 +53,15 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
|
||||
if r.URL.Path == "/" || !d.user.Perm.Delete {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
fileOpts := files.FileOptions{
|
||||
Path: realPath,
|
||||
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
Path: r.URL.Path,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
Checker: d,
|
||||
}
|
||||
file, err := files.FileInfoFaster(fileOpts)
|
||||
})
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
@@ -77,10 +72,14 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
err = files.DeleteFiles(realPath, fileOpts)
|
||||
err = d.RunHook(func() error {
|
||||
return d.user.Fs.RemoveAll(r.URL.Path)
|
||||
}, "delete", r.URL.Path, "", d.user)
|
||||
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
})
|
||||
}
|
||||
@@ -90,27 +89,21 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
|
||||
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
|
||||
// Directories creation on POST.
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
err := d.user.Fs.MkdirAll(r.URL.Path, 0775) //nolint:gomnd
|
||||
return errToStatus(err), err
|
||||
}
|
||||
fileOpts := files.FileOptions{
|
||||
Path: realPath,
|
||||
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
Path: r.URL.Path,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
Checker: d,
|
||||
}
|
||||
// Directories creation on POST.
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
err = files.WriteDirectory(fileOpts) // Assign to the existing `err` variable
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
file, err := files.FileInfoFaster(fileOpts)
|
||||
})
|
||||
if err == nil {
|
||||
if r.URL.Query().Get("override") != "true" {
|
||||
return http.StatusConflict, nil
|
||||
@@ -126,7 +119,22 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
}
|
||||
err = files.WriteFile(fileOpts, r.Body)
|
||||
|
||||
err = d.RunHook(func() error {
|
||||
info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body)
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
|
||||
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
|
||||
w.Header().Set("ETag", etag)
|
||||
return nil
|
||||
}, "upload", r.URL.Path, "", d.user)
|
||||
|
||||
if err != nil {
|
||||
_ = d.user.Fs.RemoveAll(r.URL.Path)
|
||||
}
|
||||
|
||||
return errToStatus(err), err
|
||||
})
|
||||
}
|
||||
@@ -141,22 +149,27 @@ var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
exists, err := afero.Exists(d.user.Fs, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
fileOpts := files.FileOptions{
|
||||
Path: realPath,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
Checker: d,
|
||||
if !exists {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
err = files.WriteFile(fileOpts, r.Body)
|
||||
|
||||
err = d.RunHook(func() error {
|
||||
info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body)
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
|
||||
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
|
||||
w.Header().Set("ETag", etag)
|
||||
return nil
|
||||
}, "save", r.URL.Path, "", d.user)
|
||||
return errToStatus(err), err
|
||||
})
|
||||
|
||||
// TODO fix and verify this function still works in tests
|
||||
func resourcePatchHandler(fileCache FileCache) handleFunc {
|
||||
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
src := r.URL.Path
|
||||
@@ -172,22 +185,29 @@ func resourcePatchHandler(fileCache FileCache) handleFunc {
|
||||
if dst == "/" || src == "/" {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
err = checkParent(src, dst)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
override := r.URL.Query().Get("override") == "true"
|
||||
rename := r.URL.Query().Get("rename") == "true"
|
||||
if !override && !rename {
|
||||
if _, err = os.Stat(dst); err == nil {
|
||||
if _, err = d.user.Fs.Stat(dst); err == nil {
|
||||
return http.StatusConflict, nil
|
||||
}
|
||||
}
|
||||
if rename {
|
||||
dst = addVersionSuffix(dst)
|
||||
dst = addVersionSuffix(dst, d.user.Fs)
|
||||
}
|
||||
|
||||
// Permission for overwriting the file
|
||||
if override && !d.user.Perm.Modify {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
err = d.RunHook(func() error {
|
||||
fmt.Println("hook", src, dst)
|
||||
return patchAction(r.Context(), action, src, dst, d, fileCache)
|
||||
}, action, src, dst, d.user)
|
||||
|
||||
@@ -195,22 +215,69 @@ func resourcePatchHandler(fileCache FileCache) handleFunc {
|
||||
})
|
||||
}
|
||||
|
||||
func addVersionSuffix(source string) string {
|
||||
func checkParent(src, dst string) error {
|
||||
rel, err := filepath.Rel(src, dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rel = filepath.ToSlash(rel)
|
||||
if !strings.HasPrefix(rel, "../") && rel != ".." && rel != "." {
|
||||
return errors.ErrSourceIsParent
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addVersionSuffix(source string, fs afero.Fs) string {
|
||||
counter := 1
|
||||
dir, name := path.Split(source)
|
||||
ext := filepath.Ext(name)
|
||||
base := strings.TrimSuffix(name, ext)
|
||||
|
||||
for {
|
||||
if _, err := os.Stat(source); err != nil {
|
||||
if _, err := fs.Stat(source); err != nil {
|
||||
break
|
||||
}
|
||||
renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext)
|
||||
source = path.Join(dir, renamed)
|
||||
counter++
|
||||
}
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
func writeFile(fs afero.Fs, dst string, in io.Reader) (os.FileInfo, error) {
|
||||
dir, _ := path.Split(dst)
|
||||
err := fs.MkdirAll(dir, 0775) //nolint:gomnd
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) //nolint:gomnd
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Gets the info about the file.
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//files.RefreshFileInfo(files.FileOptions{
|
||||
// Fs: info,
|
||||
//})
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func delThumbs(ctx context.Context, fileCache FileCache, file *files.FileInfo) error {
|
||||
for _, previewSizeName := range PreviewSizeNames() {
|
||||
size, _ := ParsePreviewSize(previewSizeName)
|
||||
@@ -230,23 +297,17 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
|
||||
return errors.ErrPermissionDenied
|
||||
}
|
||||
|
||||
return fileutils.Copy(src, dst)
|
||||
return fileutils.Copy(d.user.Fs, src, dst)
|
||||
case "rename":
|
||||
if !d.user.Perm.Rename {
|
||||
return errors.ErrPermissionDenied
|
||||
}
|
||||
src = path.Clean("/" + src)
|
||||
dst = path.Clean("/" + dst)
|
||||
realDest, err := files.GetRealPath(d.user.Scope, dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
realSrc, err := files.GetRealPath(d.user.Scope, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: realSrc,
|
||||
Fs: d.user.Fs,
|
||||
Path: src,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: false,
|
||||
@@ -262,7 +323,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
|
||||
return err
|
||||
}
|
||||
|
||||
return fileutils.MoveFile(realSrc, realDest)
|
||||
return fileutils.MoveFile(d.user.Fs, src, dst)
|
||||
default:
|
||||
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
|
||||
}
|
||||
@@ -274,12 +335,9 @@ type DiskUsageResponse struct {
|
||||
}
|
||||
|
||||
var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: realPath,
|
||||
Fs: d.user.Fs,
|
||||
Path: r.URL.Path,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: false,
|
||||
@@ -295,6 +353,7 @@ var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (
|
||||
Used: 0,
|
||||
})
|
||||
}
|
||||
|
||||
usage, err := disk.UsageWithContext(r.Context(), fPath)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
|
||||
@@ -15,17 +15,20 @@ type settingsData struct {
|
||||
Defaults settings.UserDefaults `json:"defaults"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
Frontend settings.Frontend `json:"frontend"`
|
||||
Shell []string `json:"shell"`
|
||||
Commands map[string][]string `json:"commands"`
|
||||
}
|
||||
|
||||
var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
data := &settingsData{
|
||||
Signup: d.settings.Auth.Signup,
|
||||
CreateUserDir: d.settings.Server.CreateUserDir,
|
||||
UserHomeBasePath: d.settings.Server.UserHomeBasePath,
|
||||
Signup: settings.Config.Auth.Signup,
|
||||
CreateUserDir: settings.Config.Server.CreateUserDir,
|
||||
UserHomeBasePath: settings.Config.Server.UserHomeBasePath,
|
||||
Defaults: d.settings.UserDefaults,
|
||||
Rules: d.settings.Rules,
|
||||
Frontend: d.settings.Frontend,
|
||||
Shell: d.settings.Shell,
|
||||
Commands: d.settings.Commands,
|
||||
}
|
||||
|
||||
return renderJSON(w, r, data)
|
||||
@@ -43,7 +46,8 @@ var settingsPutHandler = withAdmin(func(w http.ResponseWriter, r *http.Request,
|
||||
d.settings.UserDefaults = req.Defaults
|
||||
d.settings.Rules = req.Rules
|
||||
d.settings.Frontend = req.Frontend
|
||||
d.settings.Auth.Signup = req.Signup
|
||||
d.settings.Shell = req.Shell
|
||||
d.settings.Commands = req.Commands
|
||||
err = d.store.Settings.Save(d.settings)
|
||||
return errToStatus(err), err
|
||||
})
|
||||
@@ -34,7 +34,6 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
|
||||
"Color": d.settings.Frontend.Color,
|
||||
"BaseURL": d.server.BaseURL,
|
||||
"Version": version.Version,
|
||||
"CommitSHA": version.CommitSHA,
|
||||
"StaticURL": path.Join(d.server.BaseURL, "/static"),
|
||||
"Signup": settings.Config.Auth.Signup,
|
||||
"NoAuth": d.settings.Auth.Method == "noauth",
|
||||
@@ -65,12 +64,7 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
auther, ok := raw.(*auth.JSONAuth)
|
||||
if !ok {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type *auth.JSONAuth")
|
||||
}
|
||||
|
||||
auther := raw.(*auth.JSONAuth)
|
||||
if auther.ReCaptcha != nil {
|
||||
data["ReCaptcha"] = auther.ReCaptcha.Key != "" && auther.ReCaptcha.Secret != ""
|
||||
data["ReCaptchaHost"] = auther.ReCaptcha.Host
|
||||
|
||||
+20
-24
@@ -2,10 +2,8 @@ package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
@@ -14,7 +12,6 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
@@ -138,11 +135,7 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
|
||||
}
|
||||
req.Data.Scope = userHome
|
||||
log.Printf("user: %s, home dir: [%s].", req.Data.Username, userHome)
|
||||
_, err = files.GetRealPath(d.server.Root, req.Data.Scope)
|
||||
if err != nil {
|
||||
fmt.Println("user path is not valid", req.Data.Scope)
|
||||
return http.StatusBadRequest, nil
|
||||
}
|
||||
|
||||
err = d.store.Users.Save(req.Data)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
@@ -161,29 +154,31 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
|
||||
if req.Data.ID != d.raw.(uint) {
|
||||
return http.StatusBadRequest, nil
|
||||
}
|
||||
_, err = files.GetRealPath(d.server.Root, req.Data.Scope)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, nil
|
||||
}
|
||||
|
||||
if len(req.Which) == 0 || req.Which[0] == "all" {
|
||||
if len(req.Which) == 0 || (len(req.Which) == 1 && req.Which[0] == "all") {
|
||||
if !d.user.Perm.Admin {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
if req.Data.Password != "" {
|
||||
req.Data.Password, err = users.HashPwd(req.Data.Password)
|
||||
} else {
|
||||
var suser *users.User
|
||||
suser, err = d.store.Users.Get(d.server.Root, d.raw.(uint))
|
||||
req.Data.Password = suser.Password
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
req.Which = []string{}
|
||||
v := reflect.ValueOf(req.Data)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
t := v.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if field.Name != "Password" && field.Name != "Fs" {
|
||||
req.Which = append(req.Which, field.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range req.Which {
|
||||
v = cases.Title(language.English, cases.NoLower).String(v)
|
||||
req.Which[k] = v
|
||||
|
||||
if v == "Password" {
|
||||
if !d.user.Perm.Admin && d.user.LockPassword {
|
||||
return http.StatusForbidden, nil
|
||||
@@ -200,6 +195,7 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = d.store.Users.Update(req.Data, req.Which...)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
@@ -20,8 +19,8 @@ type Runner struct {
|
||||
|
||||
// RunHook runs the hooks for the before and after event.
|
||||
func (r *Runner) RunHook(fn func() error, evt, path, dst string, user *users.User) error {
|
||||
path, _ = files.GetRealPath(user.Scope, path)
|
||||
dst, _ = files.GetRealPath(user.Scope, dst)
|
||||
path = user.FullPath(path)
|
||||
dst = user.FullPath(dst)
|
||||
|
||||
if r.Enabled {
|
||||
if val, ok := r.Commands["before_"+evt]; ok {
|
||||
|
||||
@@ -71,7 +71,6 @@ func setDefaults() Settings {
|
||||
},
|
||||
},
|
||||
UserDefaults: UserDefaults{
|
||||
StickySidebar: true,
|
||||
Scope: ".",
|
||||
LockPassword: false,
|
||||
HideDotfiles: true,
|
||||
@@ -93,7 +92,6 @@ func setDefaults() Settings {
|
||||
|
||||
// Apply applies the default options to a user.
|
||||
func (d *UserDefaults) Apply(u *users.User) {
|
||||
u.StickySidebar = d.StickySidebar
|
||||
u.DisableSettings = d.DisableSettings
|
||||
u.DarkMode = d.DarkMode
|
||||
u.Scope = d.Scope
|
||||
|
||||
@@ -6,14 +6,16 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
var (
|
||||
invalidFilenameChars = regexp.MustCompile(`[^0-9A-Za-z@_\-.]`)
|
||||
dashes = regexp.MustCompile(`[\-]+`)
|
||||
|
||||
dashes = regexp.MustCompile(`[\-]+`)
|
||||
)
|
||||
|
||||
// MakeUserDir makes the user directory according to settings.
|
||||
@@ -30,8 +32,8 @@ func (s *Settings) MakeUserDir(username, userScope, serverRoot string) (string,
|
||||
|
||||
userScope = path.Join("/", userScope)
|
||||
|
||||
fullPath := filepath.Join(serverRoot, userScope)
|
||||
if err := os.MkdirAll(fullPath, os.ModePerm); err != nil {
|
||||
fs := afero.NewBasePathFs(afero.NewOsFs(), serverRoot)
|
||||
if err := fs.MkdirAll(userScope, os.ModePerm); err != nil {
|
||||
return "", fmt.Errorf("failed to create user home dir: [%s]: %w", userScope, err)
|
||||
}
|
||||
return userScope, nil
|
||||
|
||||
@@ -65,6 +65,10 @@ func (s *Storage) Save(set *Settings) error {
|
||||
set.Rules = []rules.Rule{}
|
||||
}
|
||||
|
||||
if set.Shell == nil {
|
||||
set.Shell = []string{}
|
||||
}
|
||||
|
||||
if set.Commands == nil {
|
||||
set.Commands = map[string][]string{}
|
||||
}
|
||||
|
||||
@@ -68,14 +68,12 @@ type Frontend struct {
|
||||
// UserDefaults is a type that holds the default values
|
||||
// for some fields on User.
|
||||
type UserDefaults struct {
|
||||
StickySidebar bool `json:"stickySidebar"`
|
||||
DarkMode bool `json:"darkMode"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
DisableSettings bool `json:"disableSettings,omitempty"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
GallerySize int `json:"gallerySize"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
Sorting struct {
|
||||
|
||||
@@ -55,6 +55,7 @@ func (st usersBackend) Update(user *users.User, fields ...string) error {
|
||||
if len(fields) == 0 {
|
||||
return st.Save(user)
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
userField := reflect.ValueOf(user).Elem().FieldByName(field)
|
||||
if !userField.IsValid() {
|
||||
@@ -62,9 +63,10 @@ func (st usersBackend) Update(user *users.User, fields ...string) error {
|
||||
}
|
||||
val := userField.Interface()
|
||||
if err := st.db.UpdateField(user, field, val); err != nil {
|
||||
return fmt.Errorf("Error updating user field: %s, error: %v", field, err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,9 @@ func (s *Storage) Get(baseScope string, id interface{}) (user *User, err error)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := user.Clean(baseScope); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
|
||||
@@ -63,12 +66,23 @@ func (s *Storage) Gets(baseScope string) ([]*User, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range users {
|
||||
if err := user.Clean(baseScope); err != nil { //nolint:govet
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return users, err
|
||||
}
|
||||
|
||||
// Update updates a user in the database.
|
||||
func (s *Storage) Update(user *User, fields ...string) error {
|
||||
err := s.back.Update(user, fields...)
|
||||
err := user.Clean("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.back.Update(user, fields...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -124,6 +138,10 @@ func (s *Storage) DeleteRule(userID string, ruleID string) error {
|
||||
// Save saves the user in a storage.
|
||||
func (s *Storage) Save(user *User) error {
|
||||
log.Println("Saving new user:", user.Username)
|
||||
if err := user.Clean(""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.back.Save(user)
|
||||
}
|
||||
|
||||
|
||||
+25
-2
@@ -1,8 +1,11 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/rules"
|
||||
)
|
||||
|
||||
@@ -25,7 +28,6 @@ type Sorting struct {
|
||||
|
||||
// User describes a user.
|
||||
type User struct {
|
||||
StickySidebar bool `json:"stickySidebar"`
|
||||
DarkMode bool `json:"darkMode"`
|
||||
DisableSettings bool `json:"disableSettings"`
|
||||
ID uint `storm:"id,increment" json:"id"`
|
||||
@@ -39,10 +41,10 @@ type User struct {
|
||||
Perm Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
Sorting Sorting `json:"sorting"`
|
||||
Fs afero.Fs `json:"-" yaml:"-"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
GallerySize int `json:"gallerySize"`
|
||||
}
|
||||
|
||||
var PublicUser = User{
|
||||
@@ -51,6 +53,7 @@ var PublicUser = User{
|
||||
Scope: "./",
|
||||
ViewMode: "normal",
|
||||
LockPassword: true,
|
||||
Fs: afero.NewMemMapFs(),
|
||||
Perm: Permissions{
|
||||
Create: false,
|
||||
Rename: false,
|
||||
@@ -67,6 +70,26 @@ func (u *User) GetRules() []rules.Rule {
|
||||
return u.Rules
|
||||
}
|
||||
|
||||
// Clean cleans up a user and verifies if all its fields
|
||||
// are alright to be saved.
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (u *User) Clean(baseScope string) error {
|
||||
|
||||
if u.Fs == nil {
|
||||
scope := u.Scope
|
||||
scope = filepath.Join(baseScope, filepath.Join("/", scope)) //nolint:gocritic
|
||||
u.Fs = afero.NewBasePathFs(afero.NewOsFs(), scope)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FullPath gets the full path for a user's relative path.
|
||||
func (u *User) FullPath(path string) string {
|
||||
return afero.FullBaseFsPath(u.Fs.(*afero.BasePathFs), path)
|
||||
}
|
||||
|
||||
// CanExecute checks if an user can execute a specific command.
|
||||
func (u *User) CanExecute(command string) bool {
|
||||
if !u.Perm.Execute {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package version
|
||||
|
||||
var (
|
||||
// Dynamically updated during build via build args
|
||||
Version = "untracked"
|
||||
CommitSHA = "untracked"
|
||||
// Version is the current File Browser version.
|
||||
Version = "(0.2.6)"
|
||||
// CommitSHA is the commmit sha.
|
||||
CommitSHA = "(unknown)"
|
||||
)
|
||||
@@ -35,7 +35,9 @@ auth:
|
||||
tokenExpirationTime: 2h
|
||||
header: ""
|
||||
method: json
|
||||
command: ""
|
||||
signup: false
|
||||
shell: ""
|
||||
frontend:
|
||||
name: ""
|
||||
disableExternal: false
|
||||
@@ -61,6 +63,7 @@ userDefaults:
|
||||
delete: true
|
||||
share: true
|
||||
download: true
|
||||
commands: []
|
||||
hideDotfiles: false
|
||||
dateFormat: false
|
||||
```
|
||||
@@ -162,6 +165,10 @@ userDefaults:
|
||||
- `oath` - oath authentication
|
||||
- `noauth` - no authentication/login required.
|
||||
|
||||
- `command`: Deprecated: This is the authentication command.
|
||||
|
||||
- `shell`: This is the shell configuration.
|
||||
|
||||
- `Signup`: This boolean value indicates whether user signup is enabled on the login page. NOTE: Be mindful of `userDefaults` settings if enabled. Default: `false`
|
||||
|
||||
- `AdminUsername`: This is the username of the admin user. Default: `admin`
|
||||
@@ -220,6 +227,7 @@ userDefaults:
|
||||
|
||||
- `download`: This boolean value determines whether download permissions are granted.
|
||||
|
||||
- `commands`: Deprecated: This is a list of commands.
|
||||
|
||||
- `hideDotfiles`: This boolean value determines whether dotfiles are hidden. (`true` or `false`)
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
frontend
|
||||
@@ -9,14 +9,13 @@
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build && cp -R dist/ ../backend/cmd/",
|
||||
"build-docker": "vite build",
|
||||
"build": "vite build",
|
||||
"watch": "vite build --watch",
|
||||
"typecheck": "vue-tsc -p ./tsconfig.json --noEmit",
|
||||
"lint": "npm run typecheck && eslint src/",
|
||||
"lint:fix": "eslint --fix src/",
|
||||
"format": "prettier --write .",
|
||||
"test": "npx playwright test"
|
||||
"test": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"ace-builds": "^1.24.2",
|
||||
@@ -32,7 +31,6 @@
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "line",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
|
||||
/* Set default locale to English (US) */
|
||||
locale: "en-US",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "firefox",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -3,18 +3,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { onMounted } from "vue";
|
||||
import { onMounted } from 'vue';
|
||||
import { mutations } from "@/store"; // Import your store's mutations
|
||||
mutations.setLoading("main-app", true);
|
||||
|
||||
export default {
|
||||
name: "app",
|
||||
computed: {},
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
console.log("static vars", window.FileBrowser);
|
||||
mutations.setLoading("main-app", false);
|
||||
mutations.setLoading(false); // Call your mutation or method to set loading to false
|
||||
// Query the loading element and remove it from the DOM
|
||||
const loadingDiv = document.getElementById("loading");
|
||||
const loadingDiv = document.getElementById('loading');
|
||||
if (loadingDiv) {
|
||||
loadingDiv.remove();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ export default async function search(base, query) {
|
||||
|
||||
data = data.map((item) => {
|
||||
item.url = `/files${base}` + url.encodePath(item.path);
|
||||
|
||||
if (item.dir) {
|
||||
item.url += "/";
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
|
||||
@@ -24,12 +24,6 @@ export async function create(user) {
|
||||
}
|
||||
|
||||
export async function update(user, which = ["all"]) {
|
||||
if (which[0] != "password") {
|
||||
user.password = "";
|
||||
}
|
||||
if (user.username == "publicUser") {
|
||||
return
|
||||
}
|
||||
await fetchURL(`/api/users/${user.id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -14,23 +14,11 @@
|
||||
<component :is="element" :to="link.url">{{ link.name }}</component>
|
||||
</span>
|
||||
<action style="display: contents" v-if="showShare" icon="share" show="share" />
|
||||
<div v-if="isResizableView">
|
||||
Size:
|
||||
<input
|
||||
v-model="gallerySize"
|
||||
type="range"
|
||||
id="gallary-size"
|
||||
name="gallary-size"
|
||||
:value="gallerySize"
|
||||
min="0"
|
||||
max="10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { state, mutations, getters } from "@/store"; // Import mutations as well
|
||||
import { state, mutations } from "@/store"; // Import mutations as well
|
||||
import Action from "@/components/header/Action.vue";
|
||||
|
||||
export default {
|
||||
@@ -38,22 +26,8 @@ export default {
|
||||
components: {
|
||||
Action,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
gallerySize: state.user.gallerySize,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
gallerySize(newValue) {
|
||||
this.gallerySize = parseInt(newValue, 0); // Update the user object
|
||||
mutations.setGallerySize(this.gallerySize);
|
||||
},
|
||||
},
|
||||
props: ["base", "noLink"],
|
||||
computed: {
|
||||
isResizableView() {
|
||||
return getters.isResizableView();
|
||||
},
|
||||
items() {
|
||||
const relativePath = state.route.path.replace(this.base, "");
|
||||
let parts = relativePath.split("/");
|
||||
@@ -110,7 +84,7 @@ export default {
|
||||
methods: {
|
||||
// Example of a method using mutations
|
||||
updateUserPermissions(newPerms) {
|
||||
mutations.updateUser({ perm: newPerms });
|
||||
mutations.updateUser({ perm: newPerms })
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
<template>
|
||||
<div class="button-group">
|
||||
<button v-if="isDisabled" disabled>
|
||||
No options for folders
|
||||
<button
|
||||
v-for="(btn, index) in buttons"
|
||||
:key="index"
|
||||
:disabled="isDisabled"
|
||||
:class="{ active: activeButton === index && !isDisabled }"
|
||||
@click="setActiveButton(index, btn.label)"
|
||||
>
|
||||
{{ btn.label }}
|
||||
</button>
|
||||
<template v-else>
|
||||
<button
|
||||
v-for="(btn, index) in buttons"
|
||||
:key="index"
|
||||
:class="{ active: activeButton === index }"
|
||||
@click="setActiveButton(index, btn.label)"
|
||||
>
|
||||
{{ btn.label }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -107,10 +103,6 @@ button:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
button.active {
|
||||
background-color: var(--blue) !important;
|
||||
color: #ffffff;
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
<template>
|
||||
<div v-if="selectedCount > 0" id="file-selection" :class="{ 'dark-mode': isDarkMode }">
|
||||
<span>{{ selectedCount }} selected</span>
|
||||
<div>
|
||||
<action
|
||||
v-if="headerButtons.select"
|
||||
icon="info"
|
||||
:label="$t('buttons.info')"
|
||||
show="info"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.select"
|
||||
icon="check_circle"
|
||||
:label="$t('buttons.selectMultiple')"
|
||||
@action="toggleMultipleSelection"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.download"
|
||||
icon="file_download"
|
||||
:label="$t('buttons.download')"
|
||||
@action="download"
|
||||
:counter="selectedCount"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.share"
|
||||
icon="share"
|
||||
:label="$t('buttons.share')"
|
||||
show="share"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.rename"
|
||||
icon="mode_edit"
|
||||
:label="$t('buttons.rename')"
|
||||
show="rename"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.copy"
|
||||
icon="content_copy"
|
||||
:label="$t('buttons.copyFile')"
|
||||
show="copy"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.move"
|
||||
icon="forward"
|
||||
:label="$t('buttons.moveFile')"
|
||||
show="move"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.delete"
|
||||
icon="delete"
|
||||
:label="$t('buttons.delete')"
|
||||
show="delete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
import { files as api } from "@/api";
|
||||
import Action from "@/components/header/Action.vue";
|
||||
|
||||
export default {
|
||||
name: "fileSelection",
|
||||
components: {
|
||||
Action,
|
||||
},
|
||||
computed: {
|
||||
isDarkMode() {
|
||||
return getters.isDarkMode();
|
||||
},
|
||||
headerButtons() {
|
||||
return {
|
||||
select: state.selected.length > 0,
|
||||
upload: state.user.perm?.create && state.selected.length > 0,
|
||||
download: state.user.perm.download && state.selected.length > 0,
|
||||
delete: state.selected.length > 0 && state.user.perm.delete,
|
||||
rename: state.selected.length === 1 && state.user.perm.rename,
|
||||
share: state.selected.length === 1 && state.user.perm.share,
|
||||
move: state.selected.length > 0 && state.user.perm.rename,
|
||||
copy: state.selected.length > 0 && state.user.perm?.create,
|
||||
};
|
||||
},
|
||||
selectedCount() {
|
||||
return getters.selectedCount();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleMultipleSelection() {
|
||||
mutations.setMultiple(!state.multiple);
|
||||
mutations.closeHovers();
|
||||
},
|
||||
download() {
|
||||
if (getters.isSingleFileSelected()) {
|
||||
api.download(null, getters.selectedDownloadUrl());
|
||||
return;
|
||||
}
|
||||
mutations.showHover({
|
||||
name: "download",
|
||||
confirm: (format) => {
|
||||
mutations.closeHovers();
|
||||
let files = [];
|
||||
if (state.selected.length > 0) {
|
||||
for (let i of state.selected) {
|
||||
files.push(state.req.items[i].url);
|
||||
}
|
||||
} else {
|
||||
files.push(state.route.path);
|
||||
}
|
||||
try {
|
||||
api.download(format, ...files);
|
||||
showSuccess("download started");
|
||||
} catch (e) {
|
||||
showError("error downloading", e);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
@media (min-width: 800px) {
|
||||
#file-selection {
|
||||
bottom: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
#file-selection .action {
|
||||
border-radius: 50%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#file-selection > span {
|
||||
display: inline-block;
|
||||
margin-left: 1em;
|
||||
color: #6f6f6f;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
#file-selection .action span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* File Selection */
|
||||
#file-selection {
|
||||
box-shadow: rgba(0, 0, 0, 0.3) 0px 2em 50px 10px;
|
||||
position: fixed;
|
||||
bottom: 4em;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
max-width: 30em;
|
||||
z-index: 3;
|
||||
border-radius: 1em;
|
||||
display: flex;
|
||||
width: 90%;
|
||||
}
|
||||
/* File selection */
|
||||
#file-selection.dark-mode {
|
||||
background: var(--surfaceSecondary) !important;
|
||||
}
|
||||
|
||||
#file-selection.dark-mode span {
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -219,14 +219,3 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.vue-simple-progress {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.vue-simple-progress,
|
||||
.vue-simple-progress-bar {
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div id="search" :class="{ active, ongoing, 'dark-mode': isDarkMode }">
|
||||
<div id="search" @click="open" :class="{ active, ongoing, 'dark-mode': isDarkMode }">
|
||||
<!-- Search input section -->
|
||||
<div id="input" @click="open">
|
||||
<div id="input">
|
||||
<!-- Close button visible when search is active -->
|
||||
<button
|
||||
v-if="active"
|
||||
@@ -34,8 +34,13 @@
|
||||
<div class="button" style="width: 100%">Search Context: {{ getContext }}</div>
|
||||
<!-- List of search results -->
|
||||
<ul v-show="results.length > 0">
|
||||
<li v-for="(s, k) in results" :key="k" style="cursor: pointer">
|
||||
<router-link :to="s.url">
|
||||
<li
|
||||
v-for="(s, k) in results"
|
||||
:key="k"
|
||||
@click.stop.prevent="navigateTo(s.url)"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<router-link to="#" event="">
|
||||
<i v-if="s.dir" class="material-icons folder-icons"> folder </i>
|
||||
<i v-else-if="s.audio" class="material-icons audio-icons"> volume_up </i>
|
||||
<i v-else-if="s.image" class="material-icons image-icons"> photo </i>
|
||||
@@ -96,6 +101,39 @@
|
||||
<div class="searchContext">Search Context: {{ getContext }}</div>
|
||||
<div id="result-list">
|
||||
<div>
|
||||
<!-- Loading icon when search is ongoing -->
|
||||
<p v-show="isEmpty && isRunning" id="renew">
|
||||
<i class="material-icons spin">autorenew</i>
|
||||
</p>
|
||||
<!-- Message when no results are found -->
|
||||
<div class="searchPrompt" v-show="isEmpty && !isRunning">
|
||||
<p>{{ noneMessage }}</p>
|
||||
<div class="helpButton" @click="toggleHelp()">Help</div>
|
||||
</div>
|
||||
<!-- Help text section -->
|
||||
<div class="helpText" v-if="showHelp">
|
||||
<p>
|
||||
Search occurs on each character you type (3 character minimum for search
|
||||
terms).
|
||||
</p>
|
||||
<p>
|
||||
<b>The index:</b> Search utilizes the index which automatically gets updated
|
||||
on the configured interval (default: 5 minutes). Searching when the program
|
||||
has just started may result in incomplete results.
|
||||
</p>
|
||||
<p>
|
||||
<b>Filter by type:</b> You can have multiple type filters by adding
|
||||
<code>type:condition</code> followed by search terms.
|
||||
</p>
|
||||
<p>
|
||||
<b>Multiple Search terms:</b> Additional terms separated by <code>|</code>,
|
||||
for example <code>"test|not"</code> searches for both terms independently.
|
||||
</p>
|
||||
<p>
|
||||
<b>File size:</b> Searching files by size may have significantly longer
|
||||
search times.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<!-- Button groups for filtering search results -->
|
||||
<ButtonGroup
|
||||
@@ -112,7 +150,7 @@
|
||||
:isDisabled="isTypeSelectDisabled"
|
||||
/>
|
||||
<!-- Inputs for filtering by file size -->
|
||||
<div v-if="!foldersOnly" class="sizeConstraints">
|
||||
<div class="sizeConstraints">
|
||||
<div class="sizeInputWrapper">
|
||||
<p>Smaller Than:</p>
|
||||
<input
|
||||
@@ -137,43 +175,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Loading icon when search is ongoing -->
|
||||
<p v-show="isEmpty && isRunning" id="renew">
|
||||
<i class="material-icons spin">autorenew</i>
|
||||
</p>
|
||||
<!-- Message when no results are found -->
|
||||
<div class="searchPrompt" v-show="isEmpty && !isRunning">
|
||||
<p>{{ noneMessage }}</p>
|
||||
<div class="helpButton" @click="toggleHelp()">Help</div>
|
||||
</div>
|
||||
<!-- Help text section -->
|
||||
<div class="helpText" v-if="showHelp">
|
||||
<p>
|
||||
Search occurs on each character you type (3 character minimum for search
|
||||
terms).
|
||||
</p>
|
||||
<p>
|
||||
<b>The index:</b> Search utilizes the index which automatically gets updated
|
||||
on the configured interval (default: 5 minutes). Searching when the program
|
||||
has just started may result in incomplete results.
|
||||
</p>
|
||||
<p>
|
||||
<b>Filter by type:</b> You can have multiple type filters by adding
|
||||
<code>type:condition</code> followed by search terms.
|
||||
</p>
|
||||
<p>
|
||||
<b>Multiple Search terms:</b> Additional terms separated by <code>|</code>,
|
||||
for example <code>"test|not"</code> searches for both terms independently.
|
||||
</p>
|
||||
<p>
|
||||
<b>File size:</b> Searching files by size may have significantly longer search
|
||||
times.
|
||||
</p>
|
||||
</div>
|
||||
<!-- List of search results -->
|
||||
<ul v-show="results.length > 0">
|
||||
<li v-for="(s, k) in results" :key="k" style="cursor: pointer">
|
||||
<router-link :to="s.url">
|
||||
<li
|
||||
v-for="(s, k) in results"
|
||||
:key="k"
|
||||
@click.stop.prevent="navigateTo(s.url)"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<router-link to="#" event="">
|
||||
<i v-if="s.dir" class="material-icons folder-icons"> folder </i>
|
||||
<i v-else-if="s.audio" class="material-icons audio-icons"> volume_up </i>
|
||||
<i v-else-if="s.image" class="material-icons image-icons"> photo </i>
|
||||
@@ -231,6 +241,7 @@ export default {
|
||||
{ label: "Archives", value: "type:archive" },
|
||||
],
|
||||
value: "",
|
||||
width: window.innerWidth,
|
||||
ongoing: false,
|
||||
results: [],
|
||||
reload: false,
|
||||
@@ -238,15 +249,6 @@ export default {
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
largerThan() {
|
||||
this.submit();
|
||||
},
|
||||
smallerThan() {
|
||||
this.submit();
|
||||
},
|
||||
searchTypes() {
|
||||
this.submit();
|
||||
},
|
||||
active(active) {
|
||||
const resultList = document.getElementById("result-list");
|
||||
if (!active) {
|
||||
@@ -257,6 +259,25 @@ export default {
|
||||
resultList.classList.add("active");
|
||||
}, 100);
|
||||
},
|
||||
currentPrompt(val, old) {
|
||||
this.active = val?.prompt === "search";
|
||||
if (old?.prompt === "search" && !this.active) {
|
||||
if (this.reload) {
|
||||
this.setReload(true);
|
||||
}
|
||||
|
||||
document.body.style.overflow = "auto";
|
||||
this.ongoing = false;
|
||||
this.results = [];
|
||||
this.value = "";
|
||||
this.active = false;
|
||||
this.$refs.input.blur();
|
||||
} else if (this.active) {
|
||||
this.reload = false;
|
||||
this.$refs.input.focus();
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
},
|
||||
value() {
|
||||
if (this.results.length) {
|
||||
this.ongoing = false;
|
||||
@@ -265,14 +286,11 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isMobile() {
|
||||
return state.isMobile;
|
||||
},
|
||||
foldersOnly() {
|
||||
return this.isTypeSelectDisabled;
|
||||
},
|
||||
active() {
|
||||
return getters.currentPromptName() == "search";
|
||||
return getters.currentPromptName() === "search";
|
||||
},
|
||||
showOverlay() {
|
||||
return getters.currentPrompt() !== null && getters.currentPromptName() !== "more";
|
||||
},
|
||||
isDarkMode() {
|
||||
return getters.isDarkMode();
|
||||
@@ -295,6 +313,9 @@ export default {
|
||||
? this.$t("search.typeToSearch")
|
||||
: this.$t("search.pressToSearch");
|
||||
},
|
||||
isMobile() {
|
||||
return this.width <= 800;
|
||||
},
|
||||
isRunning() {
|
||||
return this.ongoing;
|
||||
},
|
||||
@@ -309,7 +330,16 @@ export default {
|
||||
return path;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleResize() {
|
||||
this.width = window.innerWidth;
|
||||
},
|
||||
async navigateTo(url) {
|
||||
mutations.closeHovers();
|
||||
await this.$nextTick();
|
||||
setTimeout(() => this.$router.push(url), 10);
|
||||
},
|
||||
basePath(str, isDir) {
|
||||
let parts = str.replace(/(\/$|^\/)/, "").split("/");
|
||||
if (parts.length <= 1) {
|
||||
@@ -323,7 +353,6 @@ export default {
|
||||
if (isDir) {
|
||||
parts = "/" + parts; // fix weird rtl thing
|
||||
}
|
||||
|
||||
return parts;
|
||||
},
|
||||
baseName(str) {
|
||||
@@ -331,12 +360,9 @@ export default {
|
||||
return parts.pop();
|
||||
},
|
||||
open() {
|
||||
if (!this.active) {
|
||||
mutations.showHover("search");
|
||||
}
|
||||
mutations.showHover("search");
|
||||
},
|
||||
close(event) {
|
||||
this.value = "";
|
||||
event.stopPropagation();
|
||||
mutations.closeHovers();
|
||||
},
|
||||
@@ -364,7 +390,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
this.searchTypes = this.searchTypes.replace(string + " ", "");
|
||||
if (state.isMobile) {
|
||||
if (this.isMobile) {
|
||||
this.$refs.input.focus();
|
||||
}
|
||||
},
|
||||
@@ -376,9 +402,7 @@ export default {
|
||||
},
|
||||
async submit(event) {
|
||||
this.showHelp = false;
|
||||
if (event != undefined) {
|
||||
event.preventDefault();
|
||||
}
|
||||
event.preventDefault();
|
||||
if (this.value === "" || this.value.length < 3) {
|
||||
this.ongoing = false;
|
||||
this.results = [];
|
||||
@@ -408,6 +432,12 @@ export default {
|
||||
this.showHelp = !this.showHelp;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("resize", this.handleResize);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("resize", this.handleResize);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -489,7 +519,7 @@ export default {
|
||||
/* Search */
|
||||
#search {
|
||||
background-color: unset !important;
|
||||
z-index: 5;
|
||||
z-index: 3;
|
||||
position: fixed;
|
||||
top: 0.5em;
|
||||
min-width: 35em;
|
||||
@@ -609,10 +639,6 @@ body.rtl #search #result ul > * {
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
input.sizeInput:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Search Input Placeholder */
|
||||
#search::-webkit-input-placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<nav :class="{ active, 'dark-mode': isDarkMode }">
|
||||
<!-- Section for logged-in users -->
|
||||
<template v-if="isLoggedIn">
|
||||
<!-- My Files button -->
|
||||
<button
|
||||
class="action"
|
||||
@click="toRoot"
|
||||
:aria-label="$t('sidebar.myFiles')"
|
||||
:title="$t('sidebar.myFiles')"
|
||||
>
|
||||
<i class="material-icons">folder</i>
|
||||
<span>{{ $t("sidebar.myFiles") }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Buttons visible if user has create permission -->
|
||||
<div v-if="user.perm?.create">
|
||||
<!-- New Folder button -->
|
||||
<button
|
||||
@click="showHover('newDir')"
|
||||
class="action"
|
||||
:aria-label="$t('sidebar.newFolder')"
|
||||
:title="$t('sidebar.newFolder')"
|
||||
>
|
||||
<i class="material-icons">create_new_folder</i>
|
||||
<span>{{ $t("sidebar.newFolder") }}</span>
|
||||
</button>
|
||||
<!-- New File button -->
|
||||
<button
|
||||
@click="showHover('newFile')"
|
||||
class="action"
|
||||
:aria-label="$t('sidebar.newFile')"
|
||||
:title="$t('sidebar.newFile')"
|
||||
>
|
||||
<i class="material-icons">note_add</i>
|
||||
<span>{{ $t("sidebar.newFile") }}</span>
|
||||
</button>
|
||||
<!-- Upload button -->
|
||||
<button id="upload-button" @click="upload($event)" class="action">
|
||||
<i class="material-icons">file_upload</i>
|
||||
<span>Upload file</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Settings and Logout buttons -->
|
||||
<div>
|
||||
<!-- Settings button -->
|
||||
<button
|
||||
class="action"
|
||||
@click="toSettings"
|
||||
:aria-label="$t('sidebar.settings')"
|
||||
:title="$t('sidebar.settings')"
|
||||
>
|
||||
<i class="material-icons">settings_applications</i>
|
||||
<span>{{ $t("sidebar.settings") }}</span>
|
||||
</button>
|
||||
<!-- Logout button -->
|
||||
<button
|
||||
v-if="canLogout"
|
||||
@click="logout"
|
||||
class="action"
|
||||
id="logout"
|
||||
:aria-label="$t('sidebar.logout')"
|
||||
:title="$t('sidebar.logout')"
|
||||
>
|
||||
<i class="material-icons">exit_to_app</i>
|
||||
<span>{{ $t("sidebar.logout") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Section for non-logged-in users -->
|
||||
<template v-else>
|
||||
<!-- Login button -->
|
||||
<router-link
|
||||
class="action"
|
||||
to="/login"
|
||||
:aria-label="$t('sidebar.login')"
|
||||
:title="$t('sidebar.login')"
|
||||
>
|
||||
<i class="material-icons">exit_to_app</i>
|
||||
<span>{{ $t("sidebar.login") }}</span>
|
||||
</router-link>
|
||||
<!-- Signup button, if signup is enabled -->
|
||||
<router-link
|
||||
v-if="signup"
|
||||
class="action"
|
||||
to="/login"
|
||||
:aria-label="$t('sidebar.signup')"
|
||||
:title="$t('sidebar.signup')"
|
||||
>
|
||||
<i class="material-icons">person_add</i>
|
||||
<span>{{ $t("sidebar.signup") }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<!-- Credits and usage information section -->
|
||||
<div class="credits" v-if="isFiles && !disableUsedPercentage && usage">
|
||||
<progress-bar :val="usage.usedPercentage" size="medium"></progress-bar>
|
||||
<span style="text-align: center">{{ usage.usedPercentage }}%</span>
|
||||
<span>{{ usage.used }} of {{ usage.total }} used</span>
|
||||
<br />
|
||||
<span v-if="disableExternal">File Browser</span>
|
||||
<span v-else>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href="https://github.com/gtsteffaniak/filebrowser"
|
||||
>
|
||||
File Browser
|
||||
</a>
|
||||
</span>
|
||||
<span>{{ version }}</span>
|
||||
<span>
|
||||
<a @click="help">{{ $t("sidebar.help") }}</a>
|
||||
</span>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as upload from "@/utils/upload";
|
||||
import * as auth from "@/utils/auth";
|
||||
import {
|
||||
version,
|
||||
signup,
|
||||
disableExternal,
|
||||
disableUsedPercentage,
|
||||
noAuth,
|
||||
loginPage,
|
||||
} from "@/utils/constants";
|
||||
import { files as api } from "@/api";
|
||||
import ProgressBar from "@/components/ProgressBar.vue";
|
||||
import { getHumanReadableFilesize } from "@/utils/filesizes";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
import { showError } from "@/notify";
|
||||
|
||||
export default {
|
||||
name: "sidebar",
|
||||
components: {
|
||||
ProgressBar,
|
||||
},
|
||||
mounted() {
|
||||
this.updateUsage();
|
||||
},
|
||||
computed: {
|
||||
isFiles() {
|
||||
return getters.isFiles();
|
||||
},
|
||||
user() {
|
||||
return state.user;
|
||||
},
|
||||
isDarkMode() {
|
||||
return getters.isDarkMode();
|
||||
},
|
||||
isLoggedIn() {
|
||||
return getters.isLoggedIn();
|
||||
},
|
||||
currentPrompt() {
|
||||
return getters.currentPrompt();
|
||||
},
|
||||
active() {
|
||||
return getters.currentPromptName() === "sidebar";
|
||||
},
|
||||
signup: () => signup,
|
||||
version: () => version,
|
||||
disableExternal: () => disableExternal,
|
||||
disableUsedPercentage: () => disableUsedPercentage,
|
||||
canLogout: () => !noAuth && loginPage,
|
||||
usage: () => state.usage,
|
||||
},
|
||||
methods: {
|
||||
async updateUsage() {
|
||||
console.log("updating usage");
|
||||
|
||||
let path = getters.getRoutePath();
|
||||
let usageStats = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
||||
if (this.disableUsedPercentage) {
|
||||
return usageStats;
|
||||
}
|
||||
try {
|
||||
let usage = await api.usage(path);
|
||||
usageStats = {
|
||||
used: getHumanReadableFilesize(usage.used / 1024),
|
||||
total: getHumanReadableFilesize(usage.total / 1024),
|
||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
||||
};
|
||||
} catch (error) {
|
||||
showError("Error fetching usage:", error);
|
||||
}
|
||||
console.log(usageStats);
|
||||
mutations.setUsage(usageStats);
|
||||
},
|
||||
showHover(value) {
|
||||
return mutations.showHover(value);
|
||||
},
|
||||
// Navigate to the root files directory
|
||||
toRoot() {
|
||||
this.$router.push({ path: "/files/" }, () => {});
|
||||
mutations.closeHovers();
|
||||
},
|
||||
// Navigate to the settings page
|
||||
toSettings() {
|
||||
this.$router.push({ path: "/settings" }, () => {});
|
||||
mutations.closeHovers();
|
||||
},
|
||||
// Show the help overlay
|
||||
help() {
|
||||
mutations.showHover("help");
|
||||
},
|
||||
// Handle file upload
|
||||
upload(event) {
|
||||
return this.$upload(event);
|
||||
},
|
||||
// Handle files selected for upload
|
||||
uploadInput(event) {
|
||||
mutations.closeHovers();
|
||||
|
||||
let files = event.currentTarget.files;
|
||||
let folder_upload =
|
||||
files[0].webkitRelativePath !== undefined && files[0].webkitRelativePath !== "";
|
||||
|
||||
if (folder_upload) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let file = files[i];
|
||||
files[i].fullPath = file.webkitRelativePath;
|
||||
}
|
||||
}
|
||||
|
||||
let path = getters.getRoutePath();
|
||||
let conflict = upload.checkConflict(files, state.req.items);
|
||||
|
||||
if (conflict) {
|
||||
mutations.showHover({
|
||||
name: "replace",
|
||||
confirm: (event) => {
|
||||
event.preventDefault();
|
||||
mutations.closeHovers();
|
||||
upload.handleFiles(files, path, true);
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
upload.handleFiles(files, path);
|
||||
},
|
||||
// Logout the user
|
||||
logout: auth.logout,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -10,21 +10,18 @@
|
||||
@mouseup="mouseUp"
|
||||
@wheel="wheelMove"
|
||||
>
|
||||
<div v-if="!isLoaded">Loading image...</div>
|
||||
|
||||
<img
|
||||
v-if="!isTiff && isLoaded"
|
||||
v-if="!isTiff"
|
||||
:src="src"
|
||||
class="image-ex-img"
|
||||
class="image-ex-img image-ex-img-center"
|
||||
ref="imgex"
|
||||
@load="onLoad"
|
||||
/>
|
||||
<canvas v-else-if="isLoaded" ref="imgex" class="image-ex-img"></canvas>
|
||||
<canvas v-else ref="imgex" class="image-ex-img"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import throttle from "@/utils/throttle";
|
||||
import { showError } from "@/notify";
|
||||
export default {
|
||||
@@ -85,17 +82,8 @@ export default {
|
||||
window.removeEventListener("resize", this.onResize);
|
||||
document.removeEventListener("mouseup", this.onMouseUp);
|
||||
},
|
||||
computed: {
|
||||
isLoaded() {
|
||||
return !("preview-img" in state.loading);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
src: function () {
|
||||
if (this.src == undefined || this.$refs.imgex == undefined) {
|
||||
mutations.setLoading("preview-img", false);
|
||||
return;
|
||||
}
|
||||
this.isTiff = this.checkIfTiff(this.src);
|
||||
if (this.isTiff) {
|
||||
this.decodeTiff(this.src);
|
||||
@@ -106,8 +94,6 @@ export default {
|
||||
this.scale = 1;
|
||||
this.setZoom();
|
||||
this.setCenter();
|
||||
mutations.setLoading("preview-img", false);
|
||||
this.showSpinner = false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -135,6 +121,36 @@ export default {
|
||||
console.error("Error decoding TIFF:", error);
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
let img = this.$refs.imgex;
|
||||
|
||||
this.imageLoaded = true;
|
||||
|
||||
if (img === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
img.classList.remove("image-ex-img-center");
|
||||
this.setCenter();
|
||||
img.classList.add("image-ex-img-ready");
|
||||
|
||||
document.addEventListener("mouseup", this.onMouseUp);
|
||||
|
||||
let realSize = img.naturalWidth;
|
||||
let displaySize = img.offsetWidth;
|
||||
|
||||
// Image is in portrait orientation
|
||||
if (img.naturalHeight > img.naturalWidth) {
|
||||
realSize = img.naturalHeight;
|
||||
displaySize = img.offsetHeight;
|
||||
}
|
||||
|
||||
// Scale needed to display the image on full size
|
||||
const fullScale = realSize / displaySize;
|
||||
|
||||
// Full size plus additional zoom
|
||||
this.maxScale = fullScale + 4;
|
||||
},
|
||||
onMouseUp() {
|
||||
this.inDrag = false;
|
||||
},
|
||||
@@ -242,19 +258,32 @@ export default {
|
||||
}
|
||||
},
|
||||
doMove(x, y) {
|
||||
this.position.relative.x += x;
|
||||
this.position.relative.y += y;
|
||||
// Update the transform with separate translate and scale values
|
||||
this.$refs.imgex.style.transform = `translate(${this.position.relative.x}px, ${this.position.relative.y}px) scale(${this.scale})`;
|
||||
let style = this.$refs.imgex.style;
|
||||
let posX = this.pxStringToNumber(style.left) + x;
|
||||
let posY = this.pxStringToNumber(style.top) + y;
|
||||
|
||||
style.left = posX + "px";
|
||||
style.top = posY + "px";
|
||||
|
||||
this.position.relative.x = Math.abs(this.position.center.x - posX);
|
||||
this.position.relative.y = Math.abs(this.position.center.y - posY);
|
||||
|
||||
if (posX < this.position.center.x) {
|
||||
this.position.relative.x = this.position.relative.x * -1;
|
||||
}
|
||||
|
||||
if (posY < this.position.center.y) {
|
||||
this.position.relative.y = this.position.relative.y * -1;
|
||||
}
|
||||
},
|
||||
wheelMove(event) {
|
||||
this.scale += -Math.sign(event.deltaY) * this.zoomStep;
|
||||
this.setZoom();
|
||||
},
|
||||
setZoom() {
|
||||
this.scale = Math.max(this.minScale, Math.min(this.maxScale, this.scale));
|
||||
// Update the transform with both translate and scale values
|
||||
this.$refs.imgex.style.transform = `translate(${this.position.relative.x}px, ${this.position.relative.y}px) scale(${this.scale})`;
|
||||
this.scale = this.scale < this.minScale ? this.minScale : this.scale;
|
||||
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale;
|
||||
this.$refs.imgex.style.transform = `scale(${this.scale})`;
|
||||
},
|
||||
pxStringToNumber(style) {
|
||||
return +style.replace("px", "");
|
||||
@@ -265,17 +294,26 @@ export default {
|
||||
|
||||
<style>
|
||||
.image-ex-container {
|
||||
max-width: 100%; /* Image container max width */
|
||||
max-height: 100%; /* Image container max height */
|
||||
overflow: hidden; /* Hide overflow if image exceeds container */
|
||||
position: relative; /* Required for absolute positioning of child */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image-ex-img {
|
||||
max-width: 100%; /* Image max width */
|
||||
max-height: 100%; /* Image max height */
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.image-ex-img-center {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
position: absolute;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.image-ex-img-ready {
|
||||
left: 0;
|
||||
top: 0;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
</style>
|
||||
@@ -31,10 +31,12 @@
|
||||
></i>
|
||||
</div>
|
||||
|
||||
<div class="text" :class="{ activecontent: this.isMaximized && this.isSelected }">
|
||||
<div :class="{ activecontent: this.isMaximized && this.isSelected }">
|
||||
<p class="name">{{ name }}</p>
|
||||
|
||||
<p v-if="isDir" class="size" data-order="-1">—</p>
|
||||
<p v-else class="size" :data-order="humanSize()">{{ humanSize() }}</p>
|
||||
|
||||
<p class="modified">
|
||||
<time :datetime="modified">{{ humanTime() }}</time>
|
||||
</p>
|
||||
|
||||
@@ -23,6 +23,4 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
<style></style>
|
||||
@@ -33,7 +33,7 @@
|
||||
import { files as api } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import { state, getters, mutations } from "@/store";
|
||||
import { showError,showSuccess } from "@/notify";
|
||||
import { showError } from "@/notify";
|
||||
|
||||
export default {
|
||||
name: "delete",
|
||||
@@ -59,7 +59,6 @@ export default {
|
||||
if (!this.isListing) {
|
||||
await api.remove(state.route.path);
|
||||
buttons.success("delete");
|
||||
showSuccess("Deleted item successfully")
|
||||
|
||||
this.currentPrompt?.confirm();
|
||||
this.closeHovers();
|
||||
@@ -79,7 +78,6 @@ export default {
|
||||
|
||||
await Promise.all(promises);
|
||||
buttons.success("delete");
|
||||
showSuccess("Deleted item successfully")
|
||||
mutations.setReload(true); // Handle reload as needed
|
||||
} catch (e) {
|
||||
buttons.done("delete");
|
||||
|
||||
@@ -68,7 +68,7 @@ export default {
|
||||
return state.user;
|
||||
},
|
||||
closeHovers() {
|
||||
return mutations.closeHovers();
|
||||
return mutations.closeHovers()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -91,7 +91,7 @@ export default {
|
||||
.then(() => {
|
||||
buttons.success("move");
|
||||
this.$router.push({ path: this.dest });
|
||||
mutations.setReload(true);
|
||||
mutations.setReload(true)
|
||||
})
|
||||
.catch((e) => {
|
||||
buttons.done("move");
|
||||
@@ -111,17 +111,18 @@ export default {
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
|
||||
event.preventDefault();
|
||||
mutations.closeHovers();
|
||||
action(overwrite, rename);
|
||||
mutations.setReload(true);
|
||||
mutations.setReload(true)
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
action(overwrite, rename);
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ import Share from "./Share.vue";
|
||||
import Upload from "./Upload.vue";
|
||||
import ShareDelete from "./ShareDelete.vue";
|
||||
import DeleteUser from "./DeleteUser.vue";
|
||||
import Sidebar from "../sidebar/Sidebar.vue";
|
||||
import Sidebar from "../Sidebar.vue";
|
||||
import buttons from "@/utils/buttons";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
|
||||
|
||||
@@ -7,32 +7,29 @@
|
||||
<div class="card-content">
|
||||
<p>{{ $t("prompts.replaceMessage") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="closeHovers"
|
||||
@click="(event) => currentPrompt.confirm(event, 'rename')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
tabindex="3"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat button--blue"
|
||||
@click="(event) => currentPrompt.confirm(event, 'rename')"
|
||||
@click="(event) => currentPrompt.confirm(event, 'overwrite')"
|
||||
:aria-label="$t('buttons.rename')"
|
||||
:title="$t('buttons.rename')"
|
||||
tabindex="2"
|
||||
>
|
||||
{{ $t("buttons.rename") }}
|
||||
</button>
|
||||
<button
|
||||
id="focus-prompt"
|
||||
class="button button--flat button--red"
|
||||
@click="(event) => currentPrompt.confirm(event, 'overwrite')"
|
||||
@click="(event) => showConfirm(event, 'overwrite')"
|
||||
:aria-label="$t('buttons.replace')"
|
||||
:title="$t('buttons.replace')"
|
||||
tabindex="1"
|
||||
>
|
||||
{{ $t("buttons.replace") }}
|
||||
</button>
|
||||
|
||||
@@ -9,134 +9,30 @@
|
||||
</div>
|
||||
|
||||
<div class="card-action full">
|
||||
<div
|
||||
@click="uploadFile"
|
||||
@keypress.enter="uploadFile"
|
||||
class="action"
|
||||
id="focus-prompt"
|
||||
tabindex="1"
|
||||
>
|
||||
<div @click="uploadFile" class="action">
|
||||
<i class="material-icons">insert_drive_file</i>
|
||||
<div class="title">{{ $t("buttons.file") }}</div>
|
||||
</div>
|
||||
<div
|
||||
@click="uploadFolder"
|
||||
@keypress.enter="uploadFolder"
|
||||
class="action"
|
||||
tabindex="2"
|
||||
>
|
||||
<div @click="uploadFolder" class="action">
|
||||
<i class="material-icons">folder</i>
|
||||
<div class="title">{{ $t("buttons.folder") }}</div>
|
||||
</div>
|
||||
<input ref="fileInput" @change="onFilePicked" type="file" style="display: none" />
|
||||
<input
|
||||
ref="folderInput"
|
||||
@change="onFolderPicked"
|
||||
type="file"
|
||||
webkitdirectory
|
||||
directory
|
||||
style="display: none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from "vue";
|
||||
import * as upload from "@/utils/upload";
|
||||
import { mutations, state, getters } from "@/store";
|
||||
|
||||
export default {
|
||||
name: "UploadFiles",
|
||||
setup() {
|
||||
const fileInput = ref(null);
|
||||
const folderInput = ref(null);
|
||||
|
||||
const triggerFilePicker = () => {
|
||||
fileInput.value.click();
|
||||
};
|
||||
|
||||
const triggerFolderPicker = () => {
|
||||
folderInput.value.click();
|
||||
};
|
||||
|
||||
const onFilePicked = (event) => {
|
||||
handleFiles(event);
|
||||
};
|
||||
|
||||
const onFolderPicked = (event) => {
|
||||
handleFiles(event);
|
||||
};
|
||||
|
||||
const handleFiles = (event) => {
|
||||
mutations.closeHovers();
|
||||
const files = event.target.files;
|
||||
if (!files) return;
|
||||
|
||||
const folderUpload = !!files[0].webkitRelativePath;
|
||||
const uploadFiles = [];
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const fullPath = folderUpload ? file.webkitRelativePath : undefined;
|
||||
uploadFiles.push({
|
||||
file, // File object directly
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
isDir: false,
|
||||
fullPath,
|
||||
});
|
||||
}
|
||||
|
||||
const path = getters.getRoutePath();
|
||||
const conflict = upload.checkConflict(uploadFiles, state.req.items);
|
||||
|
||||
if (conflict) {
|
||||
mutations.showHover({
|
||||
name: "replace",
|
||||
action: (event) => {
|
||||
event.preventDefault();
|
||||
mutations.closeHovers();
|
||||
upload.handleFiles(uploadFiles, path, false);
|
||||
},
|
||||
confirm: (event) => {
|
||||
event.preventDefault();
|
||||
mutations.closeHovers();
|
||||
upload.handleFiles(uploadFiles, path, true);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
upload.handleFiles(uploadFiles, path, true);
|
||||
mutations.setReload(true);
|
||||
};
|
||||
|
||||
const openUpload = (isFolder) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = true;
|
||||
input.webkitdirectory = isFolder;
|
||||
input.addEventListener("change", handleFiles);
|
||||
input.click();
|
||||
};
|
||||
|
||||
const uploadFile = () => {
|
||||
openUpload(false);
|
||||
};
|
||||
|
||||
const uploadFolder = () => {
|
||||
openUpload(true);
|
||||
};
|
||||
|
||||
return {
|
||||
triggerFilePicker,
|
||||
triggerFolderPicker,
|
||||
uploadFile,
|
||||
uploadFolder,
|
||||
onFilePicked,
|
||||
onFolderPicked,
|
||||
};
|
||||
name: "upload",
|
||||
methods: {
|
||||
uploadFile: function () {
|
||||
document.getElementById("upload-input").value = "";
|
||||
document.getElementById("upload-input").click();
|
||||
},
|
||||
uploadFolder: function () {
|
||||
document.getElementById("upload-folder-input").value = "";
|
||||
document.getElementById("upload-folder-input").click();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="filesInUploadCount > 0"
|
||||
class="upload-files"
|
||||
v-bind:class="{ closed: !open }"
|
||||
>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2>
|
||||
|
||||
<button
|
||||
class="action"
|
||||
@click="toggle"
|
||||
aria-label="Toggle file upload list"
|
||||
title="Toggle file upload list"
|
||||
>
|
||||
<i class="material-icons">{{
|
||||
open ? "keyboard_arrow_down" : "keyboard_arrow_up"
|
||||
}}</i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card-content file-icons">
|
||||
<div
|
||||
class="file"
|
||||
v-for="file in filesInUpload"
|
||||
:key="file.id"
|
||||
:data-dir="file.isDir"
|
||||
:data-type="file.type"
|
||||
:aria-label="file.name"
|
||||
>
|
||||
<div class="file-name"><i class="material-icons"></i> {{ file.name }}</div>
|
||||
<div class="file-progress">
|
||||
<div v-bind:style="{ width: file.progress + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { getters } from "@/store"; // Import your custom store
|
||||
|
||||
export default {
|
||||
name: "uploadFiles",
|
||||
data() {
|
||||
return {
|
||||
open: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filesInUpload() {
|
||||
return getters.filesInUpload(); // Access the getter directly from the store
|
||||
},
|
||||
filesInUploadCount() {
|
||||
return getters.filesInUploadCount(); // Access the getter directly from the store
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggle() {
|
||||
this.open = !this.open;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<select @change="change" :value="locale">
|
||||
<select v-on:change="change" :value="locale">
|
||||
<option v-for="(value, label) in locales" :key="label" :value="label">
|
||||
{{ $t("languages." + label) }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
@@ -47,8 +47,8 @@ export default defineComponent({
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
change(event) {
|
||||
const target = event.target;
|
||||
change(event: Event) {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
this.$emit("update:locale", target.value);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { state } from "@/store"
|
||||
import Languages from "./Languages.vue";
|
||||
import Rules from "./Rules.vue";
|
||||
import Permissions from "./Permissions.vue";
|
||||
@@ -88,15 +87,12 @@ export default {
|
||||
Rules,
|
||||
Commands,
|
||||
},
|
||||
props: [ "createUserDir", "isNew", "isDefault"],
|
||||
props: ["user", "createUserDir", "isNew", "isDefault"],
|
||||
created() {
|
||||
this.originalUserScope = state.user.scope;
|
||||
this.originalUserScope = this.user.scope;
|
||||
this.createUserDirData = this.createUserDir;
|
||||
},
|
||||
computed: {
|
||||
user() {
|
||||
return state.user;
|
||||
},
|
||||
passwordPlaceholder() {
|
||||
return this.isNew ? "" : this.$t("settings.avoidChanges");
|
||||
},
|
||||
@@ -113,8 +109,12 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"user.perm.admin": function () {
|
||||
if (!this.user.perm.admin) return;
|
||||
this.user.lockPassword = false;
|
||||
},
|
||||
createUserDirData(newVal) {
|
||||
state.user.scope = newVal ? "" : this.originalUserScope;
|
||||
this.user.scope = newVal ? "" : this.originalUserScope;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,320 +0,0 @@
|
||||
<template>
|
||||
<div class="card clickable" style="min-height: 4em">
|
||||
<div class="card-wrapper user-card">
|
||||
<div @click="navigateTo('/settings#profile-main')" class="inner-card">
|
||||
<i class="material-icons">person</i>
|
||||
{{ user.username }}
|
||||
<i class="material-icons">settings</i>
|
||||
</div>
|
||||
|
||||
<div class="inner-card">
|
||||
<i v-if="canLogout" @click="logout" class="material-icons">exit_to_app</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="min-height: 6em">
|
||||
<div class="card-wrapper" @mouseleave="resetHoverTextToDefault">
|
||||
<span>{{ hoverText }}</span>
|
||||
<div class="quick-toggles">
|
||||
<div
|
||||
:class="{ active: user?.singleClick }"
|
||||
@click="toggleClick"
|
||||
@mouseover="updateHoverText('Toggle single click')"
|
||||
>
|
||||
<i class="material-icons">ads_click</i>
|
||||
</div>
|
||||
<div
|
||||
:class="{ active: user?.darkMode }"
|
||||
@click="toggleDarkMode"
|
||||
@mouseover="updateHoverText('Toggle dark mode')"
|
||||
>
|
||||
<i class="material-icons">dark_mode</i>
|
||||
</div>
|
||||
<div
|
||||
:class="{ active: isStickySidebar }"
|
||||
@click="toggleSticky"
|
||||
@mouseover="updateHoverText('Toggle sticky sidebar')"
|
||||
v-if="!isMobile"
|
||||
>
|
||||
<i class="material-icons">push_pin</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section for logged-in users -->
|
||||
<div v-if="isLoggedIn" class="sidebar-scroll-list">
|
||||
<!-- Buttons visible if user has create permission -->
|
||||
<div v-if="user.perm?.create">
|
||||
<!-- New Folder button -->
|
||||
<button
|
||||
@click="showHover('newDir')"
|
||||
class="action"
|
||||
:aria-label="$t('sidebar.newFolder')"
|
||||
:title="$t('sidebar.newFolder')"
|
||||
>
|
||||
<i class="material-icons">create_new_folder</i>
|
||||
<span>{{ $t("sidebar.newFolder") }}</span>
|
||||
</button>
|
||||
<!-- New File button -->
|
||||
<button
|
||||
@click="showHover('newFile')"
|
||||
class="action"
|
||||
:aria-label="$t('sidebar.newFile')"
|
||||
:title="$t('sidebar.newFile')"
|
||||
>
|
||||
<i class="material-icons">note_add</i>
|
||||
<span>{{ $t("sidebar.newFile") }}</span>
|
||||
</button>
|
||||
<!-- Upload button -->
|
||||
<button id="upload-button" @click="uploadFunc" class="action">
|
||||
<i class="material-icons">file_upload</i>
|
||||
<span>Upload file</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoggedIn" class="sources card">
|
||||
<span>Sources</span>
|
||||
<div class="inner-card">
|
||||
<!-- My Files button -->
|
||||
<button
|
||||
class="action"
|
||||
@click="navigateTo('/files/')"
|
||||
:aria-label="$t('sidebar.myFiles')"
|
||||
:title="$t('sidebar.myFiles')"
|
||||
>
|
||||
<i class="material-icons">folder</i>
|
||||
<span>{{ $t("sidebar.myFiles") }}</span>
|
||||
<div>
|
||||
<progress-bar :val="usage.usedPercentage" size="medium"></progress-bar>
|
||||
<div class="usage-info">
|
||||
<span>{{ usage.usedPercentage }}%</span>
|
||||
<span>{{ usage.used }} of {{ usage.total }} used</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section for non-logged-in users -->
|
||||
<div v-else class="sidebar-scroll-list">
|
||||
<!-- Login button -->
|
||||
<router-link
|
||||
class="action"
|
||||
to="/login"
|
||||
:aria-label="$t('sidebar.login')"
|
||||
:title="$t('sidebar.login')"
|
||||
>
|
||||
<i class="material-icons">exit_to_app</i>
|
||||
<span>{{ $t("sidebar.login") }}</span>
|
||||
</router-link>
|
||||
<!-- Signup button, if signup is enabled -->
|
||||
<router-link
|
||||
v-if="signup"
|
||||
class="action"
|
||||
to="/login"
|
||||
:aria-label="$t('sidebar.signup')"
|
||||
:title="$t('sidebar.signup')"
|
||||
>
|
||||
<i class="material-icons">person_add</i>
|
||||
<span>{{ $t("sidebar.signup") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as auth from "@/utils/auth";
|
||||
import {
|
||||
version,
|
||||
commitSHA,
|
||||
signup,
|
||||
disableExternal,
|
||||
disableUsedPercentage,
|
||||
noAuth,
|
||||
loginPage,
|
||||
} from "@/utils/constants";
|
||||
import { files } from "@/api";
|
||||
import ProgressBar from "@/components/ProgressBar.vue";
|
||||
import { getHumanReadableFilesize } from "@/utils/filesizes";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
import { showError } from "@/notify";
|
||||
|
||||
export default {
|
||||
name: "SidebarGeneral",
|
||||
components: {
|
||||
ProgressBar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hoverText: "Quick Toggles", // Initially empty
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (getters.isLoggedIn()) {
|
||||
this.updateUsage();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isSettings: () => getters.isSettings(),
|
||||
isStickySidebar: () => getters.isStickySidebar(),
|
||||
isMobile: () => getters.isMobile(),
|
||||
isFiles: () => getters.isFiles(),
|
||||
user: () => (getters.isLoggedIn() ? state.user : {}),
|
||||
isDarkMode: () => getters.isDarkMode(),
|
||||
isLoggedIn: () => getters.isLoggedIn(),
|
||||
currentPrompt: () => getters.currentPrompt(),
|
||||
active: () => getters.isSidebarVisible(),
|
||||
signup: () => signup,
|
||||
version: () => version,
|
||||
commitSHA: () => commitSHA,
|
||||
disableExternal: () => disableExternal,
|
||||
disableUsedPercentage: () => disableUsedPercentage,
|
||||
canLogout: () => !noAuth && loginPage,
|
||||
usage: () => state.usage,
|
||||
route: () => state.route,
|
||||
},
|
||||
watch: {
|
||||
route() {
|
||||
if (!getters.isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
if (!this.isStickySidebar) {
|
||||
mutations.closeSidebar();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateHoverText(text) {
|
||||
this.hoverText = text;
|
||||
},
|
||||
resetHoverTextToDefault() {
|
||||
this.hoverText = "Quick Toggles"; // Reset to default hover text
|
||||
},
|
||||
toggleClick() {
|
||||
mutations.updateUser({ singleClick: !state.user.singleClick });
|
||||
},
|
||||
toggleDarkMode() {
|
||||
mutations.toggleDarkMode();
|
||||
},
|
||||
toggleSticky() {
|
||||
mutations.updateUser({ stickySidebar: !state.user.stickySidebar });
|
||||
},
|
||||
async updateUsage() {
|
||||
if (!getters.isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
let path = getters.getRoutePath();
|
||||
let usageStats = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
||||
if (this.disableUsedPercentage) {
|
||||
return usageStats;
|
||||
}
|
||||
try {
|
||||
let usage = await files.usage(path);
|
||||
usageStats = {
|
||||
used: getHumanReadableFilesize(usage.used / 1024),
|
||||
total: getHumanReadableFilesize(usage.total / 1024),
|
||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
||||
};
|
||||
} catch (error) {
|
||||
showError("Error fetching usage", error);
|
||||
}
|
||||
mutations.setUsage(usageStats);
|
||||
},
|
||||
showHover(value) {
|
||||
return mutations.showHover(value);
|
||||
},
|
||||
navigateTo(path) {
|
||||
const hashIndex = path.indexOf("#");
|
||||
if (hashIndex !== -1) {
|
||||
// Extract the hash
|
||||
const hash = path.substring(hashIndex);
|
||||
// Remove the hash from the path
|
||||
const cleanPath = path.substring(0, hashIndex);
|
||||
this.$router.push({ path: cleanPath, hash: hash }, () => {});
|
||||
} else {
|
||||
this.$router.push({ path: path }, () => {});
|
||||
}
|
||||
mutations.closeHovers();
|
||||
},
|
||||
// Show the help overlay
|
||||
help() {
|
||||
mutations.showHover("help");
|
||||
},
|
||||
uploadFunc() {
|
||||
mutations.showHover("upload");
|
||||
},
|
||||
// Logout the user
|
||||
logout: auth.logout,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.user-card {
|
||||
flex-direction: row !important;
|
||||
justify-content: space-between !important;
|
||||
}
|
||||
|
||||
.quick-toggles {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
width: 100%;
|
||||
margin-top: 0.5em !important;
|
||||
}
|
||||
|
||||
.quick-toggles button {
|
||||
border-radius: 10em;
|
||||
cursor: pointer;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.sources {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
padding: 1em;
|
||||
margin-top: 0.5em !important;
|
||||
}
|
||||
|
||||
.sources .inner-card {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.usage-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quick-toggles div {
|
||||
border-radius: 10em;
|
||||
background-color: var(--surfaceSecondary);
|
||||
}
|
||||
|
||||
.quick-toggles div i {
|
||||
font-size: 2em;
|
||||
padding: 0.25em;
|
||||
border-radius: 10em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.action {
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
.quick-toggles .active {
|
||||
background-color: var(--blue) !important;
|
||||
border-radius: 10em;
|
||||
}
|
||||
.inner-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-for="setting in settings"
|
||||
:key="setting.id + '-sidebar'"
|
||||
:id="setting.id + '-sidebar'"
|
||||
class="card clickable"
|
||||
@click="setView(setting.id + '-main')"
|
||||
:class="{ 'active-settings': active(setting.id + '-main') }"
|
||||
>
|
||||
<div class="card-wrapper">{{ setting.label }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { state, getters, mutations } from "@/store";
|
||||
import { settings } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "SidebarSettings",
|
||||
data() {
|
||||
return {
|
||||
settings // Initialize the settings array in data
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentHash: () => getters.currentHash(),
|
||||
},
|
||||
methods: {
|
||||
active: (view) => state.activeSettingsView === view,
|
||||
setView(view) {
|
||||
mutations.setActiveSettingsView(view);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.active-settings {
|
||||
font-weight: bold;
|
||||
/* border-color: white; */
|
||||
border-style: solid;
|
||||
}
|
||||
</style>
|
||||
@@ -1,171 +0,0 @@
|
||||
<template>
|
||||
<nav id="sidebar" :class="{ active: active, 'dark-mode': isDarkMode }">
|
||||
<SidebarSettings v-if="isSettings"></SidebarSettings>
|
||||
<SidebarGeneral v-else-if="isLoggedIn"></SidebarGeneral>
|
||||
|
||||
<div class="buffer"></div>
|
||||
<div class="credits">
|
||||
<span>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href="https://github.com/gtsteffaniak/filebrowser"
|
||||
>
|
||||
FileBrowser Quantum
|
||||
</a>
|
||||
</span>
|
||||
<span>
|
||||
<a
|
||||
:href="'https://github.com/gtsteffaniak/filebrowser/releases/'"
|
||||
:title="commitSHA"
|
||||
>
|
||||
({{ version }})
|
||||
</a>
|
||||
</span>
|
||||
<span>
|
||||
<a @click="help">{{ $t("sidebar.help") }}</a>
|
||||
</span>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { version, commitSHA } from "@/utils/constants";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
import SidebarGeneral from "./General.vue";
|
||||
import SidebarSettings from "./Settings.vue";
|
||||
|
||||
export default {
|
||||
name: "sidebar",
|
||||
components: {
|
||||
SidebarGeneral,
|
||||
SidebarSettings,
|
||||
},
|
||||
computed: {
|
||||
version: () => version,
|
||||
isDarkMode: () => getters.isDarkMode(),
|
||||
isLoggedIn: () => getters.isLoggedIn(),
|
||||
isSettings: () => getters.isSettings(),
|
||||
active: () => getters.isSidebarVisible(),
|
||||
},
|
||||
methods: {
|
||||
// Show the help overlay
|
||||
help() {
|
||||
mutations.showHover("help");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.sidebar-scroll-list {
|
||||
overflow: auto;
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
#sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1em;
|
||||
width: 20em;
|
||||
position: fixed;
|
||||
z-index: 4;
|
||||
left: -20em;
|
||||
height: 100%;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
transition: 0.5s ease;
|
||||
top: 4em;
|
||||
padding-bottom: 4em;
|
||||
background-color: rgb(255 255 255 / 50%) !important;
|
||||
}
|
||||
#sidebar.dark-mode {
|
||||
background-color: rgb(37 49 55 / 33%) !important;
|
||||
}
|
||||
|
||||
#sidebar.sticky {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
@supports (backdrop-filter: none) {
|
||||
nav {
|
||||
backdrop-filter: blur(16px) invert(0.1);
|
||||
}
|
||||
}
|
||||
|
||||
body.rtl nav {
|
||||
left: unset;
|
||||
right: -17em;
|
||||
}
|
||||
|
||||
#sidebar.active {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#sidebar.rtl nav.active {
|
||||
left: unset;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#sidebar .action {
|
||||
width: 100%;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
body.rtl .action {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#sidebar .action > * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* * * * * * * * * * * * * * * *
|
||||
* FOOTER *
|
||||
* * * * * * * * * * * * * * * */
|
||||
|
||||
.credits {
|
||||
font-size: 1em;
|
||||
color: var(--textSecondary);
|
||||
padding-left: 1em;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.credits > span {
|
||||
display: block;
|
||||
margin-top: 0.5em;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.credits a,
|
||||
.credits a:hover {
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.buffer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1em !important;
|
||||
min-height: 4em;
|
||||
box-shadow: 0 2px 2px #00000024, 0 1px 5px #0000001f, 0 3px 1px -2px #0003;
|
||||
/* overflow: auto; */
|
||||
border-radius: 1em;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.clickable:hover {
|
||||
box-shadow: 0 2px 2px #00000024, 0 1px 5px #0000001f, 0 3px 1px -2px #0003;
|
||||
}
|
||||
</style>
|
||||
+88
-12
@@ -1,12 +1,4 @@
|
||||
/* Basic Styles */
|
||||
:root {
|
||||
--background: white;
|
||||
--surfacePrimary: gray;
|
||||
--surfaceSecondary: lightgray;
|
||||
--textPrimary: white;
|
||||
--textSecondary: gray;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Roboto", sans-serif;
|
||||
padding-top: 4em;
|
||||
@@ -74,6 +66,57 @@ over
|
||||
padding-bottom: 4em;
|
||||
}
|
||||
|
||||
/* Navigation Styles */
|
||||
nav {
|
||||
width: 18em;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding-top: 4em;
|
||||
left: -19em;
|
||||
z-index: 4;
|
||||
background: #fff;
|
||||
height: 100%;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
transition: .1s ease left;
|
||||
}
|
||||
|
||||
body.rtl nav {
|
||||
left: unset;
|
||||
right: -17em;
|
||||
}
|
||||
|
||||
nav.active {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
body.rtl nav.active {
|
||||
left: unset;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
nav > div {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
nav .action {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: 0;
|
||||
font-size: 1.1em;
|
||||
padding: 0.5em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
body.rtl .action {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
nav .action > * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
main {
|
||||
@@ -145,6 +188,22 @@ body.rtl .breadcrumbs a {
|
||||
|
||||
|
||||
|
||||
/* File Selection */
|
||||
#file-selection {
|
||||
box-shadow: rgba(0, 0, 0, 0.3) 0px 2em 50px 10px;
|
||||
position: fixed;
|
||||
bottom: 1em;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
max-width: 30em;
|
||||
z-index: 1;
|
||||
border-radius: 1em;
|
||||
display: flex;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
height: 3em;
|
||||
@@ -152,13 +211,31 @@ button {
|
||||
border: none;
|
||||
background-color: #f5f5f5;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
/* Add borders */
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
@media (min-width: 800px) {
|
||||
#file-selection {
|
||||
bottom: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
#file-selection .action {
|
||||
border-radius: 50%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#file-selection > span {
|
||||
display: inline-block;
|
||||
margin-left: 1em;
|
||||
color: #6f6f6f;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
#file-selection .action span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#popup-notification {
|
||||
@@ -172,7 +249,6 @@ button:disabled {
|
||||
padding: 1em;
|
||||
align-items: center;
|
||||
transition: right 1s ease; /* Animate the 'right' property */
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
#popup-notification-content {
|
||||
|
||||
@@ -226,6 +226,15 @@
|
||||
background: var(--surfaceSecondary) !important;
|
||||
}
|
||||
|
||||
/* File selection */
|
||||
.dark-mode #file-selection {
|
||||
background: var(--surfaceSecondary) !important;
|
||||
}
|
||||
|
||||
.dark-mode #file-selection span {
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
|
||||
/* Dropdown */
|
||||
.dark-mode #dropdown {
|
||||
background: var(--surfaceSecondary) !important;
|
||||
|
||||
@@ -300,10 +300,11 @@ body.rtl .card .card-title>*:first-child {
|
||||
.overlay {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
position: fixed;
|
||||
top: 4em;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 4;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
animation: .3s show ease-in;
|
||||
}
|
||||
|
||||
@@ -483,4 +484,4 @@ body.rtl .card .card-title>*:first-child {
|
||||
body.rtl .credits {
|
||||
text-align: right;
|
||||
direction: ltr;
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,16 @@ header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: rgb(255 255 255 / 50%) !important;
|
||||
background-color: white;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.075);
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
@supports (backdrop-filter: none) {
|
||||
header {
|
||||
backdrop-filter: blur(16px);
|
||||
background-color: transparent;
|
||||
backdrop-filter: blur(16px) invert(0.1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,10 @@ body.rtl #listingView {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#listingView .item .name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#listingView .item i {
|
||||
font-size: 4em;
|
||||
margin-right: 0.1em;
|
||||
@@ -99,6 +103,7 @@ body.rtl #listingView {
|
||||
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
||||
}
|
||||
|
||||
|
||||
#listingView.list .item,
|
||||
#listingView.compact .item {
|
||||
max-width: 100%;
|
||||
@@ -117,49 +122,25 @@ body.rtl #listingView {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#listingView.gallery .item div {
|
||||
#listingView.gallery .item div:first-of-type {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
padding: 0.5em;
|
||||
}
|
||||
#listingView.gallery .item {
|
||||
min-width: 12em;
|
||||
min-height: 12em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#listingView.normal .item,
|
||||
#listingView.gallery .item {
|
||||
transition: width 0.25s, height 0.25s;
|
||||
}
|
||||
|
||||
#listingView.gallery .item .text {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display:flex;
|
||||
min-width: 12em;
|
||||
min-height: 12em;
|
||||
text-shadow: 0 0 2px black;
|
||||
height: 12em;
|
||||
}
|
||||
|
||||
#listingView.gallery .item div:last-of-type {
|
||||
position: absolute;
|
||||
bottom: 0.5em;
|
||||
padding: 1em;
|
||||
width: calc(100% - 1em);
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#listingView.gallery .item[data-type=image] div:last-of-type {
|
||||
color: white;
|
||||
background: linear-gradient(#0000, #0009);
|
||||
}
|
||||
|
||||
#listingView.gallery .item i {
|
||||
display:flex !important;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
font-size: 8em;
|
||||
@@ -256,6 +237,7 @@ body.rtl #listingView {
|
||||
#listingView.compact .header,
|
||||
#listingView.list .header {
|
||||
border: 1px solid rgba(0, 0, 0, .1);
|
||||
border-color: var(--divider);
|
||||
}
|
||||
|
||||
#listingView.compact .header>div:first-child {
|
||||
@@ -333,9 +315,8 @@ body.rtl #listingView {
|
||||
|
||||
#listingView .header {
|
||||
display: none;
|
||||
background: white;
|
||||
background: var(--surfacePrimary);
|
||||
border-radius: 1em;
|
||||
border: 1px solid rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
#listingView.list .header i {
|
||||
@@ -353,7 +334,9 @@ body.rtl #listingView {
|
||||
padding: .85em;
|
||||
width: 100%;
|
||||
}
|
||||
#listingView.compact .header {
|
||||
|
||||
}
|
||||
#listingView.list .item:first-child {
|
||||
margin-top: .5em;
|
||||
border-top-left-radius: 1em;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
@import "./header.css";
|
||||
@import "./listing.css";
|
||||
@import "./listing-icons.css";
|
||||
@import "./upload-files.css";
|
||||
@import "./dashboard.css";
|
||||
@import "./login.css";
|
||||
@import './mobile.css';
|
||||
@@ -89,7 +90,7 @@ main .spinner .bounce2 {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.action:not(:disabled):hover {
|
||||
.action:hover {
|
||||
background-color: rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
@@ -304,6 +305,44 @@ body.rtl .breadcrumbs .chevron {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * *
|
||||
* PROMPT *
|
||||
* * * * * * * * * * * * * * * */
|
||||
|
||||
.noty_buttons {
|
||||
text-align: right;
|
||||
padding: 0 10px 10px !important;
|
||||
}
|
||||
|
||||
.noty_buttons button {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
box-shadow: 0 0 0 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * *
|
||||
* FOOTER *
|
||||
* * * * * * * * * * * * * * * */
|
||||
|
||||
.credits {
|
||||
font-size: 1em;
|
||||
color: #a5a5a5;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.credits > span {
|
||||
display: block;
|
||||
margin-top: .5em;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.credits a,
|
||||
.credits a:hover {
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
/* * * * * * * * * * * * * * * *
|
||||
* ANIMATIONS *
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
.upload-files .card.floating {
|
||||
left: auto;
|
||||
top: auto;
|
||||
margin: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.upload-files .file {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-files .file .file-name {
|
||||
font-size: 1.1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.upload-files .file .file-name i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.upload-files .file .file-progress {
|
||||
margin-top: 2px;
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
.upload-files .file .file-progress div {
|
||||
height: 100%;
|
||||
background-color: #40c4ff;
|
||||
width: 0;
|
||||
transition: 0.2s ease width;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.upload-files.closed .card-content {
|
||||
display: none;
|
||||
padding: 0em 1em 1em 1em;
|
||||
}
|
||||
|
||||
.upload-files .card .card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.8em;
|
||||
padding: 1em 1em 0em;
|
||||
}
|
||||
|
||||
.upload-files.closed .card-title {
|
||||
font-size: 0.7em;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
@media (max-width: 450px) {
|
||||
.upload-files .card.floating {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,12 @@ import Login from "@/views/Login.vue";
|
||||
import Layout from "@/views/Layout.vue";
|
||||
import Files from "@/views/Files.vue";
|
||||
import Share from "@/views/Share.vue";
|
||||
import Users from "@/views/settings/Users.vue";
|
||||
import User from "@/views/settings/User.vue";
|
||||
import Settings from "@/views/Settings.vue";
|
||||
import GlobalSettings from "@/views/settings/Global.vue";
|
||||
import ProfileSettings from "@/views/settings/Profile.vue";
|
||||
import Shares from "@/views/settings/Shares.vue";
|
||||
import Errors from "@/views/Errors.vue";
|
||||
import { baseURL, name } from "@/utils/constants";
|
||||
import { getters, state } from "@/store";
|
||||
@@ -69,6 +74,45 @@ const routes = [
|
||||
path: "",
|
||||
name: "Settings",
|
||||
component: Settings,
|
||||
redirect: {
|
||||
path: "/settings/profile",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "profile",
|
||||
name: "ProfileSettings",
|
||||
component: ProfileSettings,
|
||||
},
|
||||
{
|
||||
path: "shares",
|
||||
name: "Shares",
|
||||
component: Shares,
|
||||
},
|
||||
{
|
||||
path: "global",
|
||||
name: "GlobalSettings",
|
||||
component: GlobalSettings,
|
||||
meta: {
|
||||
requiresAdmin: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "users",
|
||||
name: "Users",
|
||||
component: Users,
|
||||
meta: {
|
||||
requiresAdmin: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "users/:id",
|
||||
name: "User",
|
||||
component: User,
|
||||
meta: {
|
||||
requiresAdmin: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -134,7 +178,6 @@ async function initAuth() {
|
||||
}
|
||||
|
||||
router.beforeResolve(async (to, from, next) => {
|
||||
mutations.closeHovers()
|
||||
const title = i18n.global.t(titles[to.name as keyof typeof titles]);
|
||||
document.title = title + " - " + name;
|
||||
mutations.setRoute(to)
|
||||
|
||||
+17
-111
@@ -1,20 +1,13 @@
|
||||
import { state } from "./state.js";
|
||||
|
||||
export const getters = {
|
||||
isResizableView: () => (state.user.viewMode == "gallery" || state.user.viewMode == "normal" ) && getters.currentView() == "listingView" ,
|
||||
currentHash: () => state.route.hash.replace("#", ""),
|
||||
isMobile: () => state.isMobile,
|
||||
isLoading: () => Object.keys(state.loading).length > 0,
|
||||
isSettings: () => getters.currentView() === "settings",
|
||||
isDarkMode: () => {
|
||||
if (state.user == null) {
|
||||
return true;
|
||||
}
|
||||
return state.user.darkMode === true;
|
||||
},
|
||||
isLoggedIn: () => {
|
||||
return state.user !== null && state.user?.username != undefined && state.user?.username != "publicUser";
|
||||
},
|
||||
isLoggedIn: () => state.user !== null,
|
||||
isAdmin: () => state.user.perm?.admin == true,
|
||||
isFiles: () => state.route.name === "Files",
|
||||
isListing: () => getters.isFiles() && state.req.isDir,
|
||||
@@ -24,110 +17,23 @@ export const getters = {
|
||||
let selectedItem = state.selected[0]
|
||||
return state.req.items[selectedItem].url;
|
||||
},
|
||||
reqNumDirs: () => {
|
||||
let dirCount = 0;
|
||||
state.req.items.forEach((item) => {
|
||||
// Check if the item is a directory
|
||||
if (item.isDir) {
|
||||
// If hideDotfiles is enabled and the item is a dotfile, skip it
|
||||
if (state.user.hideDotfiles && item.name.startsWith(".")) {
|
||||
return;
|
||||
}
|
||||
// Otherwise, count this directory
|
||||
dirCount++;
|
||||
}
|
||||
});
|
||||
// Return the directory count
|
||||
return dirCount;
|
||||
},
|
||||
reqNumFiles: () => {
|
||||
let fileCount = 0;
|
||||
state.req.items.forEach((item) => {
|
||||
// Check if the item is a directory
|
||||
if (!item.isDir) {
|
||||
// If hideDotfiles is enabled and the item is a dotfile, skip it
|
||||
if (state.user.hideDotfiles && item.name.startsWith(".")) {
|
||||
return;
|
||||
}
|
||||
// Otherwise, count this directory
|
||||
fileCount++;
|
||||
}
|
||||
});
|
||||
// Return the directory count
|
||||
return fileCount;
|
||||
},
|
||||
reqItems: () => {
|
||||
if (state.user == null) {
|
||||
return {};
|
||||
}
|
||||
const dirs = [];
|
||||
const files = [];
|
||||
|
||||
state.req.items.forEach((item) => {
|
||||
if (state.user.hideDotfiles && item.name.startsWith(".")) {
|
||||
return;
|
||||
}
|
||||
if (item.isDir) {
|
||||
dirs.push(item);
|
||||
} else {
|
||||
item.Path = state.req.Path;
|
||||
files.push(item);
|
||||
}
|
||||
});
|
||||
return { dirs, files };
|
||||
},
|
||||
isSidebarVisible: () => {
|
||||
let visible = state.showSidebar || getters.isStickySidebar()
|
||||
if (getters.currentView() == "settings") {
|
||||
visible = !getters.isMobile();
|
||||
}
|
||||
if (typeof getters.currentPromptName() === "string" && !getters.isStickySidebar()) {
|
||||
visible = false;
|
||||
}
|
||||
return visible
|
||||
},
|
||||
isStickySidebar: () => {
|
||||
let sticky = state.user?.stickySidebar
|
||||
if (getters.currentView() == "settings") {
|
||||
sticky = true
|
||||
}
|
||||
if (getters.currentView() == null && !getters.isLoading()) {
|
||||
sticky = true
|
||||
}
|
||||
if (getters.isMobile() || getters.currentView() == "preview") {
|
||||
sticky = false
|
||||
}
|
||||
return sticky
|
||||
},
|
||||
showOverlay: () => {
|
||||
if (!getters.isLoggedIn()) {
|
||||
return false
|
||||
}
|
||||
const hasPrompt = getters.currentPrompt() !== null && getters.currentPromptName() !== "more";
|
||||
const shouldOverlaySidebar = getters.isSidebarVisible() && !getters.isStickySidebar()
|
||||
return hasPrompt || shouldOverlaySidebar;
|
||||
},
|
||||
getRoutePath: () => {
|
||||
return state.route.path.endsWith("/")
|
||||
? state.route.path
|
||||
: state.route.path + "/";
|
||||
},
|
||||
currentView: () => {
|
||||
const pathname = state.route.path.toLowerCase()
|
||||
if (pathname.includes("settings")) {
|
||||
return "settings"
|
||||
} else if (pathname.includes("files")) {
|
||||
if (state.req.type !== undefined) {
|
||||
if (state.req.isDir) {
|
||||
return "listingView";
|
||||
} else if ("content" in state.req) {
|
||||
return "editor";
|
||||
} else {
|
||||
return "preview";
|
||||
}
|
||||
let returnVal = null;
|
||||
if (state.req.type !== undefined) {
|
||||
if (state.req.isDir) {
|
||||
returnVal = "listingView";
|
||||
} else if ("content" in state.req) {
|
||||
returnVal = "editor";
|
||||
} else {
|
||||
returnVal = "preview";
|
||||
}
|
||||
}
|
||||
return null
|
||||
return returnVal;
|
||||
},
|
||||
progress: () => {
|
||||
// Check if state.upload is defined and valid
|
||||
@@ -151,8 +57,10 @@ export const getters = {
|
||||
},
|
||||
|
||||
filesInUploadCount: () => {
|
||||
const uploadsCount = state.upload.length
|
||||
const queueCount = state.queue.length
|
||||
// Ensure state.upload.uploads is an object and state.upload.queue is an array
|
||||
const uploadsCount = typeof state.upload.uploads === 'object' ? Object.keys(state.upload.uploads).length : 0;
|
||||
const queueCount = Array.isArray(state.upload.queue) ? state.upload.queue.length : 0;
|
||||
|
||||
return uploadsCount + queueCount;
|
||||
},
|
||||
|
||||
@@ -169,15 +77,13 @@ export const getters = {
|
||||
|
||||
currentPromptName: () => {
|
||||
// Ensure state.prompts is an array
|
||||
if (!Array.isArray(state.prompts) || state.prompts.length === 0) {
|
||||
if (!Array.isArray(state.prompts)) {
|
||||
return null;
|
||||
}
|
||||
// Check if the name property is a string
|
||||
const lastPrompt = state.prompts[state.prompts.length - 1];
|
||||
if (typeof lastPrompt?.name !== "string") {
|
||||
if (state.prompts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return lastPrompt.name;
|
||||
return state.prompts[state.prompts.length - 1].name;
|
||||
},
|
||||
|
||||
filesInUpload: () => {
|
||||
|
||||
@@ -1,68 +1,18 @@
|
||||
import * as i18n from "@/i18n";
|
||||
import { state } from "./state.js";
|
||||
import router from "@/router";
|
||||
import { emitStateChanged } from './eventBus'; // Import the function from eventBus.js
|
||||
import { users } from "@/api";
|
||||
|
||||
export const mutations = {
|
||||
setGallerySize: (value) => {
|
||||
state.user.gallerySize = value
|
||||
emitStateChanged();
|
||||
},
|
||||
setActiveSettingsView: (value) => {
|
||||
state.activeSettingsView = value;
|
||||
router.push({ hash: "#" + value });
|
||||
const element = document.getElementById(value);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
inline: "nearest",
|
||||
});
|
||||
}
|
||||
emitStateChanged();
|
||||
},
|
||||
setSettings: (value) => {
|
||||
state.settings = value;
|
||||
emitStateChanged();
|
||||
},
|
||||
setMobile() {
|
||||
state.isMobile = window.innerWidth <= 800
|
||||
emitStateChanged();
|
||||
},
|
||||
toggleDarkMode() {
|
||||
mutations.updateUser({ "darkMode": !state.user.darkMode });
|
||||
emitStateChanged();
|
||||
},
|
||||
toggleSidebar() {
|
||||
if (state.user.stickySidebar) {
|
||||
localStorage.setItem("stickySidebar", "false");
|
||||
mutations.updateUser({ "stickySidebar": false }); // turn off sticky when closed
|
||||
state.showSidebar = false;
|
||||
} else {
|
||||
state.showSidebar = !state.showSidebar;
|
||||
}
|
||||
emitStateChanged();
|
||||
},
|
||||
closeSidebar() {
|
||||
if (state.showSidebar) {
|
||||
state.showSidebar = false;
|
||||
emitStateChanged();
|
||||
}
|
||||
},
|
||||
setUpload(value) {
|
||||
state.upload = value;
|
||||
emitStateChanged();
|
||||
},
|
||||
setUsage: (value) => {
|
||||
state.usage = value;
|
||||
emitStateChanged();
|
||||
},
|
||||
closeHovers: () => {
|
||||
state.prompts = [];
|
||||
if (!state.stickySidebar) {
|
||||
state.showSidebar = false;
|
||||
}
|
||||
emitStateChanged();
|
||||
},
|
||||
toggleShell: () => {
|
||||
state.showShell = !state.showShell;
|
||||
emitStateChanged();
|
||||
},
|
||||
showHover: (value) => {
|
||||
@@ -87,14 +37,10 @@ export const mutations = {
|
||||
state.prompts.push("error");
|
||||
emitStateChanged();
|
||||
},
|
||||
setLoading: (loadType, status) => {
|
||||
if (status === false) {
|
||||
delete state.loading[loadType];
|
||||
} else {
|
||||
state.loading = { ...state.loading, [loadType]: true };
|
||||
}
|
||||
setLoading: (value) => {
|
||||
state.loading = value;
|
||||
emitStateChanged();
|
||||
},
|
||||
},
|
||||
setReload: (value) => {
|
||||
state.reload = value;
|
||||
emitStateChanged();
|
||||
@@ -108,13 +54,11 @@ export const mutations = {
|
||||
|
||||
let locale = value.locale;
|
||||
if (locale === "") {
|
||||
value.locale = i18n.detectLocale();
|
||||
locale = i18n.detectLocale();
|
||||
}
|
||||
let previousUser = state.user
|
||||
i18n.setLocale(locale);
|
||||
i18n.default.locale = locale;
|
||||
state.user = value;
|
||||
if (state.user != previousUser && state.user.username != "publicUser") {
|
||||
users.update(state.user);
|
||||
}
|
||||
emitStateChanged();
|
||||
},
|
||||
setJWT: (value) => {
|
||||
@@ -149,16 +93,11 @@ export const mutations = {
|
||||
if (state.user === null) {
|
||||
state.user = {};
|
||||
}
|
||||
let previousUser = state.user;
|
||||
state.user = { ...state.user, ...value };
|
||||
if (state.user.locale !== previousUser.locale) {
|
||||
state.user.locale = i18n.detectLocale();
|
||||
i18n.setLocale(state.user.locale);
|
||||
i18n.default.locale = state.user.locale;
|
||||
}
|
||||
localStorage.setItem("stickySidebar", state.user.stickySidebar);
|
||||
if (state.user != previousUser) {
|
||||
users.update(state.user);
|
||||
for (let field in value) {
|
||||
if (field === "locale") {
|
||||
i18n.setLocale(value[field]);
|
||||
}
|
||||
state.user[field] = value[field];
|
||||
}
|
||||
emitStateChanged();
|
||||
},
|
||||
@@ -167,6 +106,7 @@ export const mutations = {
|
||||
state.oldReq = state.req;
|
||||
state.req = value;
|
||||
state.selected = [];
|
||||
|
||||
if (!state.req?.items) return;
|
||||
state.selected = state.req.items
|
||||
.filter((item) => selectedItems.some((rItem) => rItem.url === item.url))
|
||||
|
||||
@@ -2,9 +2,6 @@ import { reactive } from 'vue';
|
||||
import { detectLocale } from "@/i18n";
|
||||
|
||||
export const state = reactive({
|
||||
activeSettingsView: "",
|
||||
isMobile: window.innerWidth <= 800,
|
||||
showSidebar: false,
|
||||
usage: {
|
||||
used: "0 B",
|
||||
total: "0 B",
|
||||
@@ -12,10 +9,8 @@ export const state = reactive({
|
||||
},
|
||||
editor: null,
|
||||
user: {
|
||||
gallarySize: 0,
|
||||
stickySidebar: stickyStartup(),
|
||||
locale: detectLocale(), // Default to the locale from moment
|
||||
viewMode: 'normal', // Default to mosaic view
|
||||
viewMode: 'mosaic', // Default to mosaic view
|
||||
hideDotfiles: false, // Default to false, assuming this is a boolean
|
||||
perm: {},
|
||||
rules: [], // Default to an empty array
|
||||
@@ -42,35 +37,18 @@ export const state = reactive({
|
||||
items: [],
|
||||
},
|
||||
jwt: "",
|
||||
loading: [],
|
||||
progress: 0,
|
||||
loading: false,
|
||||
reload: false,
|
||||
selected: [],
|
||||
multiple: false,
|
||||
upload: {
|
||||
uploads: {},
|
||||
queue: [],
|
||||
progress: [],
|
||||
sizes: [],
|
||||
progress: [], // Array of progress values
|
||||
sizes: [], // Array of sizes
|
||||
},
|
||||
prompts: [],
|
||||
show: null,
|
||||
showShell: false,
|
||||
showConfirm: null,
|
||||
route: {},
|
||||
settings: {
|
||||
signup: false,
|
||||
createUserDir: false,
|
||||
userHomeBasePath: "",
|
||||
rules: [],
|
||||
frontend: {
|
||||
disableExternal: false,
|
||||
disableUsedPercentage: false,
|
||||
name: "",
|
||||
files: "",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function stickyStartup() {
|
||||
const stickyStatus = localStorage.getItem("stickySidebar");
|
||||
return stickyStatus == "true"
|
||||
}
|
||||
@@ -8,7 +8,6 @@ const recaptcha = window.FileBrowser.ReCaptcha;
|
||||
const recaptchaKey = window.FileBrowser.ReCaptchaKey;
|
||||
const signup = window.FileBrowser.Signup;
|
||||
const version = window.FileBrowser.Version;
|
||||
const commitSHA = window.FileBrowser.CommitSHA;
|
||||
const logoURL = `${staticURL}/img/logo.png`;
|
||||
const noAuth = window.FileBrowser.NoAuth;
|
||||
const authMethod = window.FileBrowser.AuthMethod;
|
||||
@@ -18,13 +17,6 @@ const resizePreview = window.FileBrowser.ResizePreview;
|
||||
const enableExec = window.FileBrowser.EnableExec;
|
||||
const origin = window.location.origin;
|
||||
|
||||
const settings = [
|
||||
{ id: 'profile', label: 'Profile Management', component: 'ProfileSettings' },
|
||||
{ id: 'shares', label: 'Share Management', component: 'SharesSettings' },
|
||||
{ id: 'global', label: 'Global', component: 'GlobalSettings' },
|
||||
{ id: 'user-defaults', label: 'User Defaults', component: 'UserDefaultSettings' },
|
||||
]
|
||||
|
||||
export {
|
||||
name,
|
||||
disableExternal,
|
||||
@@ -35,7 +27,6 @@ export {
|
||||
recaptchaKey,
|
||||
signup,
|
||||
version,
|
||||
commitSHA,
|
||||
noAuth,
|
||||
authMethod,
|
||||
loginPage,
|
||||
@@ -43,6 +34,5 @@ export {
|
||||
resizePreview,
|
||||
enableExec,
|
||||
origin,
|
||||
darkMode,
|
||||
settings
|
||||
darkMode
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { state } from "@/store";
|
||||
import url from "@/utils/url";
|
||||
import { files as api } from "@/api";
|
||||
|
||||
export function checkConflict(files, items) {
|
||||
if (typeof items === "undefined" || items === null) {
|
||||
@@ -102,9 +101,10 @@ export function scanFiles(dt) {
|
||||
}
|
||||
|
||||
export function handleFiles(files, base, overwrite = false) {
|
||||
for (const file of files) {
|
||||
const id = state.upload.id;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let id = state.upload.id;
|
||||
let path = base;
|
||||
let file = files[i];
|
||||
|
||||
if (file.fullPath !== undefined) {
|
||||
path += url.encodePath(file.fullPath);
|
||||
@@ -119,19 +119,11 @@ export function handleFiles(files, base, overwrite = false) {
|
||||
const item = {
|
||||
id,
|
||||
path,
|
||||
file: file.file, // Ensure `file.file` is the Blob or File
|
||||
file,
|
||||
overwrite,
|
||||
...(!file.isDir && { type: file.type }),
|
||||
};
|
||||
|
||||
// Upload the file using your API
|
||||
api.post(item.path, item.file, item.overwrite, (event) => {
|
||||
console.log(`Upload progress: ${Math.round((event.loaded / event.total) * 100)}%`);
|
||||
})
|
||||
.then(response => {
|
||||
console.log("Upload successful:", response);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Upload error:", error);
|
||||
});
|
||||
state.dispatch("upload/upload", item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { files as api } from "@/api";
|
||||
|
||||
@@ -50,7 +49,7 @@ export default {
|
||||
return getters.currentView() !== null;
|
||||
},
|
||||
reload() {
|
||||
return state.reload;
|
||||
return state.reload; // Access reload from state
|
||||
},
|
||||
},
|
||||
created() {
|
||||
@@ -59,7 +58,8 @@ export default {
|
||||
watch: {
|
||||
$route: "fetchData",
|
||||
reload(value) {
|
||||
if (value) {
|
||||
if (value === true) {
|
||||
console.log("reloading")
|
||||
this.fetchData();
|
||||
}
|
||||
},
|
||||
@@ -71,20 +71,23 @@ export default {
|
||||
window.removeEventListener("keydown", this.keyEvent);
|
||||
},
|
||||
unmounted() {
|
||||
if (state.showShell) {
|
||||
mutations.toggleShell(); // Use mutation
|
||||
}
|
||||
mutations.replaceRequest({}); // Use mutation
|
||||
},
|
||||
methods: {
|
||||
async fetchData() {
|
||||
// Set loading to true and reset the error.
|
||||
mutations.setLoading("files", true);
|
||||
this.error = null;
|
||||
|
||||
// Reset view information using mutations
|
||||
mutations.setReload(false);
|
||||
mutations.resetSelected();
|
||||
mutations.setMultiple(false);
|
||||
mutations.closeHovers();
|
||||
|
||||
// Set loading to true and reset the error.
|
||||
mutations.setLoading(true);
|
||||
this.error = null;
|
||||
|
||||
let url = state.route.path;
|
||||
if (url === "") url = "/";
|
||||
if (url[0] !== "/") url = "/" + url;
|
||||
@@ -103,11 +106,9 @@ export default {
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = e;
|
||||
mutations.replaceRequest(null);
|
||||
} finally {
|
||||
mutations.replaceRequest(data);
|
||||
mutations.setLoading("files", false);
|
||||
}
|
||||
mutations.setLoading(false);
|
||||
mutations.replaceRequest(data);
|
||||
},
|
||||
keyEvent(event) {
|
||||
// F1!
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template v-if="isLoggedIn">
|
||||
<template>
|
||||
<div>
|
||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||
<div v-if="progress" class="progress">
|
||||
@@ -14,27 +14,24 @@
|
||||
></editorBar>
|
||||
<defaultBar :class="{ 'dark-mode-header': isDarkMode }" v-else></defaultBar>
|
||||
<sidebar></sidebar>
|
||||
<search v-if="showSearch"></search>
|
||||
<main :class="{ 'dark-mode': isDarkMode, moveWithSidebar: moveWithSidebar }">
|
||||
<main :class="{ 'dark-mode': isDarkMode }">
|
||||
<router-view></router-view>
|
||||
</main>
|
||||
<prompts :class="{ 'dark-mode': isDarkMode }"></prompts>
|
||||
<upload-files></upload-files>
|
||||
</div>
|
||||
<div class="card" id="popup-notification">
|
||||
<i v-on:click="closePopUp" class="material-icons">close</i>
|
||||
<div id="popup-notification-content">no info</div>
|
||||
</div>
|
||||
<fileSelection> </fileSelection>
|
||||
</template>
|
||||
<script>
|
||||
import editorBar from "./bars/EditorBar.vue";
|
||||
import defaultBar from "./bars/Default.vue";
|
||||
import listingBar from "./bars/ListingBar.vue";
|
||||
import Prompts from "@/components/prompts/Prompts.vue";
|
||||
import Sidebar from "@/components/sidebar/Sidebar.vue";
|
||||
import Search from "@/components/Search.vue";
|
||||
import fileSelection from "@/components/FileSelection.vue";
|
||||
|
||||
import Sidebar from "@/components/Sidebar.vue";
|
||||
import UploadFiles from "../components/prompts/UploadFiles.vue";
|
||||
import { closePopUp } from "@/notify";
|
||||
import { enableExec } from "@/utils/constants";
|
||||
import { state, getters, mutations } from "@/store";
|
||||
@@ -42,13 +39,12 @@ import { state, getters, mutations } from "@/store";
|
||||
export default {
|
||||
name: "layout",
|
||||
components: {
|
||||
fileSelection,
|
||||
Search,
|
||||
defaultBar,
|
||||
editorBar,
|
||||
listingBar,
|
||||
Sidebar,
|
||||
Prompts,
|
||||
UploadFiles,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -58,19 +54,7 @@ export default {
|
||||
itemWeight: 0,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("resize", this.updateIsMobile);
|
||||
},
|
||||
computed: {
|
||||
showSearch() {
|
||||
return getters.isLoggedIn() && this.currentView == "listingView";
|
||||
},
|
||||
isLoggedIn() {
|
||||
return getters.isLoggedIn();
|
||||
},
|
||||
moveWithSidebar() {
|
||||
return getters.isSidebarVisible() && getters.isStickySidebar();
|
||||
},
|
||||
closePopUp() {
|
||||
return closePopUp;
|
||||
},
|
||||
@@ -93,7 +77,7 @@ export default {
|
||||
return state.user; // Access state directly from the store
|
||||
},
|
||||
showOverlay() {
|
||||
return getters.showOverlay();
|
||||
return getters.currentPrompt() !== null && getters.currentPromptName() !== "more";
|
||||
},
|
||||
isDarkMode() {
|
||||
return getters.isDarkMode();
|
||||
@@ -107,9 +91,6 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
if (!getters.isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
mutations.resetSelected();
|
||||
mutations.setMultiple(false);
|
||||
if (getters.currentPromptName() !== "success") {
|
||||
@@ -118,13 +99,16 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateIsMobile() {
|
||||
mutations.setMobile();
|
||||
},
|
||||
resetPrompts() {
|
||||
mutations.closeSidebar();
|
||||
mutations.closeHovers();
|
||||
},
|
||||
getTitle() {
|
||||
let title = "Title";
|
||||
if (state.route.path.startsWith("/settings/")) {
|
||||
title = "Settings";
|
||||
}
|
||||
return title;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -133,13 +117,7 @@ export default {
|
||||
main {
|
||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
transition: 0.5s ease;
|
||||
}
|
||||
|
||||
main.moveWithSidebar {
|
||||
padding-left: 21em;
|
||||
}
|
||||
|
||||
main::-webkit-scrollbar {
|
||||
display: none; /* Safari and Chrome */
|
||||
}
|
||||
@@ -152,13 +130,13 @@ main::-webkit-scrollbar {
|
||||
/* Header */
|
||||
.dark-mode-header {
|
||||
color: white;
|
||||
background-color: rgb(255 255 255 / 50%) !important;
|
||||
background: var(--surfacePrimary);
|
||||
}
|
||||
|
||||
/* Header with backdrop-filter support */
|
||||
@supports (backdrop-filter: none) {
|
||||
.dark-mode-header {
|
||||
background-color: rgb(37 49 55 / 33%) !important;
|
||||
background: transparent;
|
||||
backdrop-filter: blur(16px) invert(0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<div class="settings-views">
|
||||
<div
|
||||
v-for="setting in settings"
|
||||
:key="setting.id + '-main'"
|
||||
:id="setting.id + '-main'"
|
||||
:class="{
|
||||
active: active(setting.id + '-main'),
|
||||
clickable: !active(setting.id + '-main'),
|
||||
}"
|
||||
@click="!active(setting.id + '-main') && setView(setting.id + '-main')"
|
||||
>
|
||||
<!-- Dynamically render the component based on the setting -->
|
||||
<component :is="setting.component"></component>
|
||||
<div id="nav">
|
||||
<div v-if="settingsEnabled" class="wrapper">
|
||||
<ul>
|
||||
<router-link to="/settings/profile"
|
||||
><li :class="{ active: $route.path === '/settings/profile' }">
|
||||
{{ $t("settings.profileSettings") }}
|
||||
</li></router-link
|
||||
>
|
||||
<router-link to="/settings/shares" v-if="user.perm.share"
|
||||
><li :class="{ active: $route.path === '/settings/shares' }">
|
||||
{{ $t("settings.shareManagement") }}
|
||||
</li></router-link
|
||||
>
|
||||
<router-link to="/settings/global" v-if="user.perm.admin"
|
||||
><li :class="{ active: $route.path === '/settings/global' }">
|
||||
{{ $t("settings.globalSettings") }}
|
||||
</li></router-link
|
||||
>
|
||||
<router-link to="/settings/users" v-if="user.perm.admin"
|
||||
><li
|
||||
:class="{
|
||||
active: $route.path === '/settings/users' || $route.name === 'User',
|
||||
}"
|
||||
>
|
||||
{{ $t("settings.userManagement") }}
|
||||
</li></router-link
|
||||
>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,74 +41,30 @@
|
||||
<span>{{ $t("files.loading") }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { state, getters, mutations } from "@/store";
|
||||
import { settings } from "@/utils/constants";
|
||||
import GlobalSettings from "@/views/settings/Global.vue";
|
||||
import UserDefaultSettings from "@/views/settings/UserDefaults.vue";
|
||||
import UserColumnSettings from "@/views/settings/UserColumn.vue";
|
||||
import ProfileSettings from "@/views/settings/Profile.vue";
|
||||
import SharesSettings from "@/views/settings/Shares.vue";
|
||||
import { state, mutations } from "@/store";
|
||||
|
||||
export default {
|
||||
name: "settings",
|
||||
components: {
|
||||
GlobalSettings,
|
||||
UserDefaultSettings,
|
||||
UserColumnSettings,
|
||||
ProfileSettings,
|
||||
SharesSettings,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
settings, // Initialize the settings array in data
|
||||
};
|
||||
mounted() {
|
||||
// Update the req name property
|
||||
mutations.replaceRequest({ name: "Settings" });
|
||||
},
|
||||
computed: {
|
||||
loading() {
|
||||
return getters.isLoading();
|
||||
return state.loading;
|
||||
},
|
||||
user() {
|
||||
return state.user;
|
||||
},
|
||||
currentHash() {
|
||||
return getters.currentHash();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
mutations.setActiveSettingsView(getters.currentHash());
|
||||
},
|
||||
methods: {
|
||||
active(id) {
|
||||
return state.activeSettingsView === id;
|
||||
},
|
||||
setView(view) {
|
||||
if (state.activeSettingsView === view) return;
|
||||
mutations.setActiveSettingsView(view);
|
||||
settingsEnabled() {
|
||||
return state.user.disableSettings == false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
.settings-views {
|
||||
max-width: 1000px;
|
||||
padding-bottom: 35vh;
|
||||
}
|
||||
.settings-views > .active > .card {
|
||||
border-style: solid;
|
||||
opacity: 1;
|
||||
}
|
||||
.settings-views .card {
|
||||
opacity: 0.3;
|
||||
}
|
||||
</style>
|
||||
@@ -207,7 +207,7 @@ export default {
|
||||
return state.req; // Access state directly from the store
|
||||
},
|
||||
loading() {
|
||||
return getters.isLoading(); // Access state directly from the store
|
||||
return state.loading; // Access state directly from the store
|
||||
},
|
||||
multiple() {
|
||||
return state.multiple; // Access state directly from the store
|
||||
@@ -257,12 +257,15 @@ export default {
|
||||
},
|
||||
async fetchData() {
|
||||
// Set loading to true and reset the error.
|
||||
mutations.setLoading("share", true);
|
||||
mutations.setLoading(true);
|
||||
this.error = null;
|
||||
|
||||
// Reset view information.
|
||||
if (!getters.isLoggedIn()) {
|
||||
if (state.user == undefined) {
|
||||
let userData = await api.getPublicUser();
|
||||
mutations.setUser(userData);
|
||||
let req = state.req;
|
||||
req.user = userData;
|
||||
mutations.replaceRequest(req);
|
||||
}
|
||||
mutations.setReload(false);
|
||||
mutations.resetSelected();
|
||||
@@ -273,12 +276,17 @@ export default {
|
||||
if (url === "") url = "/";
|
||||
if (url[0] !== "/") url = "/" + url;
|
||||
|
||||
let file = await api.fetchPub(url, this.password);
|
||||
file.hash = this.hash;
|
||||
this.token = file.token || "";
|
||||
mutations.updateRequest(file);
|
||||
document.title = `${file.name} - ${document.title}`;
|
||||
mutations.setLoading("share", false);
|
||||
try {
|
||||
let file = await api.fetchPub(url, this.password);
|
||||
file.hash = this.hash;
|
||||
this.token = file.token || "";
|
||||
mutations.updateRequest(file);
|
||||
document.title = `${file.name} - ${document.title}`;
|
||||
} catch (e) {
|
||||
this.error = e;
|
||||
} finally {
|
||||
mutations.setLoading(false);
|
||||
}
|
||||
},
|
||||
keyEvent(event) {
|
||||
// Esc!
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<header>
|
||||
<action icon="close" :label="$t('buttons.close')" @action="close()" />
|
||||
<title v-if="isSettings" class="topTitle">Settings</title>
|
||||
<title v-else class="topTitle">{{ req.name }}</title>
|
||||
<title class="topTitle">{{ req.name }}</title>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -16,8 +15,7 @@
|
||||
|
||||
<script>
|
||||
import url from "@/utils/url"
|
||||
import router from "@/router";
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import { state, mutations } from "@/store";
|
||||
import { files as api } from "@/api";
|
||||
import Action from "@/components/header/Action.vue";
|
||||
import css from "@/utils/css";
|
||||
@@ -29,7 +27,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
columnWidth: 350,
|
||||
columnWidth: 280,
|
||||
width: window.innerWidth,
|
||||
itemWeight: 0,
|
||||
viewModes: ["list", "compact", "normal", "gallery"],
|
||||
@@ -37,9 +35,6 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
isSettings() {
|
||||
return getters.isSettings();
|
||||
},
|
||||
// Map state and getters
|
||||
req() {
|
||||
return state.req;
|
||||
@@ -50,6 +45,10 @@ export default {
|
||||
selected() {
|
||||
return state.selected;
|
||||
},
|
||||
|
||||
isSettings() {
|
||||
return state.route.path.includes("/settings/");
|
||||
},
|
||||
nameSorted() {
|
||||
return state.req.sorting.by === "name";
|
||||
},
|
||||
@@ -202,7 +201,7 @@ export default {
|
||||
);
|
||||
let items = css(["#listingView .item", "#listingView .item"]);
|
||||
if (columns === 0) columns = 1;
|
||||
items.style.width = `calc(${100 / columns}%)`;
|
||||
items.style.width = `calc(${100 / columns}% - 1em)`;
|
||||
},
|
||||
action() {
|
||||
if (this.show) {
|
||||
@@ -212,16 +211,22 @@ export default {
|
||||
this.$emit("action");
|
||||
},
|
||||
close() {
|
||||
if (getters.isSettings()) {
|
||||
if (this.isSettings) {
|
||||
// Use this.isSettings to access the computed property
|
||||
router.push({ path: "/files/",hash: "" });
|
||||
this.$router.push({ path: "/files/" }, () => {});
|
||||
mutations.closeHovers();
|
||||
return;
|
||||
}
|
||||
mutations.replaceRequest({});
|
||||
let uri = url.removeLastDir(state.route.path) + "/";
|
||||
router.push({ path: uri });
|
||||
mutations.closeHovers();
|
||||
this.$router.push({ path: uri });
|
||||
},
|
||||
toggleSidebar() {
|
||||
if (state.show == "sidebar") {
|
||||
mutations.closeHovers();
|
||||
} else {
|
||||
mutations.showHover("sidebar");
|
||||
}
|
||||
},
|
||||
base64(name) {
|
||||
return window.btoa(unescape(encodeURIComponent(name)));
|
||||
|
||||
@@ -30,7 +30,7 @@ import { state, mutations } from "@/store";
|
||||
import { eventBus } from "@/store/eventBus";
|
||||
import buttons from "@/utils/buttons";
|
||||
import url from "@/utils/url";
|
||||
import { showError, showSuccess } from "@/notify";
|
||||
import { showError } from "@/notify";
|
||||
|
||||
import Action from "@/components/header/Action.vue";
|
||||
|
||||
@@ -108,10 +108,9 @@ export default {
|
||||
try {
|
||||
eventBus.emit("handleEditorValueRequest", "data");
|
||||
buttons.success(button);
|
||||
showSuccess("File Saved!");
|
||||
} catch (e) {
|
||||
buttons.done(button);
|
||||
showError("Error saving file: ", e);
|
||||
showError(e);
|
||||
}
|
||||
},
|
||||
close() {
|
||||
|
||||
@@ -5,14 +5,13 @@
|
||||
icon="menu"
|
||||
:label="$t('buttons.toggleSidebar')"
|
||||
@action="toggleSidebar()"
|
||||
:disabled="showOverlay"
|
||||
/>
|
||||
<search />
|
||||
<action
|
||||
class="menu-button"
|
||||
icon="grid_view"
|
||||
:label="$t('buttons.switchView')"
|
||||
@action="switchView"
|
||||
:disabled="showOverlay"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
@@ -26,13 +25,16 @@
|
||||
</style>
|
||||
<script>
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import { users, files as api } from "@/api";
|
||||
import Action from "@/components/header/Action.vue";
|
||||
import Search from "@/components/Search.vue";
|
||||
import { showError } from "@/notify";
|
||||
|
||||
export default {
|
||||
name: "listingView",
|
||||
components: {
|
||||
Action,
|
||||
Search,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
@@ -41,9 +43,6 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showOverlay() {
|
||||
return getters.currentPrompt() !== null && getters.currentPromptName() !== "more";
|
||||
},
|
||||
viewIcon() {
|
||||
const icons = {
|
||||
list: "view_module",
|
||||
@@ -72,15 +71,21 @@ export default {
|
||||
}
|
||||
},
|
||||
toggleSidebar() {
|
||||
mutations.toggleSidebar();
|
||||
if (getters.currentPromptName() === "sidebar") {
|
||||
mutations.closeHovers();
|
||||
} else {
|
||||
mutations.showHover("sidebar");
|
||||
}
|
||||
},
|
||||
async switchView() {
|
||||
mutations.closeHovers();
|
||||
const currentIndex = this.viewModes.indexOf(state.user.viewMode);
|
||||
const nextIndex = (currentIndex + 1) % this.viewModes.length;
|
||||
const newView = this.viewModes[nextIndex];
|
||||
let data = state.user;
|
||||
data.viewMode = this.viewModes[nextIndex];
|
||||
try {
|
||||
mutations.updateUser({ viewMode: newView });
|
||||
users.update(data, ["viewMode"]);
|
||||
mutations.setUser(data);
|
||||
} catch (e) {
|
||||
showError(e);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,60 @@
|
||||
<template>
|
||||
<div style="padding-bottom: 5em">
|
||||
<div v-if="selectedCount > 0" id="file-selection">
|
||||
<span>{{ selectedCount }} selected</span>
|
||||
<div>
|
||||
<action
|
||||
v-if="headerButtons.select"
|
||||
icon="info"
|
||||
:label="$t('buttons.info')"
|
||||
show="info"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.select"
|
||||
icon="check_circle"
|
||||
:label="$t('buttons.selectMultiple')"
|
||||
@action="toggleMultipleSelection"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.download"
|
||||
icon="file_download"
|
||||
:label="$t('buttons.download')"
|
||||
@action="download"
|
||||
:counter="selectedCount"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.share"
|
||||
icon="share"
|
||||
:label="$t('buttons.share')"
|
||||
show="share"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.rename"
|
||||
icon="mode_edit"
|
||||
:label="$t('buttons.rename')"
|
||||
show="rename"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.copy"
|
||||
icon="content_copy"
|
||||
:label="$t('buttons.copyFile')"
|
||||
show="copy"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.move"
|
||||
icon="forward"
|
||||
:label="$t('buttons.moveFile')"
|
||||
show="move"
|
||||
/>
|
||||
<action
|
||||
v-if="headerButtons.delete"
|
||||
icon="delete"
|
||||
:label="$t('buttons.delete')"
|
||||
show="delete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading">
|
||||
<h2 class="message delayed">
|
||||
<div class="spinner">
|
||||
@@ -39,56 +94,52 @@
|
||||
:class="listingViewMode + ' file-icons'"
|
||||
>
|
||||
<div>
|
||||
<div class="header" :class="{ 'dark-mode-item-header': isDarkMode }">
|
||||
<p
|
||||
:class="{ active: nameSorted }"
|
||||
class="name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('name')"
|
||||
:title="$t('files.sortByName')"
|
||||
:aria-label="$t('files.sortByName')"
|
||||
>
|
||||
<span>{{ $t("files.name") }}</span>
|
||||
<i class="material-icons">{{ nameIcon }}</i>
|
||||
</p>
|
||||
<div class="header">
|
||||
<p
|
||||
:class="{ active: nameSorted }"
|
||||
class="name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('name')"
|
||||
:title="$t('files.sortByName')"
|
||||
:aria-label="$t('files.sortByName')"
|
||||
>
|
||||
<span>{{ $t("files.name") }}</span>
|
||||
<i class="material-icons">{{ nameIcon }}</i>
|
||||
</p>
|
||||
|
||||
<p
|
||||
:class="{ active: sizeSorted }"
|
||||
class="size"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('size')"
|
||||
:title="$t('files.sortBySize')"
|
||||
:aria-label="$t('files.sortBySize')"
|
||||
>
|
||||
<span>{{ $t("files.size") }}</span>
|
||||
<i class="material-icons">{{ sizeIcon }}</i>
|
||||
</p>
|
||||
<p
|
||||
:class="{ active: modifiedSorted }"
|
||||
class="modified"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('modified')"
|
||||
:title="$t('files.sortByLastModified')"
|
||||
:aria-label="$t('files.sortByLastModified')"
|
||||
>
|
||||
<span>{{ $t("files.lastModified") }}</span>
|
||||
<i class="material-icons">{{ modifiedIcon }}</i>
|
||||
</p>
|
||||
<p
|
||||
:class="{ active: sizeSorted }"
|
||||
class="size"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('size')"
|
||||
:title="$t('files.sortBySize')"
|
||||
:aria-label="$t('files.sortBySize')"
|
||||
>
|
||||
<span>{{ $t("files.size") }}</span>
|
||||
<i class="material-icons">{{ sizeIcon }}</i>
|
||||
</p>
|
||||
<p
|
||||
:class="{ active: modifiedSorted }"
|
||||
class="modified"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('modified')"
|
||||
:title="$t('files.sortByLastModified')"
|
||||
:aria-label="$t('files.sortByLastModified')"
|
||||
>
|
||||
<span>{{ $t("files.lastModified") }}</span>
|
||||
<i class="material-icons">{{ modifiedIcon }}</i>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="numDirs > 0">
|
||||
<div v-if="numDirs > 0" >
|
||||
<div class="header-items">
|
||||
<h2>{{ $t("files.folders") }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="numDirs > 0"
|
||||
class="folder-items"
|
||||
:class="{ lastGroup: numFiles === 0 }"
|
||||
>
|
||||
<div v-if="numDirs > 0" class="folder-items" :class="{ lastGroup: numFiles === 0 }" >
|
||||
<item
|
||||
v-for="item in dirs"
|
||||
:key="base64(item.name)"
|
||||
@@ -158,6 +209,13 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.header-items {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import { files as api } from "@/api";
|
||||
import * as upload from "@/utils/upload";
|
||||
@@ -166,34 +224,23 @@ import throttle from "@/utils/throttle";
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import { showError } from "@/notify";
|
||||
|
||||
import Action from "@/components/header/Action.vue";
|
||||
import Item from "@/components/files/ListingItem.vue";
|
||||
export default {
|
||||
name: "listingView",
|
||||
components: {
|
||||
Action,
|
||||
Item,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
sortField: "name",
|
||||
columnWidth: 250 + state.user.gallerySize * 50,
|
||||
columnWidth: 280,
|
||||
dragCounter: 0,
|
||||
width: window.innerWidth,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
gallerySize() {
|
||||
this.columnWidth = 250 + state.user.gallerySize * 50; // Update columnWidth based on new gallery size\
|
||||
this.colunmsResize();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
// Create a computed property that references the Vuex state
|
||||
gallerySize() {
|
||||
return state.user.gallerySize;
|
||||
},
|
||||
isDarkMode() {
|
||||
return state.user?.darkMode;
|
||||
},
|
||||
getMultiple() {
|
||||
return state.multiple;
|
||||
},
|
||||
@@ -210,13 +257,30 @@ export default {
|
||||
return state.req.sorting.asc;
|
||||
},
|
||||
items() {
|
||||
return getters.reqItems();
|
||||
if (state.user == null) {
|
||||
return {};
|
||||
}
|
||||
const dirs = [];
|
||||
const files = [];
|
||||
|
||||
state.req.items.forEach((item) => {
|
||||
if (state.user.hideDotfiles && item.name.startsWith(".")) {
|
||||
return;
|
||||
}
|
||||
if (item.isDir) {
|
||||
dirs.push(item);
|
||||
} else {
|
||||
item.Path = state.req.Path;
|
||||
files.push(item);
|
||||
}
|
||||
});
|
||||
return { dirs, files };
|
||||
},
|
||||
numDirs() {
|
||||
return getters.reqNumDirs();
|
||||
return state.req.numDirs;
|
||||
},
|
||||
numFiles() {
|
||||
return getters.reqNumFiles();
|
||||
return state.req.numFiles;
|
||||
},
|
||||
dirs() {
|
||||
return this.items.dirs;
|
||||
@@ -255,10 +319,20 @@ export default {
|
||||
return icons[state.user.viewMode];
|
||||
},
|
||||
listingViewMode() {
|
||||
this.colunmsResize();
|
||||
return state.user.viewMode;
|
||||
return state.user?.viewMode;
|
||||
},
|
||||
headerButtons() {
|
||||
return {
|
||||
select: state.selected.length > 0,
|
||||
upload: state.user.perm?.create && state.selected.length > 0,
|
||||
download: state.user.perm.download && state.selected.length > 0,
|
||||
delete: state.selected.length > 0 && state.user.perm.delete,
|
||||
rename: state.selected.length === 1 && state.user.perm.rename,
|
||||
share: state.selected.length === 1 && state.user.perm.share,
|
||||
move: state.selected.length > 0 && state.user.perm.rename,
|
||||
copy: state.selected.length > 0 && state.user.perm?.create,
|
||||
};
|
||||
},
|
||||
|
||||
selectedCount() {
|
||||
return state.selected.length;
|
||||
},
|
||||
@@ -266,7 +340,7 @@ export default {
|
||||
return state.req;
|
||||
},
|
||||
loading() {
|
||||
return getters.isLoading();
|
||||
return state.loading;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@@ -393,12 +467,12 @@ export default {
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
mutations.setLoading("listing", true);
|
||||
|
||||
let action = (overwrite, rename) => {
|
||||
api
|
||||
.copy(items, overwrite, rename)
|
||||
.then(() => {
|
||||
mutations.setLoading("listing", false);
|
||||
mutations.setLoading(true);
|
||||
})
|
||||
.catch(showError);
|
||||
};
|
||||
@@ -409,7 +483,7 @@ export default {
|
||||
.move(items, overwrite, rename)
|
||||
.then(() => {
|
||||
this.clipboard = {};
|
||||
mutations.setLoading("listing", false);
|
||||
mutations.setLoading(true);
|
||||
})
|
||||
.catch(showError);
|
||||
};
|
||||
@@ -446,11 +520,6 @@ export default {
|
||||
let items = css(["#listingView .item", "#listingView .item"]);
|
||||
if (columns === 0) columns = 1;
|
||||
items.style.width = `calc(${100 / columns}% - 1em)`;
|
||||
if (state.user.viewMode == "gallery") {
|
||||
items.style.height = `${this.columnWidth / 20}em`;
|
||||
} else {
|
||||
items.style.height = `auto`;
|
||||
}
|
||||
},
|
||||
dragEnter() {
|
||||
this.dragCounter++;
|
||||
@@ -568,12 +637,38 @@ export default {
|
||||
openSearch() {
|
||||
this.currentPrompt = "search";
|
||||
},
|
||||
toggleMultipleSelection() {
|
||||
mutations.setMultiple(!state.multiple);
|
||||
mutations.closeHovers();
|
||||
},
|
||||
windowsResize: throttle(function () {
|
||||
this.colunmsResize();
|
||||
this.width = window.innerWidth;
|
||||
// Listing element is not displayed
|
||||
if (this.$refs.listingView == null) return;
|
||||
}, 100),
|
||||
download() {
|
||||
if (getters.isSingleFileSelected()) {
|
||||
api.download(null, getters.selectedDownloadUrl());
|
||||
return;
|
||||
}
|
||||
mutations.showHover({
|
||||
name: "download",
|
||||
confirm: (format) => {
|
||||
mutations.closeHovers();
|
||||
let files = [];
|
||||
if (state.selected.length > 0) {
|
||||
for (let i of state.selected) {
|
||||
files.push(state.req.items[i].url);
|
||||
}
|
||||
} else {
|
||||
files.push(state.route.path);
|
||||
}
|
||||
|
||||
api.download(format, ...files);
|
||||
},
|
||||
});
|
||||
},
|
||||
upload() {
|
||||
if (
|
||||
typeof window.DataTransferItem !== "undefined" &&
|
||||
@@ -587,15 +682,3 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dark-mode-item-header {
|
||||
border-color: var(--divider) !important;
|
||||
background: var(--surfacePrimary) !important;
|
||||
}
|
||||
.header-items {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -155,9 +155,6 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
if (!getters.isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
this.updatePreview();
|
||||
this.toggleNavigation();
|
||||
},
|
||||
@@ -188,12 +185,10 @@ export default {
|
||||
};
|
||||
},
|
||||
prev() {
|
||||
mutations.setLoading("preview-img", true);
|
||||
this.hoverNav = false;
|
||||
this.$router.replace({ path: this.previousLink });
|
||||
},
|
||||
next() {
|
||||
mutations.setLoading("preview-img", true);
|
||||
this.hoverNav = false;
|
||||
this.$router.replace({ path: this.nextLink });
|
||||
},
|
||||
@@ -233,28 +228,23 @@ export default {
|
||||
|
||||
this.previousLink = "";
|
||||
this.nextLink = "";
|
||||
const path = state.req.path;
|
||||
const directoryPath = path.substring(0, path.lastIndexOf("/"));
|
||||
|
||||
for (let i = 0; i < this.listing.length; i++) {
|
||||
if (this.listing[i].name !== this.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
let composedListing = this.listing[j];
|
||||
composedListing.path = directoryPath + "/" + composedListing.name;
|
||||
if (mediaTypes.includes(composedListing.type)) {
|
||||
this.previousLink = composedListing.url;
|
||||
this.previousRaw = this.prefetchUrl(composedListing);
|
||||
if (mediaTypes.includes(this.listing[j].type)) {
|
||||
this.previousLink = this.listing[j].url;
|
||||
this.previousRaw = this.prefetchUrl(this.listing[j]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (let j = i + 1; j < this.listing.length; j++) {
|
||||
let composedListing = this.listing[j];
|
||||
composedListing.path = directoryPath + "/" + composedListing.name;
|
||||
if (mediaTypes.includes(composedListing.type)) {
|
||||
this.nextLink = composedListing.url;
|
||||
this.nextRaw = this.prefetchUrl(composedListing);
|
||||
if (mediaTypes.includes(this.listing[j].type)) {
|
||||
this.nextLink = this.listing[j].url;
|
||||
this.nextRaw = this.prefetchUrl(this.listing[j]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -266,6 +256,7 @@ export default {
|
||||
if (item.type !== "image") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return this.fullSize
|
||||
? api.getDownloadURL(item, true)
|
||||
: api.getPreviewURL(item, "big");
|
||||
@@ -290,7 +281,7 @@ export default {
|
||||
this.showNav = false || this.hoverNav;
|
||||
this.navTimeout = null;
|
||||
}, 1500);
|
||||
}, 100),
|
||||
}, 500),
|
||||
close() {
|
||||
mutations.replaceRequest({}); // Reset request data
|
||||
let uri = url.removeLastDir(state.route.path) + "/";
|
||||
|
||||
@@ -1,101 +1,196 @@
|
||||
<template>
|
||||
<errors v-if="error" :errorCode="error.status" />
|
||||
<form class="card" @submit.prevent="save">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("settings.globalSettings") }}</h2>
|
||||
<div class="row" v-else-if="!loading">
|
||||
<div class="column">
|
||||
<form class="card" @submit.prevent="save">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("settings.globalSettings") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>
|
||||
<input type="checkbox" v-model="settings.signup" />
|
||||
{{ $t("settings.allowSignup") }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input type="checkbox" v-model="settings.createUserDir" />
|
||||
{{ $t("settings.createUserDir") }}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p class="small">{{ $t("settings.userHomeBasePath") }}</p>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="text"
|
||||
v-model="settings.userHomeBasePath"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3>{{ $t("settings.rules") }}</h3>
|
||||
<p class="small">{{ $t("settings.globalRules") }}</p>
|
||||
<rules :rules="settings.rules" @update:rules="updateRules" />
|
||||
|
||||
<div v-if="isExecEnabled">
|
||||
<h3>{{ $t("settings.executeOnShell") }}</h3>
|
||||
<p class="small">{{ $t("settings.executeOnShellDescription") }}</p>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="text"
|
||||
placeholder="bash -c, cmd /c, ..."
|
||||
v-model="settings.shell"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3>{{ $t("settings.branding") }}</h3>
|
||||
|
||||
<i18n path="settings.brandingHelp" tag="p" class="small">
|
||||
<a
|
||||
class="link"
|
||||
target="_blank"
|
||||
href="https://filebrowser.org/configuration/custom-branding"
|
||||
>{{ $t("settings.documentation") }}</a
|
||||
>
|
||||
</i18n>
|
||||
|
||||
<p>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="settings.frontend.disableExternal"
|
||||
id="branding-links"
|
||||
/>
|
||||
{{ $t("settings.disableExternalLinks") }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="settings.frontend.disableUsedPercentage"
|
||||
id="branding-links"
|
||||
/>
|
||||
{{ $t("settings.disableUsedDiskPercentage") }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="branding-name">{{ $t("settings.instanceName") }}</label>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="text"
|
||||
v-model="settings.frontend.name"
|
||||
id="branding-name"
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="branding-files">{{ $t("settings.brandingDirectoryPath") }}</label>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="text"
|
||||
v-model="settings.frontend.files"
|
||||
id="branding-files"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<input
|
||||
class="button button--flat"
|
||||
type="submit"
|
||||
:value="$t('buttons.update')"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>
|
||||
<input type="checkbox" v-model="selectedSettings.signup" />
|
||||
{{ $t("settings.allowSignup") }}
|
||||
</p>
|
||||
<div class="column">
|
||||
<form class="card" @submit.prevent="save">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("settings.userDefaults") }}</h2>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<input type="checkbox" v-model="selectedSettings.createUserDir" />
|
||||
{{ $t("settings.createUserDir") }}
|
||||
</p>
|
||||
<div class="card-content">
|
||||
<p class="small">{{ $t("settings.defaultUserDescription") }}</p>
|
||||
|
||||
<div>
|
||||
<p class="small">{{ $t("settings.userHomeBasePath") }}</p>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="text"
|
||||
v-model="selectedSettings.userHomeBasePath"
|
||||
/>
|
||||
</div>
|
||||
<user-form
|
||||
:isNew="false"
|
||||
:isDefault="true"
|
||||
:user="settings.defaults"
|
||||
@update:user="updateUser"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3>{{ $t("settings.rules") }}</h3>
|
||||
<p class="small">{{ $t("settings.globalRules") }}</p>
|
||||
<rules :rules="selectedSettings.rules" @update:rules="updateRules" />
|
||||
|
||||
<h3>{{ $t("settings.branding") }}</h3>
|
||||
|
||||
<i18n path="settings.brandingHelp" tag="p" class="small">
|
||||
<a
|
||||
class="link"
|
||||
target="_blank"
|
||||
href="https://filebrowser.org/configuration/custom-branding"
|
||||
>{{ $t("settings.documentation") }}</a
|
||||
>
|
||||
</i18n>
|
||||
|
||||
<p>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="selectedSettings.frontend.disableExternal"
|
||||
id="branding-links"
|
||||
/>
|
||||
{{ $t("settings.disableExternalLinks") }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="selectedSettings.frontend.disableUsedPercentage"
|
||||
id="branding-links"
|
||||
/>
|
||||
{{ $t("settings.disableUsedDiskPercentage") }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="branding-name">{{ $t("settings.instanceName") }}</label>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="text"
|
||||
v-model="selectedSettings.frontend.name"
|
||||
id="branding-name"
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="branding-files">{{ $t("settings.brandingDirectoryPath") }}</label>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="text"
|
||||
v-model="selectedSettings.frontend.files"
|
||||
id="branding-files"
|
||||
/>
|
||||
</p>
|
||||
<div class="card-action">
|
||||
<input
|
||||
class="button button--flat"
|
||||
type="submit"
|
||||
:value="$t('buttons.update')"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<input class="button button--flat" type="submit" :value="$t('buttons.update')" />
|
||||
<div class="column">
|
||||
<form v-if="isExecEnabled" class="card" @submit.prevent="save">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("settings.commandRunner") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<i18n path="settings.commandRunnerHelp" tag="p" class="small">
|
||||
<code>FILE</code>
|
||||
<code>SCOPE</code>
|
||||
<a
|
||||
class="link"
|
||||
target="_blank"
|
||||
href="https://filebrowser.org/configuration/command-runner"
|
||||
>{{ $t("settings.documentation") }}</a
|
||||
>
|
||||
</i18n>
|
||||
|
||||
<div
|
||||
v-for="(command, index) in settings.commands"
|
||||
:key="index"
|
||||
class="collapsible"
|
||||
>
|
||||
<input :id="command.name" type="checkbox" />
|
||||
<label :for="command.name">
|
||||
<p>{{ capitalize(command.name) }}</p>
|
||||
<i class="material-icons">arrow_drop_down</i>
|
||||
</label>
|
||||
<div class="collapse">
|
||||
<textarea
|
||||
class="input input--block input--textarea"
|
||||
v-model.trim="command.value"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<input
|
||||
class="button button--flat"
|
||||
type="submit"
|
||||
:value="$t('buttons.update')"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { showSuccess, showError } from "@/notify";
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import { showSuccess } from "@/notify";
|
||||
import { state, mutations } from "@/store";
|
||||
import { settings as api } from "@/api";
|
||||
import { enableExec } from "@/utils/constants";
|
||||
import UserForm from "@/components/settings/UserForm.vue";
|
||||
import Rules from "@/components/settings/Rules.vue";
|
||||
import Errors from "@/views/Errors.vue";
|
||||
|
||||
export default {
|
||||
name: "settings",
|
||||
components: {
|
||||
UserForm,
|
||||
Rules,
|
||||
Errors,
|
||||
},
|
||||
@@ -103,27 +198,45 @@ export default {
|
||||
return {
|
||||
error: null,
|
||||
originalSettings: null,
|
||||
selectedSettings: state.settings,
|
||||
settings: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
loading() {
|
||||
return getters.isLoading();
|
||||
},
|
||||
return state.loading;
|
||||
},
|
||||
user() {
|
||||
return state.user;
|
||||
},
|
||||
isExecEnabled: () => enableExec,
|
||||
},
|
||||
async created() {
|
||||
mutations.setLoading("settings", true);
|
||||
const original = await api.get();
|
||||
mutations.setSettings(original);
|
||||
mutations.setLoading("settings", false);
|
||||
try {
|
||||
mutations.setLoading(true);
|
||||
const original = await api.get();
|
||||
let settings = { ...original, commands: [] };
|
||||
|
||||
for (const key in original.commands) {
|
||||
settings.commands.push({
|
||||
name: key,
|
||||
value: original.commands[key].join("\n"),
|
||||
});
|
||||
}
|
||||
settings.shell = settings.shell.join(" ");
|
||||
this.originalSettings = original;
|
||||
this.settings = settings;
|
||||
} catch (e) {
|
||||
this.error = e;
|
||||
} finally {
|
||||
mutations.setLoading(false);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateRules(updatedRules) {
|
||||
this.selectedSettings = { ...this.selectedSettings, rules: updatedRules };
|
||||
this.settings.rules = updatedRules;
|
||||
},
|
||||
updateUser(updatedUser) {
|
||||
this.settings.defaults = updatedUser;
|
||||
},
|
||||
capitalize(name, where = "_") {
|
||||
if (where === "caps") where = /(?=[A-Z])/;
|
||||
@@ -137,9 +250,21 @@ export default {
|
||||
return name.slice(0, -1);
|
||||
},
|
||||
async save() {
|
||||
let settings = {
|
||||
...this.settings,
|
||||
shell: this.settings.shell
|
||||
.trim()
|
||||
.split(" ")
|
||||
.filter((s) => s !== ""),
|
||||
commands: {},
|
||||
};
|
||||
|
||||
for (const { name, value } of this.settings.commands) {
|
||||
settings.commands[name] = value.split("\n").filter((cmd) => cmd !== "");
|
||||
}
|
||||
|
||||
try {
|
||||
mutations.setSettings(this.selectedSettings);
|
||||
await api.update(state.settings);
|
||||
await api.update(settings);
|
||||
showSuccess(this.$t("settings.settingsUpdated"));
|
||||
} catch (e) {
|
||||
showError(e);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<div class="card" id="profile-main" :class="{ active: active }">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("settings.profileSettings") }}</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<form @submit="updateSettings">
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<form class="card" @submit="updateSettings">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("settings.profileSettings") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>
|
||||
<input type="checkbox" v-model="darkMode" />
|
||||
@@ -28,23 +29,6 @@
|
||||
:viewMode="viewMode"
|
||||
@update:viewMode="updateViewMode"
|
||||
></ViewMode>
|
||||
<br />
|
||||
<h3>Default View Size</h3>
|
||||
<p>
|
||||
Note: only applicable for normal and gallery views. Changes here will persist
|
||||
accross logins.
|
||||
</p>
|
||||
<div>
|
||||
<input
|
||||
v-model="gallerySize"
|
||||
type="range"
|
||||
id="gallary-size"
|
||||
name="gallary-size"
|
||||
:value="gallerySize"
|
||||
min="0"
|
||||
max="10"
|
||||
/>
|
||||
</div>
|
||||
<h3>{{ $t("settings.language") }}</h3>
|
||||
<Languages
|
||||
class="input input--block"
|
||||
@@ -61,8 +45,10 @@
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<hr />
|
||||
<form v-if="!user.lockPassword" @submit="updatePassword">
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<form class="card" v-if="!user.lockPassword" @submit="updatePassword">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("settings.changePassword") }}</h2>
|
||||
</div>
|
||||
@@ -99,7 +85,7 @@
|
||||
<script>
|
||||
import { showSuccess, showError } from "@/notify";
|
||||
import { state, mutations } from "@/store";
|
||||
import { users } from "@/api";
|
||||
import { users as api } from "@/api";
|
||||
import Languages from "@/components/settings/Languages.vue";
|
||||
import ViewMode from "@/components/settings/ViewMode.vue";
|
||||
import i18n, { rtlLanguages } from "@/i18n";
|
||||
@@ -120,16 +106,9 @@ export default {
|
||||
darkMode: false,
|
||||
viewMode: "list",
|
||||
locale: "",
|
||||
gallerySize: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
settings() {
|
||||
return state.settings;
|
||||
},
|
||||
active() {
|
||||
return state.activeSettingsView === "profile-main";
|
||||
},
|
||||
user() {
|
||||
return state.user;
|
||||
},
|
||||
@@ -148,18 +127,13 @@ export default {
|
||||
},
|
||||
},
|
||||
created() {
|
||||
mutations.setLoading(false);
|
||||
this.darkMode = state.user.darkMode;
|
||||
this.locale = state.user.locale;
|
||||
this.viewMode = state.user.viewMode;
|
||||
this.hideDotfiles = state.user.hideDotfiles;
|
||||
this.singleClick = state.user.singleClick;
|
||||
this.dateFormat = state.user.dateFormat;
|
||||
this.gallerySize = state.user.gallerySize;
|
||||
},
|
||||
watch: {
|
||||
gallerySize(newValue) {
|
||||
this.gallerySize = parseInt(newValue, 0); // Update the user object
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async updatePassword(event) {
|
||||
@@ -173,7 +147,8 @@ export default {
|
||||
let newUserSettings = state.user;
|
||||
newUserSettings.id = state.user.id;
|
||||
newUserSettings.password = this.password;
|
||||
await users.update(newUserSettings, ["password"]);
|
||||
await api.update(newUserSettings, ["password"]);
|
||||
muations.updateUser(newUserSettings);
|
||||
showSuccess(this.$t("settings.passwordUpdated"));
|
||||
} catch (e) {
|
||||
showError(e);
|
||||
@@ -190,18 +165,16 @@ export default {
|
||||
hideDotfiles: this.hideDotfiles,
|
||||
singleClick: this.singleClick,
|
||||
dateFormat: this.dateFormat,
|
||||
gallerySize: this.gallerySize,
|
||||
};
|
||||
const shouldReload =
|
||||
rtlLanguages.includes(data.locale) !== rtlLanguages.includes(i18n.locale);
|
||||
await users.update(data, [
|
||||
await api.update(data, [
|
||||
"locale",
|
||||
"darkMode",
|
||||
"viewMode",
|
||||
"hideDotfiles",
|
||||
"singleClick",
|
||||
"dateFormat",
|
||||
"gallerySize",
|
||||
]);
|
||||
mutations.updateUser(data);
|
||||
if (shouldReload) {
|
||||
|
||||
@@ -1,63 +1,67 @@
|
||||
<template>
|
||||
<errors v-if="error" :errorCode="error.status" />
|
||||
<div class="card" id="shares-main" :class="{ active: active }">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("settings.shareManagement") }}</h2>
|
||||
</div>
|
||||
<div class="row" v-else-if="!loading">
|
||||
<div class="column">
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("settings.shareManagement") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content full" v-if="links.length > 0">
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{ $t("settings.path") }}</th>
|
||||
<th>{{ $t("settings.shareDuration") }}</th>
|
||||
<th v-if="user.perm.admin">{{ $t("settings.username") }}</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<div class="card-content full" v-if="links.length > 0">
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{ $t("settings.path") }}</th>
|
||||
<th>{{ $t("settings.shareDuration") }}</th>
|
||||
<th v-if="user.perm.admin">{{ $t("settings.username") }}</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
<tr v-for="link in links" :key="link.hash">
|
||||
<td>
|
||||
<a :href="buildLink(link)" target="_blank">{{ link.path }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template>
|
||||
<template v-else>{{ $t("permanent") }}</template>
|
||||
</td>
|
||||
<td v-if="user.perm.admin">{{ link.username }}</td>
|
||||
<td class="small">
|
||||
<button
|
||||
class="action"
|
||||
@click="deleteLink($event, link)"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"
|
||||
>
|
||||
<i class="material-icons">delete</i>
|
||||
</button>
|
||||
</td>
|
||||
<td class="small">
|
||||
<button
|
||||
class="action copy-clipboard"
|
||||
:data-clipboard-text="buildLink(link)"
|
||||
:aria-label="$t('buttons.copyToClipboard')"
|
||||
:title="$t('buttons.copyToClipboard')"
|
||||
>
|
||||
<i class="material-icons">content_paste</i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<tr v-for="link in links" :key="link.hash">
|
||||
<td>
|
||||
<a :href="buildLink(link)" target="_blank">{{ link.path }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template>
|
||||
<template v-else>{{ $t("permanent") }}</template>
|
||||
</td>
|
||||
<td v-if="user.perm.admin">{{ link.username }}</td>
|
||||
<td class="small">
|
||||
<button
|
||||
class="action"
|
||||
@click="deleteLink($event, link)"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"
|
||||
>
|
||||
<i class="material-icons">delete</i>
|
||||
</button>
|
||||
</td>
|
||||
<td class="small">
|
||||
<button
|
||||
class="action copy-clipboard"
|
||||
:data-clipboard-text="buildLink(link)"
|
||||
:aria-label="$t('buttons.copyToClipboard')"
|
||||
:title="$t('buttons.copyToClipboard')"
|
||||
>
|
||||
<i class="material-icons">content_paste</i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<h2 class="message" v-else>
|
||||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>{{ $t("files.lonely") }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="message" v-else>
|
||||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>{{ $t("files.lonely") }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { showSuccess, showError } from "@/notify";
|
||||
import { share as api, users } from "@/api";
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import { state, mutations } from "@/store";
|
||||
import { fromNow } from "@/utils/moment";
|
||||
import Clipboard from "clipboard";
|
||||
import Errors from "@/views/Errors.vue";
|
||||
@@ -75,7 +79,7 @@ export default {
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
mutations.setLoading("shares", true);
|
||||
mutations.setLoading(true);
|
||||
|
||||
try {
|
||||
let links = await api.list();
|
||||
@@ -89,7 +93,7 @@ export default {
|
||||
} catch (e) {
|
||||
this.error = e;
|
||||
} finally {
|
||||
mutations.setLoading("shares", false);
|
||||
mutations.setLoading(false);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -102,17 +106,11 @@ export default {
|
||||
this.clip.destroy();
|
||||
},
|
||||
computed: {
|
||||
settings() {
|
||||
return state.settings;
|
||||
},
|
||||
active() {
|
||||
return state.activeSettingsView === "shares-main";
|
||||
},
|
||||
user() {
|
||||
return state.user;
|
||||
},
|
||||
loading() {
|
||||
return getters.isLoading();
|
||||
return state.loading;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -1,39 +1,43 @@
|
||||
<template>
|
||||
<errors v-if="error" :errorCode="error.status" />
|
||||
<form @submit="save" id="user-main" class="card">
|
||||
<div class="card-title">
|
||||
<h2 v-if="user.id === 0">{{ $t("settings.newUser") }}</h2>
|
||||
<h2 v-else>{{ $t("settings.user") }} {{ user.username }}</h2>
|
||||
</div>
|
||||
<div class="row" v-else-if="!loading">
|
||||
<div class="column">
|
||||
<form @submit="save" class="card">
|
||||
<div class="card-title">
|
||||
<h2 v-if="user.id === 0">{{ $t("settings.newUser") }}</h2>
|
||||
<h2 v-else>{{ $t("settings.user") }} {{ user.username }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<user-form
|
||||
:user="user"
|
||||
:createUserDir="createUserDir"
|
||||
:isDefault="false"
|
||||
:isNew="isNew"
|
||||
@update:user="(updatedUser) => (user = updatedUser)"
|
||||
@update:createUserDir="(updatedDir) => (createUserDir = updatedDir)"
|
||||
/>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<user-form
|
||||
:user="user"
|
||||
:createUserDir="createUserDir"
|
||||
:isDefault="false"
|
||||
:isNew="isNew"
|
||||
@update:user="(updatedUser) => (user = updatedUser)"
|
||||
@update:createUserDir="(updatedDir) => (createUserDir = updatedDir)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button
|
||||
v-if="!isNew"
|
||||
@click.prevent="deletePrompt"
|
||||
type="button"
|
||||
class="button button--flat button--red"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"
|
||||
>
|
||||
{{ $t("buttons.delete") }}
|
||||
</button>
|
||||
<input class="button button--flat" type="submit" :value="$t('buttons.save')" />
|
||||
<div class="card-action">
|
||||
<button
|
||||
v-if="!isNew"
|
||||
@click.prevent="deletePrompt"
|
||||
type="button"
|
||||
class="button button--flat button--red"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"
|
||||
>
|
||||
{{ $t("buttons.delete") }}
|
||||
</button>
|
||||
<input class="button button--flat" type="submit" :value="$t('buttons.save')" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mutations, state } from "@/store";
|
||||
import { mutations,state } from "@/store";
|
||||
import { users as api, settings } from "@/api";
|
||||
import UserForm from "@/components/settings/UserForm.vue";
|
||||
import Errors from "@/views/Errors.vue";
|
||||
@@ -49,7 +53,7 @@ export default {
|
||||
return {
|
||||
error: null,
|
||||
originalUser: null,
|
||||
user: { perm: { admin: false } },
|
||||
user: { perm: {admin: false} },
|
||||
showDelete: false,
|
||||
createUserDir: false,
|
||||
loading: false, // Replaces Vuex state `loading`
|
||||
@@ -61,9 +65,6 @@ export default {
|
||||
this.fetchData();
|
||||
},
|
||||
computed: {
|
||||
settings() {
|
||||
return state.settings;
|
||||
},
|
||||
isNew() {
|
||||
return state.route.path === "/settings/users/new";
|
||||
},
|
||||
@@ -73,7 +74,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async fetchData() {
|
||||
mutations.setLoading("users", true);
|
||||
mutations.setLoading(true);
|
||||
try {
|
||||
if (this.isNew) {
|
||||
let { defaults, createUserDir } = await settings.get();
|
||||
@@ -91,10 +92,10 @@ export default {
|
||||
this.user = { ...(await api.get(id)) };
|
||||
}
|
||||
} catch (e) {
|
||||
showError(e);
|
||||
showError(e)
|
||||
this.error = e;
|
||||
} finally {
|
||||
mutations.setLoading("users", false);
|
||||
mutations.setLoading(false);
|
||||
}
|
||||
},
|
||||
deletePrompt() {
|
||||
@@ -115,7 +116,6 @@ export default {
|
||||
} else {
|
||||
await api.update(user);
|
||||
if (user.id === state.user.id) {
|
||||
consoel.log("set user");
|
||||
mutations.setUser(user);
|
||||
}
|
||||
showSuccess(this.$t("settings.userUpdated"));
|
||||
|
||||
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user