v0.2.8 release (#193)
This commit is contained in:
parent
5ebaf2a45b
commit
9e9109984d
|
@ -1,4 +1,4 @@
|
||||||
name: main job
|
name: main release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
@ -16,12 +16,6 @@ jobs:
|
||||||
uses: docker/setup-qemu-action@v3.0.0
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.0.0
|
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
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
|
@ -32,10 +26,20 @@ jobs:
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: gtstef/filebrowser
|
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
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
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
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
push: true
|
push: true
|
||||||
|
|
|
@ -1,13 +1,28 @@
|
||||||
name: pr-merge
|
name: pr-request
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- "main"
|
- "main"
|
||||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||||
- "dev_*"
|
|
||||||
|
|
||||||
jobs:
|
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:
|
push_pr_to_registry:
|
||||||
name: Push PR
|
name: Push PR
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -18,12 +33,6 @@ jobs:
|
||||||
uses: docker/setup-qemu-action@v3.0.0
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.0.0
|
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
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
|
@ -35,10 +44,13 @@ jobs:
|
||||||
with:
|
with:
|
||||||
images: gtstef/filebrowser
|
images: gtstef/filebrowser
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
build-args: |
|
||||||
|
version=${{ steps.meta.outputs.version }}
|
||||||
|
commitSHA=${{ steps.meta.outputs.revision }}
|
|
@ -1,4 +1,4 @@
|
||||||
name: dev
|
name: dev tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
@ -21,10 +21,10 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
go-version: '1.22.5'
|
||||||
- uses: golangci/golangci-lint-action@v6
|
- uses: golangci/golangci-lint-action@v5
|
||||||
with:
|
with:
|
||||||
version: v1.59
|
version: v1.60
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
format-backend:
|
format-backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
name: release
|
name: version release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
@ -34,6 +34,7 @@ jobs:
|
||||||
draft: false
|
draft: false
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
name: ${{ steps.extract_branch.outputs.branch }}
|
name: ${{ steps.extract_branch.outputs.branch }}
|
||||||
|
|
||||||
push_release_to_registry:
|
push_release_to_registry:
|
||||||
name: Push release
|
name: Push release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -44,14 +45,7 @@ jobs:
|
||||||
uses: docker/setup-qemu-action@v3.0.0
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.0.0
|
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
|
- name: Login to Docker Hub
|
||||||
# Only push to Docker Hub when making a release
|
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
@ -69,9 +63,12 @@ jobs:
|
||||||
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
|
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
|
||||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
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
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
push: true
|
push: true
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
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 }}
|
|
@ -1,24 +0,0 @@
|
||||||
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
|
|
|
@ -7,10 +7,12 @@ rice-box.go
|
||||||
/filebrowser.exe
|
/filebrowser.exe
|
||||||
/frontend/dist
|
/frontend/dist
|
||||||
/frontend/pkg
|
/frontend/pkg
|
||||||
|
/frontend/test-results
|
||||||
/frontend/package-lock.json
|
/frontend/package-lock.json
|
||||||
/backend/vendor
|
/backend/vendor
|
||||||
/backend/*.cov
|
/backend/*.cov
|
||||||
/backend/test_config.yaml
|
/backend/test_config.yaml
|
||||||
|
/backend/srv
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
|
|
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -2,6 +2,16 @@
|
||||||
|
|
||||||
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).
|
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
|
## v0.2.7
|
||||||
|
|
||||||
- **Change**: New sidebar style and behavior
|
- **Change**: New sidebar style and behavior
|
||||||
|
|
20
Dockerfile
20
Dockerfile
|
@ -1,18 +1,22 @@
|
||||||
FROM node:slim as nbuild
|
FROM golang:1.22-alpine AS base
|
||||||
|
ARG VERSION
|
||||||
|
ARG REVISION
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./frontend/package*.json ./
|
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 ./
|
||||||
RUN npm i --maxsockets 1
|
RUN npm i --maxsockets 1
|
||||||
COPY ./frontend/ ./
|
COPY ./frontend/ ./
|
||||||
RUN npm run build-docker
|
RUN npm run build-docker
|
||||||
|
|
||||||
FROM golang:1.22-alpine as base
|
|
||||||
WORKDIR /app
|
|
||||||
COPY ./backend ./
|
|
||||||
RUN go build -ldflags="-w -s" -o filebrowser .
|
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
ENV FILEBROWSER_NO_EMBEDED="true"
|
ENV FILEBROWSER_NO_EMBEDED="true"
|
||||||
ARG app="/app/filebrowser"
|
|
||||||
RUN apk --no-cache add ca-certificates mailcap
|
RUN apk --no-cache add ca-certificates mailcap
|
||||||
COPY --from=base /app/filebrowser* ./
|
COPY --from=base /app/filebrowser* ./
|
||||||
COPY --from=nbuild /app/dist/ ./frontend/dist/
|
COPY --from=nbuild /app/dist/ ./frontend/dist/
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
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
|
92
README.md
92
README.md
|
@ -4,7 +4,7 @@
|
||||||
<p align="center">
|
<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>
|
</p>
|
||||||
<h3 align="center">Filebrowser - A modern web-based file manager</h3>
|
<h3 align="center">Filebrowser Quantum - A modern web-based file manager</h3>
|
||||||
<p align="center">
|
<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/user-attachments/assets/8ba93582-aba2-4996-8ac3-25f763a2e596" title="Main Screenshot">
|
||||||
</p>
|
</p>
|
||||||
|
@ -15,38 +15,41 @@
|
||||||
> Starting with v0.2.4 *ALL* share links need to be re-created (due to
|
> Starting with v0.2.4 *ALL* share links need to be re-created (due to
|
||||||
> security fix).
|
> security fix).
|
||||||
|
|
||||||
This fork makes the following significant changes to filebrowser for
|
Filebrowser Quantum is a fork of the filebrowser opensource project with the
|
||||||
origin:
|
following changes:
|
||||||
|
|
||||||
1. [x] Better search
|
1. [x] Enhanced lightning fast indexed search
|
||||||
- Lightning fast
|
- Real-time results as you type
|
||||||
- real-time results as you type
|
|
||||||
- Works with more type filters
|
- Works with more type filters
|
||||||
- interactive results page.
|
- Enhanced interactive results page.
|
||||||
2. [x] Revamped and simplified GUI navbar and sidebar menu.
|
2. [x] Revamped and simplified GUI navbar and sidebar menu.
|
||||||
- Additional compact view mode as well as refreshed view mode
|
- Additional compact view mode as well as refreshed view mode
|
||||||
styles.
|
styles.
|
||||||
3. [x] Revamped configuration via `filebrowser.yml` config file.
|
3. [x] Revamped and simplified configuration via `filebrowser.yml` config file.
|
||||||
- More configurations possible at a per-user level
|
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
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
Filebrowser provides a file managing interface within a specified directory
|
Filebrowser Quantum provides a file managing interface within a specified directory
|
||||||
and can be used to upload, delete, preview, rename, and edit your files.
|
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
|
It allows the creation of multiple users and each user can have its
|
||||||
directory.
|
directory.
|
||||||
|
|
||||||
This repository is a fork, a collection of changes that make this program
|
This repository is a fork of the original [filebrowser](https://github.com/filebrowser/filebrowser)
|
||||||
work better in terms of aesthetics and performance. Improved search,
|
with a collection of changes that make this program work better in terms of
|
||||||
simplified ui (without removing features) and more secure and up-to-date
|
aesthetics and performance. Improved search, simplified ui
|
||||||
|
(without removing features) and more secure and up-to-date
|
||||||
build are just a few examples.
|
build are just a few examples.
|
||||||
|
|
||||||
This Implementation of filebrowser differs significantly to the original.
|
Filebrowser Quantum differs significantly to the original.
|
||||||
There are hundreds of thousands of lines changed and they are generally
|
There are hundreds of thousands of lines changed and they are generally
|
||||||
no longer compatible with each other. This has been intentional -- the
|
no longer compatible with each other. This has been intentional -- the
|
||||||
focus of this fork is on a few key principles:
|
focus of this fork is on a few key principles:
|
||||||
- Simplicity and improved user experience
|
- Simplicity and improved user experience
|
||||||
- Efficiency of operations and performance
|
- Improving performance and faster feedback when making changes.
|
||||||
- Minimize external dependencies and standard library usage.
|
- Minimize external dependencies and standard library usage.
|
||||||
- Of course -- adding much-needed features.
|
- Of course -- adding much-needed features.
|
||||||
|
|
||||||
|
@ -81,7 +84,7 @@ Using docker:
|
||||||
docker run -it -v /path/to/folder:/srv -p 80:80 gtstef/filebrowser
|
docker run -it -v /path/to/folder:/srv -p 80:80 gtstef/filebrowser
|
||||||
```
|
```
|
||||||
|
|
||||||
1. docker-compose:
|
1. docker compose:
|
||||||
|
|
||||||
- with local storage
|
- with local storage
|
||||||
|
|
||||||
|
@ -140,8 +143,8 @@ configuration options and other help.
|
||||||
|
|
||||||
## Migration from filebrowser/filebrowser
|
## Migration from filebrowser/filebrowser
|
||||||
|
|
||||||
If you currently use filebrowser from the filebrowser/filebrowser
|
If you currently use the original opensource filebrowser
|
||||||
repo but want to try using this. I recommend you start fresh without
|
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
|
reusing the database, but there are a few things you'll need to do if you
|
||||||
must migrate:
|
must migrate:
|
||||||
|
|
||||||
|
@ -157,10 +160,61 @@ must migrate:
|
||||||
filebrowser.yml and have a valid filebrowser config.
|
filebrowser.yml and have a valid filebrowser config.
|
||||||
|
|
||||||
|
|
||||||
The filebrowser application should run with the same user and rules that
|
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
|
you have from the original. But keep in mind the differences that are
|
||||||
mentioned at the top of this readme.
|
mentioned at the top of this readme.
|
||||||
|
|
||||||
|
|
||||||
|
## Comparison Chart
|
||||||
|
|
||||||
|
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 | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
see [Roadmap Page](./roadmap.md)
|
see [Roadmap Page](./roadmap.md)
|
||||||
|
|
|
@ -0,0 +1,232 @@
|
||||||
|
# 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
|
|
@ -5,8 +5,8 @@ version: 2
|
||||||
builds:
|
builds:
|
||||||
# Build configuration for darwin and linux
|
# Build configuration for darwin and linux
|
||||||
- id: default
|
- id: default
|
||||||
ldflags:
|
ldflags: &ldflags
|
||||||
- -s -w
|
- -s -w -X github.com/gtsteffaniak/filebrowser/version.Version={{ .Version }} -X github.com/gtsteffaniak/filebrowser/version.CommitSHA={{ .ShortCommit }}
|
||||||
main: main.go
|
main: main.go
|
||||||
binary: filebrowser
|
binary: filebrowser
|
||||||
goos:
|
goos:
|
||||||
|
@ -25,8 +25,7 @@ builds:
|
||||||
|
|
||||||
# Build configuration for windows without arm
|
# Build configuration for windows without arm
|
||||||
- id: windows
|
- id: windows
|
||||||
ldflags:
|
ldflags: *ldflags
|
||||||
- -s -w
|
|
||||||
main: main.go
|
main: main.go
|
||||||
binary: filebrowser
|
binary: filebrowser
|
||||||
goos:
|
goos:
|
||||||
|
|
|
@ -17,7 +17,6 @@ import (
|
||||||
|
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/auth"
|
"github.com/gtsteffaniak/filebrowser/auth"
|
||||||
|
@ -64,13 +63,20 @@ var rootCmd = &cobra.Command{
|
||||||
log.Fatal("Image resize workers count could not be < 1")
|
log.Fatal("Image resize workers count could not be < 1")
|
||||||
}
|
}
|
||||||
imgSvc := img.New(serverConfig.NumImageProcessors)
|
imgSvc := img.New(serverConfig.NumImageProcessors)
|
||||||
var fileCache diskcache.Interface = diskcache.NewNoOp()
|
|
||||||
cacheDir := "/tmp"
|
cacheDir := "/tmp"
|
||||||
|
var fileCache diskcache.Interface
|
||||||
|
|
||||||
|
// Use file cache if cacheDir is specified
|
||||||
if cacheDir != "" {
|
if cacheDir != "" {
|
||||||
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet,gomnd
|
var err error
|
||||||
log.Fatalf("can't make directory %s: %s", cacheDir, err)
|
fileCache, err = diskcache.NewFileCache(cacheDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create file cache: %v", err)
|
||||||
}
|
}
|
||||||
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
|
} else {
|
||||||
|
// No-op cache if no cacheDir is specified
|
||||||
|
fileCache = diskcache.NewNoOp()
|
||||||
}
|
}
|
||||||
// initialize indexing and schedule indexing ever n minutes (default 5)
|
// initialize indexing and schedule indexing ever n minutes (default 5)
|
||||||
go files.InitializeIndex(serverConfig.IndexingInterval, serverConfig.Indexing)
|
go files.InitializeIndex(serverConfig.IndexingInterval, serverConfig.Indexing)
|
||||||
|
|
|
@ -34,11 +34,6 @@ list or set it to 0.`,
|
||||||
err = unmarshal(args[0], &list)
|
err = unmarshal(args[0], &list)
|
||||||
checkErr("unmarshal", err)
|
checkErr("unmarshal", err)
|
||||||
|
|
||||||
for _, user := range list {
|
|
||||||
err = user.Clean("")
|
|
||||||
checkErr("Clean", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mustGetBool(cmd.Flags(), "replace") {
|
if mustGetBool(cmd.Flags(), "replace") {
|
||||||
oldUsers, err := d.store.Users.Gets("")
|
oldUsers, err := d.store.Users.Gets("")
|
||||||
checkErr("d.store.Users.Gets", err)
|
checkErr("d.store.Users.Gets", err)
|
||||||
|
|
|
@ -10,13 +10,17 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileCache struct {
|
// Cache interface for caching operations
|
||||||
fs afero.Fs
|
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
|
||||||
// granular locks
|
// granular locks
|
||||||
scopedLocks struct {
|
scopedLocks struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
|
@ -25,10 +29,12 @@ type FileCache struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(fs afero.Fs, root string) *FileCache {
|
// NewFileCache creates a new FileCache
|
||||||
return &FileCache{
|
func NewFileCache(dir string) (*FileCache, error) {
|
||||||
fs: afero.NewBasePathFs(fs, root),
|
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||||
|
return nil, fmt.Errorf("can't make directory %s: %v", dir, err)
|
||||||
}
|
}
|
||||||
|
return &FileCache{dir: dir}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
|
func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
|
||||||
|
@ -37,11 +43,11 @@ func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|
||||||
fileName := f.getFileName(key)
|
fileName := f.getFileName(key)
|
||||||
if err := f.fs.MkdirAll(filepath.Dir(fileName), 0700); err != nil { //nolint:gomnd
|
if err := os.MkdirAll(filepath.Dir(fileName), 0700); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil { //nolint:gomnd
|
if err := os.WriteFile(fileName, value, 0600); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,15 +74,15 @@ func (f *FileCache) Delete(ctx context.Context, key string) error {
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|
||||||
fileName := f.getFileName(key)
|
fileName := f.getFileName(key)
|
||||||
if err := f.fs.Remove(fileName); err != nil && !errors.Is(err, os.ErrNotExist) {
|
if err := os.Remove(fileName); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileCache) open(key string) (afero.File, bool, error) {
|
func (f *FileCache) open(key string) (*os.File, bool, error) {
|
||||||
fileName := f.getFileName(key)
|
fileName := f.getFileName(key)
|
||||||
file, err := f.fs.Open(fileName)
|
file, err := os.Open(fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
|
@ -106,5 +112,5 @@ func (f *FileCache) getFileName(key string) string {
|
||||||
hasher := sha1.New() //nolint:gosec
|
hasher := sha1.New() //nolint:gosec
|
||||||
_, _ = hasher.Write([]byte(key))
|
_, _ = hasher.Write([]byte(key))
|
||||||
hash := hex.EncodeToString(hasher.Sum(nil))
|
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||||
return fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash)
|
return filepath.Join(f.dir, fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash))
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,10 @@ package diskcache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,35 +15,39 @@ func TestFileCache(t *testing.T) {
|
||||||
key = "key"
|
key = "key"
|
||||||
value = "some text"
|
value = "some text"
|
||||||
newValue = "new text"
|
newValue = "new text"
|
||||||
cacheRoot = "/cache"
|
cacheRoot = "cache"
|
||||||
cachedFilePath = "a/62/a62f2225bf70bfaccbc7f1ef2a397836717377de"
|
cachedFilePath = "a/62/a62f2225bf70bfaccbc7f1ef2a397836717377de"
|
||||||
)
|
)
|
||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
// Create temporary directory for the cache
|
||||||
cache := New(fs, "/cache")
|
cacheDir, err := os.MkdirTemp("", cacheRoot)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(cacheDir) // Clean up
|
||||||
|
|
||||||
|
cache, err := NewFileCache(cacheDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
// store new key
|
// store new key
|
||||||
err := cache.Store(ctx, key, []byte(value))
|
err = cache.Store(ctx, key, []byte(value))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, value)
|
checkValue(t, ctx, cache, filepath.Join(cacheDir, cachedFilePath), key, value)
|
||||||
|
|
||||||
// update existing key
|
// update existing key
|
||||||
err = cache.Store(ctx, key, []byte(newValue))
|
err = cache.Store(ctx, key, []byte(newValue))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, newValue)
|
checkValue(t, ctx, cache, filepath.Join(cacheDir, cachedFilePath), key, newValue)
|
||||||
|
|
||||||
// delete key
|
// delete key
|
||||||
err = cache.Delete(ctx, key)
|
err = cache.Delete(ctx, key)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
exists, err := afero.Exists(fs, filepath.Join(cacheRoot, cachedFilePath))
|
exists := fileExists(filepath.Join(cacheDir, cachedFilePath))
|
||||||
require.NoError(t, err)
|
|
||||||
require.False(t, exists)
|
require.False(t, exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:golint
|
func checkValue(t *testing.T, ctx context.Context, cache *FileCache, fileFullPath string, key, wantValue string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
// check actual file content
|
// check actual file content
|
||||||
b, err := afero.ReadFile(fs, fileFullPath)
|
b, err := os.ReadFile(fileFullPath)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, wantValue, string(b))
|
require.Equal(t, wantValue, string(b))
|
||||||
|
|
||||||
|
@ -53,3 +57,11 @@ func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath str
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
require.Equal(t, wantValue, string(b))
|
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()
|
||||||
|
}
|
||||||
|
|
|
@ -6,21 +6,21 @@ import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
filepath "path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/errors"
|
"github.com/gtsteffaniak/filebrowser/errors"
|
||||||
"github.com/gtsteffaniak/filebrowser/rules"
|
"github.com/gtsteffaniak/filebrowser/rules"
|
||||||
|
"github.com/gtsteffaniak/filebrowser/settings"
|
||||||
"github.com/gtsteffaniak/filebrowser/users"
|
"github.com/gtsteffaniak/filebrowser/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,7 +33,6 @@ var (
|
||||||
// FileInfo describes a file.
|
// FileInfo describes a file.
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
*Listing
|
*Listing
|
||||||
Fs afero.Fs `json:"-"`
|
|
||||||
Path string `json:"path,omitempty"`
|
Path string `json:"path,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
|
@ -52,8 +51,7 @@ type FileInfo struct {
|
||||||
|
|
||||||
// FileOptions are the options when getting a file info.
|
// FileOptions are the options when getting a file info.
|
||||||
type FileOptions struct {
|
type FileOptions struct {
|
||||||
Fs afero.Fs
|
Path string // realpath
|
||||||
Path string
|
|
||||||
Modify bool
|
Modify bool
|
||||||
Expand bool
|
Expand bool
|
||||||
ReadHeader bool
|
ReadHeader bool
|
||||||
|
@ -91,7 +89,7 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
|
||||||
}
|
}
|
||||||
if opts.Expand {
|
if opts.Expand {
|
||||||
if file.IsDir {
|
if file.IsDir {
|
||||||
if err := file.readListing(opts.Path, opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
|
if err = file.readListing(opts.Path, opts.Checker, opts.ReadHeader); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return file, nil
|
return file, nil
|
||||||
|
@ -166,22 +164,19 @@ func RefreshFileInfo(opts FileOptions) bool {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
//_, exists := index.GetFileMetadata(adjustedPath)
|
|
||||||
|
|
||||||
return index.UpdateFileMetadata(adjustedPath, *file)
|
return index.UpdateFileMetadata(adjustedPath, *file)
|
||||||
} else {
|
} else {
|
||||||
//_, exists := index.GetFileMetadata(adjustedPath)
|
|
||||||
return index.UpdateFileMetadata(adjustedPath, *file)
|
return index.UpdateFileMetadata(adjustedPath, *file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stat(path string, opts FileOptions) (*FileInfo, error) {
|
func stat(path string, opts FileOptions) (*FileInfo, error) {
|
||||||
var file *FileInfo
|
info, err := os.Lstat(path)
|
||||||
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
|
if err != nil {
|
||||||
info, _, err := lstaterFs.LstatIfPossible(path)
|
return nil, err
|
||||||
if err == nil {
|
}
|
||||||
file = &FileInfo{
|
|
||||||
Fs: opts.Fs,
|
file := &FileInfo{
|
||||||
Path: opts.Path,
|
Path: opts.Path,
|
||||||
Name: info.Name(),
|
Name: info.Name(),
|
||||||
ModTime: info.ModTime(),
|
ModTime: info.ModTime(),
|
||||||
|
@ -190,36 +185,16 @@ func stat(path string, opts FileOptions) (*FileInfo, error) {
|
||||||
Extension: filepath.Ext(info.Name()),
|
Extension: filepath.Ext(info.Name()),
|
||||||
Token: opts.Token,
|
Token: opts.Token,
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
file.IsDir = true
|
file.IsDir = true
|
||||||
}
|
}
|
||||||
if info.Mode()&os.ModeSymlink != 0 {
|
if info.Mode()&os.ModeSymlink != 0 {
|
||||||
file.IsSymlink = true
|
file.IsSymlink = true
|
||||||
}
|
targetInfo, err := os.Stat(path)
|
||||||
}
|
if err == nil {
|
||||||
}
|
file.Size = targetInfo.Size()
|
||||||
if file == nil || file.IsSymlink {
|
file.IsDir = targetInfo.IsDir()
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,7 +212,7 @@ func (i *FileInfo) Checksum(algo string) error {
|
||||||
i.Checksums = map[string]string{}
|
i.Checksums = map[string]string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
reader, err := i.Fs.Open(i.Path)
|
reader, err := os.Open(i.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -266,23 +241,127 @@ func (i *FileInfo) Checksum(algo string) error {
|
||||||
|
|
||||||
// RealPath gets the real path for the file, resolving symlinks if supported.
|
// RealPath gets the real path for the file, resolving symlinks if supported.
|
||||||
func (i *FileInfo) RealPath() string {
|
func (i *FileInfo) RealPath() string {
|
||||||
if realPathFs, ok := i.Fs.(interface {
|
realPath, err := filepath.EvalSymlinks(i.Path)
|
||||||
RealPath(name string) (fPath string, err error)
|
|
||||||
}); ok {
|
|
||||||
realPath, err := realPathFs.RealPath(i.Path)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return realPath
|
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 i.Path
|
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.
|
// addContent reads and sets content based on the file type.
|
||||||
func (i *FileInfo) addContent(path string) error {
|
func (i *FileInfo) addContent(path string) error {
|
||||||
if !i.IsDir {
|
if !i.IsDir {
|
||||||
afs := &afero.Afero{Fs: i.Fs}
|
fmt.Println("getting content for ", path)
|
||||||
content, err := afs.ReadFile(path)
|
content, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -301,6 +380,9 @@ func (i *FileInfo) addContent(path string) error {
|
||||||
|
|
||||||
// detectType detects the file type.
|
// detectType detects the file type.
|
||||||
func (i *FileInfo) detectType(path string, modify, saveContent, readHeader bool) error {
|
func (i *FileInfo) detectType(path string, modify, saveContent, readHeader bool) error {
|
||||||
|
if i.IsDir {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if IsNamedPipe(i.Mode) {
|
if IsNamedPipe(i.Mode) {
|
||||||
i.Type = "blob"
|
i.Type = "blob"
|
||||||
if saveContent {
|
if saveContent {
|
||||||
|
@ -345,7 +427,6 @@ func (i *FileInfo) detectType(path string, modify, saveContent, readHeader bool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if i.Type == "" {
|
if i.Type == "" {
|
||||||
i.Type = "blob"
|
i.Type = "blob"
|
||||||
if saveContent {
|
if saveContent {
|
||||||
|
@ -358,15 +439,15 @@ func (i *FileInfo) detectType(path string, modify, saveContent, readHeader bool)
|
||||||
|
|
||||||
// readFirstBytes reads the first bytes of the file.
|
// readFirstBytes reads the first bytes of the file.
|
||||||
func (i *FileInfo) readFirstBytes() []byte {
|
func (i *FileInfo) readFirstBytes() []byte {
|
||||||
reader, err := i.Fs.Open(i.Path)
|
file, err := os.Open(i.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
i.Type = "blob"
|
i.Type = "blob"
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
defer reader.Close()
|
defer file.Close()
|
||||||
|
|
||||||
buffer := make([]byte, 512) //nolint:gomnd
|
buffer := make([]byte, 512) //nolint:gomnd
|
||||||
n, err := reader.Read(buffer)
|
n, err := file.Read(buffer)
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
i.Type = "blob"
|
i.Type = "blob"
|
||||||
return nil
|
return nil
|
||||||
|
@ -387,7 +468,8 @@ func (i *FileInfo) detectSubtitles(parentDir string) {
|
||||||
// Directory must have been deleted, remove it from the index
|
// Directory must have been deleted, remove it from the index
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Read the directory contents
|
defer dir.Close() // Ensure directory handle is closed
|
||||||
|
|
||||||
files, err := dir.Readdir(-1)
|
files, err := dir.Readdir(-1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -412,8 +494,13 @@ func (i *FileInfo) detectSubtitles(parentDir string) {
|
||||||
|
|
||||||
// readListing reads the contents of a directory and fills the listing.
|
// readListing reads the contents of a directory and fills the listing.
|
||||||
func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bool) error {
|
func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bool) error {
|
||||||
afs := &afero.Afero{Fs: i.Fs}
|
dir, err := os.Open(i.Path)
|
||||||
dir, err := afs.ReadDir(i.Path)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dir.Close()
|
||||||
|
|
||||||
|
files, err := dir.Readdir(-1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -425,7 +512,7 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
|
||||||
NumFiles: 0,
|
NumFiles: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, f := range dir {
|
for _, f := range files {
|
||||||
name := f.Name()
|
name := f.Name()
|
||||||
fPath := filepath.Join(i.Path, name)
|
fPath := filepath.Join(i.Path, name)
|
||||||
|
|
||||||
|
@ -436,7 +523,7 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
|
||||||
isSymlink, isInvalidLink := false, false
|
isSymlink, isInvalidLink := false, false
|
||||||
if IsSymlink(f.Mode()) {
|
if IsSymlink(f.Mode()) {
|
||||||
isSymlink = true
|
isSymlink = true
|
||||||
info, err := i.Fs.Stat(fPath)
|
info, err := os.Stat(fPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
f = info
|
f = info
|
||||||
} else {
|
} else {
|
||||||
|
@ -478,6 +565,7 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
|
||||||
i.Listing = listing
|
i.Listing = listing
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsNamedPipe(mode os.FileMode) bool {
|
func IsNamedPipe(mode os.FileMode) bool {
|
||||||
return mode&os.ModeNamedPipe != 0
|
return mode&os.ModeNamedPipe != 0
|
||||||
}
|
}
|
||||||
|
@ -498,3 +586,14 @@ func getMutex(path string) *sync.Mutex {
|
||||||
|
|
||||||
return pathMutexes[path]
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -2,23 +2,23 @@ package fileutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Copy copies a file or folder from one place to another.
|
// Copy copies a file or folder from one place to another.
|
||||||
func Copy(fs afero.Fs, src, dst string) error {
|
func Copy(src, dst string) error {
|
||||||
if src = path.Clean("/" + src); src == "" {
|
src = filepath.Clean(src)
|
||||||
|
if src == "" {
|
||||||
return os.ErrNotExist
|
return os.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
if dst = path.Clean("/" + dst); dst == "" {
|
dst = filepath.Clean(dst)
|
||||||
|
if dst == "" {
|
||||||
return os.ErrNotExist
|
return os.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
if src == "/" || dst == "/" {
|
if src == "/" || dst == "/" {
|
||||||
// Prohibit copying from or to the virtual root directory.
|
// Prohibit copying from or to the root directory.
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,14 +26,14 @@ func Copy(fs afero.Fs, src, dst string) error {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := fs.Stat(src)
|
info, err := os.Stat(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
return CopyDir(fs, src, dst)
|
return CopyDir(src, dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
return CopyFile(fs, src, dst)
|
return CopyFile(src, dst)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,27 +2,32 @@ package fileutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"os"
|
||||||
"github.com/spf13/afero"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CopyDir copies a directory from source to dest and all
|
// CopyDir copies a directory from source to dest and all
|
||||||
// of its sub-directories. It doesn't stop if it finds an error
|
// of its sub-directories. It doesn't stop if it finds an error
|
||||||
// during the copy. Returns an error if any.
|
// during the copy. Returns an error if any.
|
||||||
func CopyDir(fs afero.Fs, source, dest string) error {
|
func CopyDir(source, dest string) error {
|
||||||
// Get properties of source.
|
// Get properties of source.
|
||||||
srcinfo, err := fs.Stat(source)
|
srcinfo, err := os.Stat(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the destination directory.
|
// Create the destination directory.
|
||||||
err = fs.MkdirAll(dest, srcinfo.Mode())
|
err = os.MkdirAll(dest, srcinfo.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
dir, _ := fs.Open(source)
|
dir, err := os.Open(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dir.Close()
|
||||||
|
|
||||||
obs, err := dir.Readdir(-1)
|
obs, err := dir.Readdir(-1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -31,18 +36,18 @@ func CopyDir(fs afero.Fs, source, dest string) error {
|
||||||
var errs []error
|
var errs []error
|
||||||
|
|
||||||
for _, obj := range obs {
|
for _, obj := range obs {
|
||||||
fsource := source + "/" + obj.Name()
|
fsource := filepath.Join(source, obj.Name())
|
||||||
fdest := dest + "/" + obj.Name()
|
fdest := filepath.Join(dest, obj.Name())
|
||||||
|
|
||||||
if obj.IsDir() {
|
if obj.IsDir() {
|
||||||
// Create sub-directories, recursively.
|
// Create sub-directories, recursively.
|
||||||
err = CopyDir(fs, fsource, fdest)
|
err = CopyDir(fsource, fdest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Perform the file copy.
|
// Perform the file copy.
|
||||||
err = CopyFile(fs, fsource, fdest)
|
err = CopyFile(fsource, fdest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,24 +5,22 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MoveFile moves file from src to dst.
|
// MoveFile moves a file from src to dst.
|
||||||
// By default the rename filesystem system call is used. If src and dst point to different volumes
|
// By default, the rename system call is used. If src and dst point to different volumes,
|
||||||
// the file copy is used as a fallback
|
// the file copy is used as a fallback.
|
||||||
func MoveFile(fs afero.Fs, src, dst string) error {
|
func MoveFile(src, dst string) error {
|
||||||
if fs.Rename(src, dst) == nil {
|
if os.Rename(src, dst) == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// fallback
|
// fallback
|
||||||
err := CopyFile(fs, src, dst)
|
err := CopyFile(src, dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = fs.Remove(dst)
|
_ = os.Remove(dst)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := fs.Remove(src); err != nil {
|
if err := os.Remove(src); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -30,23 +28,22 @@ func MoveFile(fs afero.Fs, src, dst string) error {
|
||||||
|
|
||||||
// CopyFile copies a file from source to dest and returns
|
// CopyFile copies a file from source to dest and returns
|
||||||
// an error if any.
|
// an error if any.
|
||||||
func CopyFile(fs afero.Fs, source, dest string) error {
|
func CopyFile(source, dest string) error {
|
||||||
// Open the source file.
|
// Open the source file.
|
||||||
src, err := fs.Open(source)
|
src, err := os.Open(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer src.Close()
|
defer src.Close()
|
||||||
|
|
||||||
// Makes the directory needed to create the dst
|
// Makes the directory needed to create the dst file.
|
||||||
// file.
|
err = os.MkdirAll(filepath.Dir(dest), 0775) //nolint:gomnd
|
||||||
err = fs.MkdirAll(filepath.Dir(dest), 0666) //nolint:gomnd
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the destination file.
|
// Create the destination file.
|
||||||
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) //nolint:gomnd
|
dst, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) //nolint:gomnd
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -58,12 +55,12 @@ func CopyFile(fs afero.Fs, source, dest string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the mode
|
// Copy the mode.
|
||||||
info, err := fs.Stat(source)
|
info, err := os.Stat(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = fs.Chmod(dest, info.Mode())
|
err = os.Chmod(dest, info.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -71,7 +68,7 @@ func CopyFile(fs afero.Fs, source, dest string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommonPrefix returns common directory path of provided files
|
// CommonPrefix returns the common directory path of provided files.
|
||||||
func CommonPrefix(sep byte, paths ...string) string {
|
func CommonPrefix(sep byte, paths ...string) string {
|
||||||
// Handle special cases.
|
// Handle special cases.
|
||||||
switch len(paths) {
|
switch len(paths) {
|
||||||
|
@ -81,30 +78,19 @@ func CommonPrefix(sep byte, paths ...string) string {
|
||||||
return path.Clean(paths[0])
|
return path.Clean(paths[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note, we treat string as []byte, not []rune as is often
|
// Treat string as []byte, not []rune as is often done in Go.
|
||||||
// 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]))
|
c := []byte(path.Clean(paths[0]))
|
||||||
|
|
||||||
// We add a trailing sep to handle the case where the
|
// Add a trailing sep to handle the case where the common prefix directory
|
||||||
// common prefix directory is included in the path list
|
// 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)
|
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:] {
|
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)
|
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) {
|
if len(v) < len(c) {
|
||||||
c = c[:len(v)]
|
c = c[:len(v)]
|
||||||
}
|
}
|
||||||
|
@ -116,7 +102,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-- {
|
for i := len(c) - 1; i >= 0; i-- {
|
||||||
if c[i] == sep {
|
if c[i] == sep {
|
||||||
c = c[:i]
|
c = c[:i]
|
||||||
|
|
|
@ -19,38 +19,38 @@ require (
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
|
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
|
||||||
golang.org/x/crypto v0.25.0
|
golang.org/x/crypto v0.26.0
|
||||||
golang.org/x/image v0.18.0
|
golang.org/x/image v0.19.0
|
||||||
golang.org/x/text v0.16.0
|
golang.org/x/text v0.17.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.0.1 // indirect
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||||
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
|
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
|
||||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
|
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
|
||||||
github.com/fatih/color v1.10.0 // indirect
|
github.com/fatih/color v1.17.0 // indirect
|
||||||
github.com/go-errors/errors v1.4.2 // indirect
|
github.com/go-errors/errors v1.5.1 // indirect
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
|
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
|
||||||
github.com/golang/snappy v0.0.2 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.11.4 // indirect
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
github.com/klauspost/pgzip v1.2.5 // indirect
|
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/nwaples/rardecode v1.1.0 // indirect
|
github.com/nwaples/rardecode v1.1.3 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.2 // indirect
|
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/ulikunitz/xz v0.5.9 // indirect
|
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.etcd.io/bbolt v1.3.4 // indirect
|
go.etcd.io/bbolt v1.3.10 // indirect
|
||||||
golang.org/x/net v0.21.0 // indirect
|
golang.org/x/net v0.28.0 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/sys v0.24.0 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,8 @@ github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwv
|
||||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
|
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 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc=
|
||||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
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 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
|
||||||
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
|
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.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
@ -33,6 +35,8 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje
|
||||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
|
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 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
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=
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||||
|
@ -40,8 +44,12 @@ github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWE
|
||||||
github.com/go-errors/errors v1.1.1/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 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
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 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
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=
|
||||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||||
|
@ -56,6 +64,8 @@ github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgR
|
||||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
|
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-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=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
|
@ -63,6 +73,8 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
|
||||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
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 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
|
||||||
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
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=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
@ -75,9 +87,13 @@ github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c
|
||||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
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 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU=
|
||||||
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
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/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 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
|
||||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
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=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
@ -89,18 +105,29 @@ github.com/marusama/semaphore/v2 v2.5.0 h1:o/1QJD9DBYOWRnDhPwDVAXQn6mQYD0gZaS1Tp
|
||||||
github.com/marusama/semaphore/v2 v2.5.0/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ=
|
github.com/marusama/semaphore/v2 v2.5.0/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ=
|
||||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
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 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
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 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
|
||||||
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
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 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
|
||||||
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
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 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM=
|
||||||
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 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-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=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||||
|
@ -118,6 +145,8 @@ github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoi
|
||||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
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 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
|
||||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
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=
|
||||||
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
|
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
|
||||||
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||||
|
@ -126,12 +155,18 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
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 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
|
||||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
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.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 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||||
|
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||||
|
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.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 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||||
|
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
|
||||||
|
golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
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-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=
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
@ -141,6 +176,8 @@ golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/
|
||||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
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.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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
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-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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
@ -151,19 +188,28 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.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 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||||
|
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/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.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.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.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 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
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/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-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 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-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||||
|
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
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 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
|
Binary file not shown.
|
@ -7,6 +7,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
@ -46,7 +47,6 @@ func previewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, re
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := files.FileInfoFaster(files.FileOptions{
|
file, err := files.FileInfoFaster(files.FileOptions{
|
||||||
Fs: d.user.Fs,
|
|
||||||
Path: "/" + vars["path"],
|
Path: "/" + vars["path"],
|
||||||
Modify: d.user.Perm.Modify,
|
Modify: d.user.Perm.Modify,
|
||||||
Expand: true,
|
Expand: true,
|
||||||
|
@ -110,7 +110,7 @@ func handleImagePreview(
|
||||||
|
|
||||||
func createPreview(imgSvc ImgService, fileCache FileCache,
|
func createPreview(imgSvc ImgService, fileCache FileCache,
|
||||||
file *files.FileInfo, previewSize PreviewSize) ([]byte, error) {
|
file *files.FileInfo, previewSize PreviewSize) ([]byte, error) {
|
||||||
fd, err := file.Fs.Open(file.Path)
|
fd, err := os.Open(file.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,12 @@ package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/files"
|
"github.com/gtsteffaniak/filebrowser/files"
|
||||||
|
@ -20,31 +19,25 @@ import (
|
||||||
var withHashFile = func(fn handleFunc) handleFunc {
|
var withHashFile = func(fn handleFunc) handleFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
id, path := ifPathWithName(r)
|
id, path := ifPathWithName(r)
|
||||||
|
fmt.Println(id, path)
|
||||||
link, err := d.store.Share.GetByHash(id)
|
link, err := d.store.Share.GetByHash(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
}
|
}
|
||||||
if link.Hash != "" {
|
if link.Hash != "" {
|
||||||
status, err := authenticateShareRequest(r, link)
|
var status int
|
||||||
if status != 0 || err != nil {
|
status, err = authenticateShareRequest(r, link) // Assign to the existing `err` variable
|
||||||
|
if err != nil || status != 0 {
|
||||||
return status, err
|
return status, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.user = &users.PublicUser
|
d.user = &users.PublicUser
|
||||||
if path == "/" {
|
realPath, err := files.GetRealPath(d.user.Scope, link.Path, path)
|
||||||
path = link.Path
|
if err != nil {
|
||||||
} else if strings.HasPrefix("/"+path, link.Path) {
|
return http.StatusNotFound, err
|
||||||
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{
|
file, err := files.FileInfoFaster(files.FileOptions{
|
||||||
Fs: fsPath,
|
Path: realPath,
|
||||||
Path: lastComponent,
|
|
||||||
Modify: d.user.Perm.Modify,
|
Modify: d.user.Perm.Modify,
|
||||||
Expand: true,
|
Expand: true,
|
||||||
ReadHeader: d.server.TypeDetectionByHeader,
|
ReadHeader: d.server.TypeDetectionByHeader,
|
||||||
|
@ -63,14 +56,15 @@ func ifPathWithName(r *http.Request) (id, filePath string) {
|
||||||
pathElements := strings.Split(r.URL.Path, "/")
|
pathElements := strings.Split(r.URL.Path, "/")
|
||||||
id = pathElements[0]
|
id = pathElements[0]
|
||||||
allButFirst := path.Join(pathElements[1:]...)
|
allButFirst := path.Join(pathElements[1:]...)
|
||||||
if len(pathElements) == 1 {
|
|
||||||
allButFirst = "/"
|
|
||||||
}
|
|
||||||
return id, allButFirst
|
return id, allButFirst
|
||||||
}
|
}
|
||||||
|
|
||||||
var publicShareHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var publicShareHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
file := d.raw.(*files.FileInfo)
|
file, ok := d.raw.(*files.FileInfo)
|
||||||
|
if !ok {
|
||||||
|
return http.StatusInternalServerError, fmt.Errorf("failed to assert type *files.FileInfo")
|
||||||
|
}
|
||||||
|
|
||||||
file.Path = strings.TrimPrefix(file.Path, settings.Config.Server.Root)
|
file.Path = strings.TrimPrefix(file.Path, settings.Config.Server.Root)
|
||||||
if file.IsDir {
|
if file.IsDir {
|
||||||
return renderJSON(w, r, file)
|
return renderJSON(w, r, file)
|
||||||
|
@ -86,7 +80,11 @@ 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) {
|
var publicDlHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
file := d.raw.(*files.FileInfo)
|
file, ok := d.raw.(*files.FileInfo)
|
||||||
|
if !ok {
|
||||||
|
return http.StatusInternalServerError, fmt.Errorf("failed to assert type *files.FileInfo")
|
||||||
|
}
|
||||||
|
|
||||||
if !file.IsDir {
|
if !file.IsDir {
|
||||||
return rawFileHandler(w, r, file)
|
return rawFileHandler(w, r, file)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/asdine/storm/v3"
|
"github.com/asdine/storm/v3"
|
||||||
"github.com/spf13/afero"
|
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/settings"
|
"github.com/gtsteffaniak/filebrowser/settings"
|
||||||
"github.com/gtsteffaniak/filebrowser/share"
|
"github.com/gtsteffaniak/filebrowser/share"
|
||||||
|
@ -92,7 +91,6 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
|
||||||
|
|
||||||
storage.Users = &customFSUser{
|
storage.Users = &customFSUser{
|
||||||
Store: storage.Users,
|
Store: storage.Users,
|
||||||
fs: &afero.MemMapFs{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
|
@ -122,7 +120,6 @@ func newHTTPRequest(t *testing.T, requestModifiers ...func(*http.Request)) *http
|
||||||
|
|
||||||
type customFSUser struct {
|
type customFSUser struct {
|
||||||
users.Store
|
users.Store
|
||||||
fs afero.Fs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cu *customFSUser) Get(baseScope string, id interface{}) (*users.User, error) {
|
func (cu *customFSUser) Get(baseScope string, id interface{}) (*users.User, error) {
|
||||||
|
@ -130,7 +127,5 @@ func (cu *customFSUser) Get(baseScope string, id interface{}) (*users.User, erro
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
user.Fs = cu.fs
|
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,11 @@ package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
gopath "path"
|
gopath "path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -80,10 +82,13 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
|
||||||
if !d.user.Perm.Download {
|
if !d.user.Perm.Download {
|
||||||
return http.StatusAccepted, nil
|
return http.StatusAccepted, nil
|
||||||
}
|
}
|
||||||
|
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
fmt.Println("realpath", realPath, err)
|
||||||
file, err := files.FileInfoFaster(files.FileOptions{
|
file, err := files.FileInfoFaster(files.FileOptions{
|
||||||
Fs: d.user.Fs,
|
Path: realPath,
|
||||||
Path: r.URL.Path,
|
|
||||||
Modify: d.user.Perm.Modify,
|
Modify: d.user.Perm.Modify,
|
||||||
Expand: false,
|
Expand: false,
|
||||||
ReadHeader: d.server.TypeDetectionByHeader,
|
ReadHeader: d.server.TypeDetectionByHeader,
|
||||||
|
@ -108,7 +113,7 @@ func addFile(ar archiver.Writer, d *data, path, commonPath string) error {
|
||||||
if !d.Check(path) {
|
if !d.Check(path) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
info, err := d.user.Fs.Stat(path)
|
info, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -117,7 +122,7 @@ func addFile(ar archiver.Writer, d *data, path, commonPath string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := d.user.Fs.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -198,7 +203,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) {
|
func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo) (int, error) {
|
||||||
fd, err := file.Fs.Open(file.Path)
|
fd, err := os.Open(file.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package http
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
@ -12,7 +11,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v3/disk"
|
"github.com/shirou/gopsutil/v3/disk"
|
||||||
"github.com/spf13/afero"
|
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/errors"
|
"github.com/gtsteffaniak/filebrowser/errors"
|
||||||
"github.com/gtsteffaniak/filebrowser/files"
|
"github.com/gtsteffaniak/filebrowser/files"
|
||||||
|
@ -20,9 +18,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
|
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{
|
file, err := files.FileInfoFaster(files.FileOptions{
|
||||||
Fs: d.user.Fs,
|
Path: realPath,
|
||||||
Path: r.URL.Path,
|
|
||||||
Modify: d.user.Perm.Modify,
|
Modify: d.user.Perm.Modify,
|
||||||
Expand: true,
|
Expand: true,
|
||||||
ReadHeader: d.server.TypeDetectionByHeader,
|
ReadHeader: d.server.TypeDetectionByHeader,
|
||||||
|
@ -53,15 +55,18 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
|
||||||
if r.URL.Path == "/" || !d.user.Perm.Delete {
|
if r.URL.Path == "/" || !d.user.Perm.Delete {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||||
file, err := files.FileInfoFaster(files.FileOptions{
|
if err != nil {
|
||||||
Fs: d.user.Fs,
|
return http.StatusNotFound, err
|
||||||
Path: r.URL.Path,
|
}
|
||||||
|
fileOpts := files.FileOptions{
|
||||||
|
Path: realPath,
|
||||||
Modify: d.user.Perm.Modify,
|
Modify: d.user.Perm.Modify,
|
||||||
Expand: false,
|
Expand: false,
|
||||||
ReadHeader: d.server.TypeDetectionByHeader,
|
ReadHeader: d.server.TypeDetectionByHeader,
|
||||||
Checker: d,
|
Checker: d,
|
||||||
})
|
}
|
||||||
|
file, err := files.FileInfoFaster(fileOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
}
|
}
|
||||||
|
@ -72,14 +77,10 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.RunHook(func() error {
|
err = files.DeleteFiles(realPath, fileOpts)
|
||||||
return d.user.Fs.RemoveAll(r.URL.Path)
|
|
||||||
}, "delete", r.URL.Path, "", d.user)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
}
|
}
|
||||||
|
|
||||||
return http.StatusOK, nil
|
return http.StatusOK, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -89,21 +90,27 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
|
||||||
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
|
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||||
// Directories creation on POST.
|
if err != nil {
|
||||||
if strings.HasSuffix(r.URL.Path, "/") {
|
return http.StatusNotFound, err
|
||||||
err := d.user.Fs.MkdirAll(r.URL.Path, 0775) //nolint:gomnd
|
|
||||||
return errToStatus(err), err
|
|
||||||
}
|
}
|
||||||
|
fileOpts := files.FileOptions{
|
||||||
file, err := files.FileInfoFaster(files.FileOptions{
|
Path: realPath,
|
||||||
Fs: d.user.Fs,
|
|
||||||
Path: r.URL.Path,
|
|
||||||
Modify: d.user.Perm.Modify,
|
Modify: d.user.Perm.Modify,
|
||||||
Expand: false,
|
Expand: false,
|
||||||
ReadHeader: d.server.TypeDetectionByHeader,
|
ReadHeader: d.server.TypeDetectionByHeader,
|
||||||
Checker: d,
|
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 err == nil {
|
||||||
if r.URL.Query().Get("override") != "true" {
|
if r.URL.Query().Get("override") != "true" {
|
||||||
return http.StatusConflict, nil
|
return http.StatusConflict, nil
|
||||||
|
@ -119,22 +126,7 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
|
||||||
return errToStatus(err), err
|
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
|
return errToStatus(err), err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -149,27 +141,23 @@ var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
|
||||||
return http.StatusMethodNotAllowed, nil
|
return http.StatusMethodNotAllowed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
exists, err := afero.Exists(d.user.Fs, r.URL.Path)
|
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusNotFound, err
|
||||||
}
|
}
|
||||||
if !exists {
|
fileOpts := files.FileOptions{
|
||||||
return http.StatusNotFound, nil
|
Path: realPath,
|
||||||
|
Modify: d.user.Perm.Modify,
|
||||||
|
Expand: false,
|
||||||
|
ReadHeader: d.server.TypeDetectionByHeader,
|
||||||
|
Checker: d,
|
||||||
}
|
}
|
||||||
|
fmt.Println("realPath", realPath)
|
||||||
err = d.RunHook(func() error {
|
err = files.WriteFile(fileOpts, r.Body)
|
||||||
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
|
return errToStatus(err), err
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO fix and verify this function still works in tests
|
||||||
func resourcePatchHandler(fileCache FileCache) handleFunc {
|
func resourcePatchHandler(fileCache FileCache) handleFunc {
|
||||||
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
src := r.URL.Path
|
src := r.URL.Path
|
||||||
|
@ -185,29 +173,22 @@ func resourcePatchHandler(fileCache FileCache) handleFunc {
|
||||||
if dst == "/" || src == "/" {
|
if dst == "/" || src == "/" {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = checkParent(src, dst)
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusBadRequest, err
|
|
||||||
}
|
|
||||||
|
|
||||||
override := r.URL.Query().Get("override") == "true"
|
override := r.URL.Query().Get("override") == "true"
|
||||||
rename := r.URL.Query().Get("rename") == "true"
|
rename := r.URL.Query().Get("rename") == "true"
|
||||||
if !override && !rename {
|
if !override && !rename {
|
||||||
if _, err = d.user.Fs.Stat(dst); err == nil {
|
if _, err = os.Stat(dst); err == nil {
|
||||||
return http.StatusConflict, nil
|
return http.StatusConflict, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if rename {
|
if rename {
|
||||||
dst = addVersionSuffix(dst, d.user.Fs)
|
dst = addVersionSuffix(dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Permission for overwriting the file
|
// Permission for overwriting the file
|
||||||
if override && !d.user.Perm.Modify {
|
if override && !d.user.Perm.Modify {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.RunHook(func() error {
|
err = d.RunHook(func() error {
|
||||||
|
fmt.Println("hook", src, dst)
|
||||||
return patchAction(r.Context(), action, src, dst, d, fileCache)
|
return patchAction(r.Context(), action, src, dst, d, fileCache)
|
||||||
}, action, src, dst, d.user)
|
}, action, src, dst, d.user)
|
||||||
|
|
||||||
|
@ -215,69 +196,22 @@ func resourcePatchHandler(fileCache FileCache) handleFunc {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkParent(src, dst string) error {
|
func addVersionSuffix(source string) string {
|
||||||
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
|
counter := 1
|
||||||
dir, name := path.Split(source)
|
dir, name := path.Split(source)
|
||||||
ext := filepath.Ext(name)
|
ext := filepath.Ext(name)
|
||||||
base := strings.TrimSuffix(name, ext)
|
base := strings.TrimSuffix(name, ext)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if _, err := fs.Stat(source); err != nil {
|
if _, err := os.Stat(source); err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext)
|
renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext)
|
||||||
source = path.Join(dir, renamed)
|
source = path.Join(dir, renamed)
|
||||||
counter++
|
counter++
|
||||||
}
|
}
|
||||||
|
|
||||||
return source
|
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 {
|
func delThumbs(ctx context.Context, fileCache FileCache, file *files.FileInfo) error {
|
||||||
for _, previewSizeName := range PreviewSizeNames() {
|
for _, previewSizeName := range PreviewSizeNames() {
|
||||||
size, _ := ParsePreviewSize(previewSizeName)
|
size, _ := ParsePreviewSize(previewSizeName)
|
||||||
|
@ -297,17 +231,23 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
|
||||||
return errors.ErrPermissionDenied
|
return errors.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileutils.Copy(d.user.Fs, src, dst)
|
return fileutils.Copy(src, dst)
|
||||||
case "rename":
|
case "rename":
|
||||||
if !d.user.Perm.Rename {
|
if !d.user.Perm.Rename {
|
||||||
return errors.ErrPermissionDenied
|
return errors.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
src = path.Clean("/" + src)
|
src = path.Clean("/" + src)
|
||||||
dst = path.Clean("/" + dst)
|
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{
|
file, err := files.FileInfoFaster(files.FileOptions{
|
||||||
Fs: d.user.Fs,
|
Path: realSrc,
|
||||||
Path: src,
|
|
||||||
Modify: d.user.Perm.Modify,
|
Modify: d.user.Perm.Modify,
|
||||||
Expand: false,
|
Expand: false,
|
||||||
ReadHeader: false,
|
ReadHeader: false,
|
||||||
|
@ -323,7 +263,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileutils.MoveFile(d.user.Fs, src, dst)
|
return fileutils.MoveFile(realSrc, realDest)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
|
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
|
||||||
}
|
}
|
||||||
|
@ -335,9 +275,12 @@ type DiskUsageResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
|
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusNotFound, err
|
||||||
|
}
|
||||||
file, err := files.FileInfoFaster(files.FileOptions{
|
file, err := files.FileInfoFaster(files.FileOptions{
|
||||||
Fs: d.user.Fs,
|
Path: realPath,
|
||||||
Path: r.URL.Path,
|
|
||||||
Modify: d.user.Perm.Modify,
|
Modify: d.user.Perm.Modify,
|
||||||
Expand: false,
|
Expand: false,
|
||||||
ReadHeader: false,
|
ReadHeader: false,
|
||||||
|
@ -353,7 +296,6 @@ var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (
|
||||||
Used: 0,
|
Used: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
usage, err := disk.UsageWithContext(r.Context(), fPath)
|
usage, err := disk.UsageWithContext(r.Context(), fPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
|
|
|
@ -15,20 +15,17 @@ type settingsData struct {
|
||||||
Defaults settings.UserDefaults `json:"defaults"`
|
Defaults settings.UserDefaults `json:"defaults"`
|
||||||
Rules []rules.Rule `json:"rules"`
|
Rules []rules.Rule `json:"rules"`
|
||||||
Frontend settings.Frontend `json:"frontend"`
|
Frontend settings.Frontend `json:"frontend"`
|
||||||
Shell []string `json:"shell"`
|
|
||||||
Commands map[string][]string `json:"commands"`
|
Commands map[string][]string `json:"commands"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
data := &settingsData{
|
data := &settingsData{
|
||||||
Signup: settings.Config.Auth.Signup,
|
Signup: d.settings.Auth.Signup,
|
||||||
CreateUserDir: settings.Config.Server.CreateUserDir,
|
CreateUserDir: d.settings.Server.CreateUserDir,
|
||||||
UserHomeBasePath: settings.Config.Server.UserHomeBasePath,
|
UserHomeBasePath: d.settings.Server.UserHomeBasePath,
|
||||||
Defaults: d.settings.UserDefaults,
|
Defaults: d.settings.UserDefaults,
|
||||||
Rules: d.settings.Rules,
|
Rules: d.settings.Rules,
|
||||||
Frontend: d.settings.Frontend,
|
Frontend: d.settings.Frontend,
|
||||||
Shell: d.settings.Shell,
|
|
||||||
Commands: d.settings.Commands,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderJSON(w, r, data)
|
return renderJSON(w, r, data)
|
||||||
|
@ -46,8 +43,7 @@ var settingsPutHandler = withAdmin(func(w http.ResponseWriter, r *http.Request,
|
||||||
d.settings.UserDefaults = req.Defaults
|
d.settings.UserDefaults = req.Defaults
|
||||||
d.settings.Rules = req.Rules
|
d.settings.Rules = req.Rules
|
||||||
d.settings.Frontend = req.Frontend
|
d.settings.Frontend = req.Frontend
|
||||||
d.settings.Shell = req.Shell
|
d.settings.Auth.Signup = req.Signup
|
||||||
d.settings.Commands = req.Commands
|
|
||||||
err = d.store.Settings.Save(d.settings)
|
err = d.store.Settings.Save(d.settings)
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
})
|
})
|
||||||
|
|
|
@ -34,6 +34,7 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
|
||||||
"Color": d.settings.Frontend.Color,
|
"Color": d.settings.Frontend.Color,
|
||||||
"BaseURL": d.server.BaseURL,
|
"BaseURL": d.server.BaseURL,
|
||||||
"Version": version.Version,
|
"Version": version.Version,
|
||||||
|
"CommitSHA": version.CommitSHA,
|
||||||
"StaticURL": path.Join(d.server.BaseURL, "/static"),
|
"StaticURL": path.Join(d.server.BaseURL, "/static"),
|
||||||
"Signup": settings.Config.Auth.Signup,
|
"Signup": settings.Config.Auth.Signup,
|
||||||
"NoAuth": d.settings.Auth.Method == "noauth",
|
"NoAuth": d.settings.Auth.Method == "noauth",
|
||||||
|
@ -64,7 +65,12 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
auther := raw.(*auth.JSONAuth)
|
|
||||||
|
auther, ok := raw.(*auth.JSONAuth)
|
||||||
|
if !ok {
|
||||||
|
return http.StatusInternalServerError, fmt.Errorf("failed to assert type *auth.JSONAuth")
|
||||||
|
}
|
||||||
|
|
||||||
if auther.ReCaptcha != nil {
|
if auther.ReCaptcha != nil {
|
||||||
data["ReCaptcha"] = auther.ReCaptcha.Key != "" && auther.ReCaptcha.Secret != ""
|
data["ReCaptcha"] = auther.ReCaptcha.Key != "" && auther.ReCaptcha.Secret != ""
|
||||||
data["ReCaptchaHost"] = auther.ReCaptcha.Host
|
data["ReCaptchaHost"] = auther.ReCaptcha.Host
|
||||||
|
|
|
@ -2,6 +2,7 @@ package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
@ -13,6 +14,7 @@ import (
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/errors"
|
"github.com/gtsteffaniak/filebrowser/errors"
|
||||||
|
"github.com/gtsteffaniak/filebrowser/files"
|
||||||
"github.com/gtsteffaniak/filebrowser/users"
|
"github.com/gtsteffaniak/filebrowser/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -136,7 +138,11 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
|
||||||
}
|
}
|
||||||
req.Data.Scope = userHome
|
req.Data.Scope = userHome
|
||||||
log.Printf("user: %s, home dir: [%s].", req.Data.Username, userHome)
|
log.Printf("user: %s, home dir: [%s].", req.Data.Username, userHome)
|
||||||
|
_, err = files.GetRealPath(d.server.Root, req.Data.Scope)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("user path is not valid", req.Data.Scope)
|
||||||
|
return http.StatusBadRequest, nil
|
||||||
|
}
|
||||||
err = d.store.Users.Save(req.Data)
|
err = d.store.Users.Save(req.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
|
@ -155,6 +161,10 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
|
||||||
if req.Data.ID != d.raw.(uint) {
|
if req.Data.ID != d.raw.(uint) {
|
||||||
return http.StatusBadRequest, nil
|
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 || req.Which[0] == "all" {
|
||||||
req.Which = []string{}
|
req.Which = []string{}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gtsteffaniak/filebrowser/files"
|
||||||
"github.com/gtsteffaniak/filebrowser/settings"
|
"github.com/gtsteffaniak/filebrowser/settings"
|
||||||
"github.com/gtsteffaniak/filebrowser/users"
|
"github.com/gtsteffaniak/filebrowser/users"
|
||||||
)
|
)
|
||||||
|
@ -19,8 +20,8 @@ type Runner struct {
|
||||||
|
|
||||||
// RunHook runs the hooks for the before and after event.
|
// 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 {
|
func (r *Runner) RunHook(fn func() error, evt, path, dst string, user *users.User) error {
|
||||||
path = user.FullPath(path)
|
path, _ = files.GetRealPath(user.Scope, path)
|
||||||
dst = user.FullPath(dst)
|
dst, _ = files.GetRealPath(user.Scope, dst)
|
||||||
|
|
||||||
if r.Enabled {
|
if r.Enabled {
|
||||||
if val, ok := r.Commands["before_"+evt]; ok {
|
if val, ok := r.Commands["before_"+evt]; ok {
|
||||||
|
|
|
@ -6,15 +6,13 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
invalidFilenameChars = regexp.MustCompile(`[^0-9A-Za-z@_\-.]`)
|
invalidFilenameChars = regexp.MustCompile(`[^0-9A-Za-z@_\-.]`)
|
||||||
|
|
||||||
dashes = regexp.MustCompile(`[\-]+`)
|
dashes = regexp.MustCompile(`[\-]+`)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,8 +30,8 @@ func (s *Settings) MakeUserDir(username, userScope, serverRoot string) (string,
|
||||||
|
|
||||||
userScope = path.Join("/", userScope)
|
userScope = path.Join("/", userScope)
|
||||||
|
|
||||||
fs := afero.NewBasePathFs(afero.NewOsFs(), serverRoot)
|
fullPath := filepath.Join(serverRoot, userScope)
|
||||||
if err := fs.MkdirAll(userScope, os.ModePerm); err != nil {
|
if err := os.MkdirAll(fullPath, os.ModePerm); err != nil {
|
||||||
return "", fmt.Errorf("failed to create user home dir: [%s]: %w", userScope, err)
|
return "", fmt.Errorf("failed to create user home dir: [%s]: %w", userScope, err)
|
||||||
}
|
}
|
||||||
return userScope, nil
|
return userScope, nil
|
||||||
|
|
|
@ -65,10 +65,6 @@ func (s *Storage) Save(set *Settings) error {
|
||||||
set.Rules = []rules.Rule{}
|
set.Rules = []rules.Rule{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if set.Shell == nil {
|
|
||||||
set.Shell = []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if set.Commands == nil {
|
if set.Commands == nil {
|
||||||
set.Commands = map[string][]string{}
|
set.Commands = map[string][]string{}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,7 @@ type UserDefaults struct {
|
||||||
Scope string `json:"scope"`
|
Scope string `json:"scope"`
|
||||||
Locale string `json:"locale"`
|
Locale string `json:"locale"`
|
||||||
ViewMode string `json:"viewMode"`
|
ViewMode string `json:"viewMode"`
|
||||||
|
GallerySize int `json:"gallerySize"`
|
||||||
SingleClick bool `json:"singleClick"`
|
SingleClick bool `json:"singleClick"`
|
||||||
Rules []rules.Rule `json:"rules"`
|
Rules []rules.Rule `json:"rules"`
|
||||||
Sorting struct {
|
Sorting struct {
|
||||||
|
|
|
@ -54,9 +54,6 @@ func (s *Storage) Get(baseScope string, id interface{}) (user *User, err error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := user.Clean(baseScope); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,23 +63,12 @@ func (s *Storage) Gets(baseScope string) ([]*User, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, user := range users {
|
|
||||||
if err := user.Clean(baseScope); err != nil { //nolint:govet
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return users, err
|
return users, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update updates a user in the database.
|
// Update updates a user in the database.
|
||||||
func (s *Storage) Update(user *User, fields ...string) error {
|
func (s *Storage) Update(user *User, fields ...string) error {
|
||||||
err := user.Clean("")
|
err := s.back.Update(user, fields...)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.back.Update(user, fields...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -138,10 +124,6 @@ func (s *Storage) DeleteRule(userID string, ruleID string) error {
|
||||||
// Save saves the user in a storage.
|
// Save saves the user in a storage.
|
||||||
func (s *Storage) Save(user *User) error {
|
func (s *Storage) Save(user *User) error {
|
||||||
log.Println("Saving new user:", user.Username)
|
log.Println("Saving new user:", user.Username)
|
||||||
if err := user.Clean(""); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.back.Save(user)
|
return s.back.Save(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
package users
|
package users
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/rules"
|
"github.com/gtsteffaniak/filebrowser/rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,10 +39,10 @@ type User struct {
|
||||||
Perm Permissions `json:"perm"`
|
Perm Permissions `json:"perm"`
|
||||||
Commands []string `json:"commands"`
|
Commands []string `json:"commands"`
|
||||||
Sorting Sorting `json:"sorting"`
|
Sorting Sorting `json:"sorting"`
|
||||||
Fs afero.Fs `json:"-" yaml:"-"`
|
|
||||||
Rules []rules.Rule `json:"rules"`
|
Rules []rules.Rule `json:"rules"`
|
||||||
HideDotfiles bool `json:"hideDotfiles"`
|
HideDotfiles bool `json:"hideDotfiles"`
|
||||||
DateFormat bool `json:"dateFormat"`
|
DateFormat bool `json:"dateFormat"`
|
||||||
|
GallerySize int `json:"gallerySize"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var PublicUser = User{
|
var PublicUser = User{
|
||||||
|
@ -54,7 +51,6 @@ var PublicUser = User{
|
||||||
Scope: "./",
|
Scope: "./",
|
||||||
ViewMode: "normal",
|
ViewMode: "normal",
|
||||||
LockPassword: true,
|
LockPassword: true,
|
||||||
Fs: afero.NewMemMapFs(),
|
|
||||||
Perm: Permissions{
|
Perm: Permissions{
|
||||||
Create: false,
|
Create: false,
|
||||||
Rename: false,
|
Rename: false,
|
||||||
|
@ -71,26 +67,6 @@ func (u *User) GetRules() []rules.Rule {
|
||||||
return u.Rules
|
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.
|
// CanExecute checks if an user can execute a specific command.
|
||||||
func (u *User) CanExecute(command string) bool {
|
func (u *User) CanExecute(command string) bool {
|
||||||
if !u.Perm.Execute {
|
if !u.Perm.Execute {
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
package version
|
package version
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Version is the current File Browser version.
|
// Dynamically updated during build via build args
|
||||||
Version = "(0.2.7)"
|
Version = "untracked"
|
||||||
// CommitSHA is the commmit sha.
|
CommitSHA = "untracked"
|
||||||
CommitSHA = "(unknown)"
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -35,9 +35,7 @@ auth:
|
||||||
tokenExpirationTime: 2h
|
tokenExpirationTime: 2h
|
||||||
header: ""
|
header: ""
|
||||||
method: json
|
method: json
|
||||||
command: ""
|
|
||||||
signup: false
|
signup: false
|
||||||
shell: ""
|
|
||||||
frontend:
|
frontend:
|
||||||
name: ""
|
name: ""
|
||||||
disableExternal: false
|
disableExternal: false
|
||||||
|
@ -63,7 +61,6 @@ userDefaults:
|
||||||
delete: true
|
delete: true
|
||||||
share: true
|
share: true
|
||||||
download: true
|
download: true
|
||||||
commands: []
|
|
||||||
hideDotfiles: false
|
hideDotfiles: false
|
||||||
dateFormat: false
|
dateFormat: false
|
||||||
```
|
```
|
||||||
|
@ -165,10 +162,6 @@ userDefaults:
|
||||||
- `oath` - oath authentication
|
- `oath` - oath authentication
|
||||||
- `noauth` - no authentication/login required.
|
- `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`
|
- `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`
|
- `AdminUsername`: This is the username of the admin user. Default: `admin`
|
||||||
|
@ -227,7 +220,6 @@ userDefaults:
|
||||||
|
|
||||||
- `download`: This boolean value determines whether download permissions are granted.
|
- `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`)
|
- `hideDotfiles`: This boolean value determines whether dotfiles are hidden. (`true` or `false`)
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
"lint": "npm run typecheck && eslint src/",
|
"lint": "npm run typecheck && eslint src/",
|
||||||
"lint:fix": "eslint --fix src/",
|
"lint:fix": "eslint --fix src/",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"test": "playwright test"
|
"test": "npx playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ace-builds": "^1.24.2",
|
"ace-builds": "^1.24.2",
|
||||||
|
@ -32,6 +32,7 @@
|
||||||
"vue-router": "^4.3.0"
|
"vue-router": "^4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.42.1",
|
||||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vue/eslint-config-typescript": "^13.0.0",
|
"@vue/eslint-config-typescript": "^13.0.0",
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
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,17 +3,18 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from "vue";
|
||||||
import { mutations } from "@/store"; // Import your store's mutations
|
import { mutations } from "@/store"; // Import your store's mutations
|
||||||
|
mutations.setLoading("main-app", true);
|
||||||
export default {
|
export default {
|
||||||
name: "app",
|
name: "app",
|
||||||
computed: {},
|
computed: {},
|
||||||
setup() {
|
setup() {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
mutations.setLoading(false); // Call your mutation or method to set loading to false
|
console.log("static vars", window.FileBrowser);
|
||||||
|
mutations.setLoading("main-app", false);
|
||||||
// Query the loading element and remove it from the DOM
|
// Query the loading element and remove it from the DOM
|
||||||
const loadingDiv = document.getElementById('loading');
|
const loadingDiv = document.getElementById("loading");
|
||||||
if (loadingDiv) {
|
if (loadingDiv) {
|
||||||
loadingDiv.remove();
|
loadingDiv.remove();
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,23 @@
|
||||||
<component :is="element" :to="link.url">{{ link.name }}</component>
|
<component :is="element" :to="link.url">{{ link.name }}</component>
|
||||||
</span>
|
</span>
|
||||||
<action style="display: contents" v-if="showShare" icon="share" show="share" />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { state, mutations } from "@/store"; // Import mutations as well
|
import { state, mutations, getters } from "@/store"; // Import mutations as well
|
||||||
import Action from "@/components/header/Action.vue";
|
import Action from "@/components/header/Action.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -26,8 +38,22 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
Action,
|
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"],
|
props: ["base", "noLink"],
|
||||||
computed: {
|
computed: {
|
||||||
|
isResizableView() {
|
||||||
|
return getters.isResizableView();
|
||||||
|
},
|
||||||
items() {
|
items() {
|
||||||
const relativePath = state.route.path.replace(this.base, "");
|
const relativePath = state.route.path.replace(this.base, "");
|
||||||
let parts = relativePath.split("/");
|
let parts = relativePath.split("/");
|
||||||
|
@ -84,7 +110,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
// Example of a method using mutations
|
// Example of a method using mutations
|
||||||
updateUserPermissions(newPerms) {
|
updateUserPermissions(newPerms) {
|
||||||
mutations.updateUser({ perm: newPerms })
|
mutations.updateUser({ perm: newPerms });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -219,3 +219,14 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.vue-simple-progress {
|
||||||
|
margin: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-simple-progress,
|
||||||
|
.vue-simple-progress-bar {
|
||||||
|
border-radius: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,461 +0,0 @@
|
||||||
<template>
|
|
||||||
<nav
|
|
||||||
id="sidebar"
|
|
||||||
:class="{ active: active, 'dark-mode': isDarkMode, sticky: user?.stickySidebar }"
|
|
||||||
>
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-wrapper">
|
|
||||||
<button
|
|
||||||
v-if="user.username"
|
|
||||||
@click="navigateTo('/settings/profile')"
|
|
||||||
class="action"
|
|
||||||
>
|
|
||||||
<i class="material-icons">person</i>
|
|
||||||
<span>{{ user.username }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<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: user?.stickySidebar }"
|
|
||||||
@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>
|
|
||||||
|
|
||||||
<!-- Settings and Logout buttons -->
|
|
||||||
<div>
|
|
||||||
<!-- Settings button -->
|
|
||||||
<button
|
|
||||||
class="action"
|
|
||||||
@click="navigateTo('/settings/global')"
|
|
||||||
: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>
|
|
||||||
<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 class="usage-info">
|
|
||||||
<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>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<div class="buffer"></div>
|
|
||||||
<div class="credits">
|
|
||||||
<span>
|
|
||||||
<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 auth from "@/utils/auth";
|
|
||||||
import {
|
|
||||||
version,
|
|
||||||
signup,
|
|
||||||
disableExternal,
|
|
||||||
disableUsedPercentage,
|
|
||||||
noAuth,
|
|
||||||
loginPage,
|
|
||||||
} from "@/utils/constants";
|
|
||||||
import { files, users } 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,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hoverText: "Quick Toggles", // Initially empty
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (getters.isLoggedIn()) {
|
|
||||||
this.updateUsage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isMobile() {
|
|
||||||
return getters.isMobile();
|
|
||||||
},
|
|
||||||
isFiles() {
|
|
||||||
return getters.isFiles();
|
|
||||||
},
|
|
||||||
user() {
|
|
||||||
if (!getters.isLoggedIn()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return state.user;
|
|
||||||
},
|
|
||||||
isDarkMode() {
|
|
||||||
return getters.isDarkMode();
|
|
||||||
},
|
|
||||||
isLoggedIn() {
|
|
||||||
return getters.isLoggedIn();
|
|
||||||
},
|
|
||||||
currentPrompt() {
|
|
||||||
return getters.currentPrompt();
|
|
||||||
},
|
|
||||||
active() {
|
|
||||||
return getters.isSidebarVisible();
|
|
||||||
},
|
|
||||||
signup: () => signup,
|
|
||||||
version: () => version,
|
|
||||||
disableExternal: () => disableExternal,
|
|
||||||
disableUsedPercentage: () => disableUsedPercentage,
|
|
||||||
canLogout: () => !noAuth && loginPage,
|
|
||||||
usage: () => state.usage,
|
|
||||||
route: () => state.route,
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
route() {
|
|
||||||
if (!getters.isLoggedIn()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!state.user.stickySidebar) {
|
|
||||||
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() {
|
|
||||||
let newSettings = state.user;
|
|
||||||
newSettings.stickySidebar = !state.user.stickySidebar;
|
|
||||||
users.update(newSettings, ["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) {
|
|
||||||
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>
|
|
||||||
.sidebar-scroll-list {
|
|
||||||
overflow: scroll;
|
|
||||||
margin-bottom: 0px !important;
|
|
||||||
}
|
|
||||||
#sidebar {
|
|
||||||
top: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 1em;
|
|
||||||
padding-top: 5em;
|
|
||||||
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;
|
|
||||||
background-color: #ededed;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sidebar.sticky {
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
@supports (backdrop-filter: none) {
|
|
||||||
nav {
|
|
||||||
background-color: transparent;
|
|
||||||
backdrop-filter: blur(16px) invert(0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.usage-info {
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.rtl nav {
|
|
||||||
left: unset;
|
|
||||||
right: -17em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sidebar.active {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sidebar.rtl nav.active {
|
|
||||||
left: unset;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sidebar > div {
|
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sidebar .card {
|
|
||||||
overflow: unset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credits > span {
|
|
||||||
display: block;
|
|
||||||
margin-top: 0.5em;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credits a,
|
|
||||||
.credits a:hover {
|
|
||||||
color: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buffer {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sources {
|
|
||||||
padding: 1em;
|
|
||||||
margin-top: 0.5em !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inner-card {
|
|
||||||
border-radius: 0.5em;
|
|
||||||
padding: 0px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -10,18 +10,21 @@
|
||||||
@mouseup="mouseUp"
|
@mouseup="mouseUp"
|
||||||
@wheel="wheelMove"
|
@wheel="wheelMove"
|
||||||
>
|
>
|
||||||
|
<div v-if="!isLoaded">Loading image...</div>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
v-if="!isTiff"
|
v-if="!isTiff && isLoaded"
|
||||||
:src="src"
|
:src="src"
|
||||||
class="image-ex-img image-ex-img-center"
|
class="image-ex-img"
|
||||||
ref="imgex"
|
ref="imgex"
|
||||||
@load="onLoad"
|
@load="onLoad"
|
||||||
/>
|
/>
|
||||||
<canvas v-else ref="imgex" class="image-ex-img"></canvas>
|
<canvas v-else-if="isLoaded" ref="imgex" class="image-ex-img"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { state, mutations, getters } from "@/store";
|
||||||
import throttle from "@/utils/throttle";
|
import throttle from "@/utils/throttle";
|
||||||
import { showError } from "@/notify";
|
import { showError } from "@/notify";
|
||||||
export default {
|
export default {
|
||||||
|
@ -82,8 +85,18 @@ export default {
|
||||||
window.removeEventListener("resize", this.onResize);
|
window.removeEventListener("resize", this.onResize);
|
||||||
document.removeEventListener("mouseup", this.onMouseUp);
|
document.removeEventListener("mouseup", this.onMouseUp);
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
isLoaded() {
|
||||||
|
console.log(state.loading);
|
||||||
|
return !("preview-img" in state.loading);
|
||||||
|
},
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
src: function () {
|
src: function () {
|
||||||
|
if (this.src == undefined || this.$refs.imgex == undefined) {
|
||||||
|
mutations.setLoading("preview-img", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.isTiff = this.checkIfTiff(this.src);
|
this.isTiff = this.checkIfTiff(this.src);
|
||||||
if (this.isTiff) {
|
if (this.isTiff) {
|
||||||
this.decodeTiff(this.src);
|
this.decodeTiff(this.src);
|
||||||
|
@ -94,6 +107,8 @@ export default {
|
||||||
this.scale = 1;
|
this.scale = 1;
|
||||||
this.setZoom();
|
this.setZoom();
|
||||||
this.setCenter();
|
this.setCenter();
|
||||||
|
mutations.setLoading("preview-img", false);
|
||||||
|
this.showSpinner = false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -121,36 +136,6 @@ export default {
|
||||||
console.error("Error decoding TIFF:", error);
|
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() {
|
onMouseUp() {
|
||||||
this.inDrag = false;
|
this.inDrag = false;
|
||||||
},
|
},
|
||||||
|
@ -258,32 +243,19 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doMove(x, y) {
|
doMove(x, y) {
|
||||||
let style = this.$refs.imgex.style;
|
this.position.relative.x += x;
|
||||||
let posX = this.pxStringToNumber(style.left) + x;
|
this.position.relative.y += y;
|
||||||
let posY = this.pxStringToNumber(style.top) + 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})`;
|
||||||
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) {
|
wheelMove(event) {
|
||||||
this.scale += -Math.sign(event.deltaY) * this.zoomStep;
|
this.scale += -Math.sign(event.deltaY) * this.zoomStep;
|
||||||
this.setZoom();
|
this.setZoom();
|
||||||
},
|
},
|
||||||
setZoom() {
|
setZoom() {
|
||||||
this.scale = this.scale < this.minScale ? this.minScale : this.scale;
|
this.scale = Math.max(this.minScale, Math.min(this.maxScale, this.scale));
|
||||||
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale;
|
// Update the transform with both translate and scale values
|
||||||
this.$refs.imgex.style.transform = `scale(${this.scale})`;
|
this.$refs.imgex.style.transform = `translate(${this.position.relative.x}px, ${this.position.relative.y}px) scale(${this.scale})`;
|
||||||
},
|
},
|
||||||
pxStringToNumber(style) {
|
pxStringToNumber(style) {
|
||||||
return +style.replace("px", "");
|
return +style.replace("px", "");
|
||||||
|
@ -294,26 +266,17 @@ export default {
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.image-ex-container {
|
.image-ex-container {
|
||||||
margin: auto;
|
max-width: 100%; /* Image container max width */
|
||||||
overflow: hidden;
|
max-height: 100%; /* Image container max height */
|
||||||
position: relative;
|
overflow: hidden; /* Hide overflow if image exceeds container */
|
||||||
|
position: relative; /* Required for absolute positioning of child */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-ex-img {
|
.image-ex-img {
|
||||||
|
max-width: 100%; /* Image max width */
|
||||||
|
max-height: 100%; /* Image max height */
|
||||||
position: absolute;
|
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>
|
</style>
|
||||||
|
|
|
@ -31,12 +31,10 @@
|
||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="{ activecontent: this.isMaximized && this.isSelected }">
|
<div class="text" :class="{ activecontent: this.isMaximized && this.isSelected }">
|
||||||
<p class="name">{{ name }}</p>
|
<p class="name">{{ name }}</p>
|
||||||
|
|
||||||
<p v-if="isDir" class="size" data-order="-1">—</p>
|
<p v-if="isDir" class="size" data-order="-1">—</p>
|
||||||
<p v-else class="size" :data-order="humanSize()">{{ humanSize() }}</p>
|
<p v-else class="size" :data-order="humanSize()">{{ humanSize() }}</p>
|
||||||
|
|
||||||
<p class="modified">
|
<p class="modified">
|
||||||
<time :datetime="modified">{{ humanTime() }}</time>
|
<time :datetime="modified">{{ humanTime() }}</time>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import { state, getters, mutations } from "@/store";
|
import { state, getters, mutations } from "@/store";
|
||||||
import { showError } from "@/notify";
|
import { showError,showSuccess } from "@/notify";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "delete",
|
name: "delete",
|
||||||
|
@ -59,6 +59,7 @@ export default {
|
||||||
if (!this.isListing) {
|
if (!this.isListing) {
|
||||||
await api.remove(state.route.path);
|
await api.remove(state.route.path);
|
||||||
buttons.success("delete");
|
buttons.success("delete");
|
||||||
|
showSuccess("Deleted item successfully")
|
||||||
|
|
||||||
this.currentPrompt?.confirm();
|
this.currentPrompt?.confirm();
|
||||||
this.closeHovers();
|
this.closeHovers();
|
||||||
|
@ -78,6 +79,7 @@ export default {
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
buttons.success("delete");
|
buttons.success("delete");
|
||||||
|
showSuccess("Deleted item successfully")
|
||||||
mutations.setReload(true); // Handle reload as needed
|
mutations.setReload(true); // Handle reload as needed
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
buttons.done("delete");
|
buttons.done("delete");
|
||||||
|
|
|
@ -111,18 +111,17 @@ export default {
|
||||||
confirm: (event, option) => {
|
confirm: (event, option) => {
|
||||||
overwrite = option == "overwrite";
|
overwrite = option == "overwrite";
|
||||||
rename = option == "rename";
|
rename = option == "rename";
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
mutations.closeHovers();
|
mutations.closeHovers();
|
||||||
action(overwrite, rename);
|
action(overwrite, rename);
|
||||||
mutations.setReload(true);
|
mutations.setReload(true);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
action(overwrite, rename);
|
action(overwrite, rename);
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,7 +25,7 @@ import Share from "./Share.vue";
|
||||||
import Upload from "./Upload.vue";
|
import Upload from "./Upload.vue";
|
||||||
import ShareDelete from "./ShareDelete.vue";
|
import ShareDelete from "./ShareDelete.vue";
|
||||||
import DeleteUser from "./DeleteUser.vue";
|
import DeleteUser from "./DeleteUser.vue";
|
||||||
import Sidebar from "../Sidebar.vue";
|
import Sidebar from "../sidebar/Sidebar.vue";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||||
|
|
||||||
|
|
|
@ -7,29 +7,32 @@
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p>{{ $t("prompts.replaceMessage") }}</p>
|
<p>{{ $t("prompts.replaceMessage") }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<button
|
<button
|
||||||
class="button button--flat button--grey"
|
class="button button--flat button--grey"
|
||||||
@click="(event) => currentPrompt.confirm(event, 'rename')"
|
@click="closeHovers"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')"
|
:title="$t('buttons.cancel')"
|
||||||
|
tabindex="3"
|
||||||
>
|
>
|
||||||
{{ $t("buttons.cancel") }}
|
{{ $t("buttons.cancel") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button button--flat button--blue"
|
class="button button--flat button--blue"
|
||||||
@click="(event) => currentPrompt.confirm(event, 'overwrite')"
|
@click="(event) => currentPrompt.confirm(event, 'rename')"
|
||||||
:aria-label="$t('buttons.rename')"
|
:aria-label="$t('buttons.rename')"
|
||||||
:title="$t('buttons.rename')"
|
:title="$t('buttons.rename')"
|
||||||
|
tabindex="2"
|
||||||
>
|
>
|
||||||
{{ $t("buttons.rename") }}
|
{{ $t("buttons.rename") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
id="focus-prompt"
|
||||||
class="button button--flat button--red"
|
class="button button--flat button--red"
|
||||||
@click="(event) => showConfirm(event, 'overwrite')"
|
@click="(event) => currentPrompt.confirm(event, 'overwrite')"
|
||||||
:aria-label="$t('buttons.replace')"
|
:aria-label="$t('buttons.replace')"
|
||||||
:title="$t('buttons.replace')"
|
:title="$t('buttons.replace')"
|
||||||
|
tabindex="1"
|
||||||
>
|
>
|
||||||
{{ $t("buttons.replace") }}
|
{{ $t("buttons.replace") }}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -67,6 +67,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { state } from "@/store"
|
||||||
import Languages from "./Languages.vue";
|
import Languages from "./Languages.vue";
|
||||||
import Rules from "./Rules.vue";
|
import Rules from "./Rules.vue";
|
||||||
import Permissions from "./Permissions.vue";
|
import Permissions from "./Permissions.vue";
|
||||||
|
@ -87,12 +88,15 @@ export default {
|
||||||
Rules,
|
Rules,
|
||||||
Commands,
|
Commands,
|
||||||
},
|
},
|
||||||
props: ["user", "createUserDir", "isNew", "isDefault"],
|
props: [ "createUserDir", "isNew", "isDefault"],
|
||||||
created() {
|
created() {
|
||||||
this.originalUserScope = this.user.scope;
|
this.originalUserScope = state.user.scope;
|
||||||
this.createUserDirData = this.createUserDir;
|
this.createUserDirData = this.createUserDir;
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
user() {
|
||||||
|
return state.user;
|
||||||
|
},
|
||||||
passwordPlaceholder() {
|
passwordPlaceholder() {
|
||||||
return this.isNew ? "" : this.$t("settings.avoidChanges");
|
return this.isNew ? "" : this.$t("settings.avoidChanges");
|
||||||
},
|
},
|
||||||
|
@ -109,12 +113,8 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
"user.perm.admin": function () {
|
|
||||||
if (!this.user.perm.admin) return;
|
|
||||||
this.user.lockPassword = false;
|
|
||||||
},
|
|
||||||
createUserDirData(newVal) {
|
createUserDirData(newVal) {
|
||||||
this.user.scope = newVal ? "" : this.originalUserScope;
|
state.user.scope = newVal ? "" : this.originalUserScope;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,320 @@
|
||||||
|
<template>
|
||||||
|
<div class="card clickable" style="min-height: 4em">
|
||||||
|
<div class="card-wrapper user-card">
|
||||||
|
<div @click="navigateTo('/settings#profile-main')" class="inner-card">
|
||||||
|
{{ 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;
|
||||||
|
}
|
||||||
|
console.log("Fetching usage for", path, state.user);
|
||||||
|
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>
|
|
@ -0,0 +1,42 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,171 @@
|
||||||
|
<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>
|
|
@ -172,6 +172,7 @@ button:disabled {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
transition: right 1s ease; /* Animate the 'right' property */
|
transition: right 1s ease; /* Animate the 'right' property */
|
||||||
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
#popup-notification-content {
|
#popup-notification-content {
|
||||||
|
|
|
@ -303,6 +303,7 @@ body.rtl .card .card-title>*:first-child {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
|
top: 0;
|
||||||
animation: .3s show ease-in;
|
animation: .3s show ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,16 +9,13 @@ header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background-color: white;
|
background-color: rgb(255 255 255 / 50%) !important;
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.075);
|
|
||||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@supports (backdrop-filter: none) {
|
@supports (backdrop-filter: none) {
|
||||||
header {
|
header {
|
||||||
background-color: transparent;
|
backdrop-filter: blur(16px);
|
||||||
backdrop-filter: blur(16px) invert(0.1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,10 +53,6 @@ body.rtl #listingView {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listingView .item .name {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
#listingView .item i {
|
#listingView .item i {
|
||||||
font-size: 4em;
|
font-size: 4em;
|
||||||
margin-right: 0.1em;
|
margin-right: 0.1em;
|
||||||
|
@ -103,7 +99,6 @@ body.rtl #listingView {
|
||||||
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#listingView.list .item,
|
#listingView.list .item,
|
||||||
#listingView.compact .item {
|
#listingView.compact .item {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
@ -122,25 +117,49 @@ body.rtl #listingView {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listingView.gallery .item div:first-of-type {
|
#listingView.gallery .item div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 12em;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listingView.gallery .item div:last-of-type {
|
#listingView.gallery .item div:last-of-type {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0.5em;
|
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
width: calc(100% - 1em);
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listingView.gallery .item[data-type=image] div:last-of-type {
|
#listingView.gallery .item[data-type=image] div:last-of-type {
|
||||||
color: white;
|
color: white;
|
||||||
background: linear-gradient(#0000, #0009);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#listingView.gallery .item i {
|
#listingView.gallery .item i {
|
||||||
|
display:flex !important;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
font-size: 8em;
|
font-size: 8em;
|
||||||
|
|
|
@ -3,12 +3,7 @@ import Login from "@/views/Login.vue";
|
||||||
import Layout from "@/views/Layout.vue";
|
import Layout from "@/views/Layout.vue";
|
||||||
import Files from "@/views/Files.vue";
|
import Files from "@/views/Files.vue";
|
||||||
import Share from "@/views/Share.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 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 Errors from "@/views/Errors.vue";
|
||||||
import { baseURL, name } from "@/utils/constants";
|
import { baseURL, name } from "@/utils/constants";
|
||||||
import { getters, state } from "@/store";
|
import { getters, state } from "@/store";
|
||||||
|
@ -74,45 +69,6 @@ const routes = [
|
||||||
path: "",
|
path: "",
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
component: 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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import { state } from "./state.js";
|
import { state } from "./state.js";
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
|
isResizableView: () => (state.user.viewMode == "gallery" || state.user.viewMode == "normal" ) && getters.currentView() == "listingView" ,
|
||||||
|
currentHash: () => state.route.hash.replace("#", ""),
|
||||||
isMobile: () => state.isMobile,
|
isMobile: () => state.isMobile,
|
||||||
|
isLoading: () => Object.keys(state.loading).length > 0,
|
||||||
|
isSettings: () => getters.currentView() === "settings",
|
||||||
isDarkMode: () => {
|
isDarkMode: () => {
|
||||||
if (state.user == null) {
|
if (state.user == null) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -20,28 +24,80 @@ export const getters = {
|
||||||
let selectedItem = state.selected[0]
|
let selectedItem = state.selected[0]
|
||||||
return state.req.items[selectedItem].url;
|
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: () => {
|
isSidebarVisible: () => {
|
||||||
if (!getters.isLoggedIn()) {
|
let visible = state.showSidebar || getters.isStickySidebar()
|
||||||
return false;
|
if (getters.currentView() == "settings") {
|
||||||
|
visible = !getters.isMobile();
|
||||||
}
|
}
|
||||||
console.log(getters.currentPromptName());
|
|
||||||
if (typeof getters.currentPromptName() === "string" && !getters.isStickySidebar()) {
|
if (typeof getters.currentPromptName() === "string" && !getters.isStickySidebar()) {
|
||||||
return false;
|
visible = false;
|
||||||
}
|
}
|
||||||
console.log(getters.currentView());
|
return visible
|
||||||
if (getters.currentView() !== "listingView") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return state.showSidebar || getters.isStickySidebar();
|
|
||||||
},
|
},
|
||||||
isStickySidebar: () => {
|
isStickySidebar: () => {
|
||||||
if (getters.isMobile()) {
|
let sticky = state.user?.stickySidebar
|
||||||
return false
|
if (getters.currentView() == "settings") {
|
||||||
|
sticky = true
|
||||||
}
|
}
|
||||||
if (!getters.isLoggedIn()) {
|
if (getters.currentView() == null && !getters.isLoading()) {
|
||||||
return true
|
sticky = true
|
||||||
}
|
}
|
||||||
return state.user?.stickySidebar
|
if (getters.isMobile() || getters.currentView() == "preview") {
|
||||||
|
sticky = false
|
||||||
|
}
|
||||||
|
return sticky
|
||||||
},
|
},
|
||||||
showOverlay: () => {
|
showOverlay: () => {
|
||||||
if (!getters.isLoggedIn()) {
|
if (!getters.isLoggedIn()) {
|
||||||
|
@ -57,17 +113,21 @@ export const getters = {
|
||||||
: state.route.path + "/";
|
: state.route.path + "/";
|
||||||
},
|
},
|
||||||
currentView: () => {
|
currentView: () => {
|
||||||
let returnVal = null;
|
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.type !== undefined) {
|
||||||
if (state.req.isDir) {
|
if (state.req.isDir) {
|
||||||
returnVal = "listingView";
|
return "listingView";
|
||||||
} else if ("content" in state.req) {
|
} else if ("content" in state.req) {
|
||||||
returnVal = "editor";
|
return "editor";
|
||||||
} else {
|
} else {
|
||||||
returnVal = "preview";
|
return "preview";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return returnVal;
|
}
|
||||||
|
return null
|
||||||
},
|
},
|
||||||
progress: () => {
|
progress: () => {
|
||||||
// Check if state.upload is defined and valid
|
// Check if state.upload is defined and valid
|
||||||
|
|
|
@ -1,11 +1,33 @@
|
||||||
import * as i18n from "@/i18n";
|
import * as i18n from "@/i18n";
|
||||||
import { state } from "./state.js";
|
import { state } from "./state.js";
|
||||||
|
import router from "@/router";
|
||||||
import { emitStateChanged } from './eventBus'; // Import the function from eventBus.js
|
import { emitStateChanged } from './eventBus'; // Import the function from eventBus.js
|
||||||
import { users } from "@/api";
|
import { users } from "@/api";
|
||||||
|
|
||||||
export const mutations = {
|
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() {
|
setMobile() {
|
||||||
state.mobile = window.innerWidth <= 800
|
state.isMobile = window.innerWidth <= 800
|
||||||
emitStateChanged();
|
emitStateChanged();
|
||||||
},
|
},
|
||||||
toggleDarkMode() {
|
toggleDarkMode() {
|
||||||
|
@ -13,12 +35,13 @@ export const mutations = {
|
||||||
emitStateChanged();
|
emitStateChanged();
|
||||||
},
|
},
|
||||||
toggleSidebar() {
|
toggleSidebar() {
|
||||||
if (!state.showSidebar && state.user.stickySidebar) {
|
if (state.user.stickySidebar) {
|
||||||
state.user.stickySidebar = false;
|
localStorage.setItem("stickySidebar", "false");
|
||||||
mutations.updateUser({ "stickySidebar": false }); // turn off sticky when closed
|
mutations.updateUser({ "stickySidebar": false }); // turn off sticky when closed
|
||||||
return
|
state.showSidebar = false;
|
||||||
}
|
} else {
|
||||||
state.showSidebar = !state.showSidebar;
|
state.showSidebar = !state.showSidebar;
|
||||||
|
}
|
||||||
emitStateChanged();
|
emitStateChanged();
|
||||||
},
|
},
|
||||||
closeSidebar() {
|
closeSidebar() {
|
||||||
|
@ -37,10 +60,9 @@ export const mutations = {
|
||||||
},
|
},
|
||||||
closeHovers: () => {
|
closeHovers: () => {
|
||||||
state.prompts = [];
|
state.prompts = [];
|
||||||
emitStateChanged();
|
if (!state.stickySidebar) {
|
||||||
},
|
state.showSidebar = false;
|
||||||
toggleShell: () => {
|
}
|
||||||
state.showShell = !state.showShell;
|
|
||||||
emitStateChanged();
|
emitStateChanged();
|
||||||
},
|
},
|
||||||
showHover: (value) => {
|
showHover: (value) => {
|
||||||
|
@ -65,8 +87,12 @@ export const mutations = {
|
||||||
state.prompts.push("error");
|
state.prompts.push("error");
|
||||||
emitStateChanged();
|
emitStateChanged();
|
||||||
},
|
},
|
||||||
setLoading: (value) => {
|
setLoading: (loadType, status) => {
|
||||||
state.loading = value;
|
if (status === false) {
|
||||||
|
delete state.loading[loadType];
|
||||||
|
} else {
|
||||||
|
state.loading = { ...state.loading, [loadType]: true };
|
||||||
|
}
|
||||||
emitStateChanged();
|
emitStateChanged();
|
||||||
},
|
},
|
||||||
setReload: (value) => {
|
setReload: (value) => {
|
||||||
|
@ -130,6 +156,7 @@ export const mutations = {
|
||||||
i18n.setLocale(state.user.locale);
|
i18n.setLocale(state.user.locale);
|
||||||
i18n.default.locale = state.user.locale;
|
i18n.default.locale = state.user.locale;
|
||||||
}
|
}
|
||||||
|
localStorage.setItem("stickySidebar", state.user.stickySidebar);
|
||||||
if (state.user != previousUser) {
|
if (state.user != previousUser) {
|
||||||
users.update(state.user);
|
users.update(state.user);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { reactive } from 'vue';
|
||||||
import { detectLocale } from "@/i18n";
|
import { detectLocale } from "@/i18n";
|
||||||
|
|
||||||
export const state = reactive({
|
export const state = reactive({
|
||||||
|
activeSettingsView: "",
|
||||||
isMobile: window.innerWidth <= 800,
|
isMobile: window.innerWidth <= 800,
|
||||||
showSidebar: false,
|
showSidebar: false,
|
||||||
usage: {
|
usage: {
|
||||||
|
@ -11,9 +12,10 @@ export const state = reactive({
|
||||||
},
|
},
|
||||||
editor: null,
|
editor: null,
|
||||||
user: {
|
user: {
|
||||||
stickySidebar: false,
|
gallarySize: 0,
|
||||||
|
stickySidebar: stickyStartup(),
|
||||||
locale: detectLocale(), // Default to the locale from moment
|
locale: detectLocale(), // Default to the locale from moment
|
||||||
viewMode: 'mosaic', // Default to mosaic view
|
viewMode: 'normal', // Default to mosaic view
|
||||||
hideDotfiles: false, // Default to false, assuming this is a boolean
|
hideDotfiles: false, // Default to false, assuming this is a boolean
|
||||||
perm: {},
|
perm: {},
|
||||||
rules: [], // Default to an empty array
|
rules: [], // Default to an empty array
|
||||||
|
@ -40,7 +42,7 @@ export const state = reactive({
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
jwt: "",
|
jwt: "",
|
||||||
loading: false,
|
loading: [],
|
||||||
reload: false,
|
reload: false,
|
||||||
selected: [],
|
selected: [],
|
||||||
multiple: false,
|
multiple: false,
|
||||||
|
@ -54,4 +56,21 @@ export const state = reactive({
|
||||||
show: null,
|
show: null,
|
||||||
showConfirm: null,
|
showConfirm: null,
|
||||||
route: {},
|
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,6 +8,7 @@ const recaptcha = window.FileBrowser.ReCaptcha;
|
||||||
const recaptchaKey = window.FileBrowser.ReCaptchaKey;
|
const recaptchaKey = window.FileBrowser.ReCaptchaKey;
|
||||||
const signup = window.FileBrowser.Signup;
|
const signup = window.FileBrowser.Signup;
|
||||||
const version = window.FileBrowser.Version;
|
const version = window.FileBrowser.Version;
|
||||||
|
const commitSHA = window.FileBrowser.CommitSHA;
|
||||||
const logoURL = `${staticURL}/img/logo.png`;
|
const logoURL = `${staticURL}/img/logo.png`;
|
||||||
const noAuth = window.FileBrowser.NoAuth;
|
const noAuth = window.FileBrowser.NoAuth;
|
||||||
const authMethod = window.FileBrowser.AuthMethod;
|
const authMethod = window.FileBrowser.AuthMethod;
|
||||||
|
@ -17,6 +18,13 @@ const resizePreview = window.FileBrowser.ResizePreview;
|
||||||
const enableExec = window.FileBrowser.EnableExec;
|
const enableExec = window.FileBrowser.EnableExec;
|
||||||
const origin = window.location.origin;
|
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 {
|
export {
|
||||||
name,
|
name,
|
||||||
disableExternal,
|
disableExternal,
|
||||||
|
@ -27,6 +35,7 @@ export {
|
||||||
recaptchaKey,
|
recaptchaKey,
|
||||||
signup,
|
signup,
|
||||||
version,
|
version,
|
||||||
|
commitSHA,
|
||||||
noAuth,
|
noAuth,
|
||||||
authMethod,
|
authMethod,
|
||||||
loginPage,
|
loginPage,
|
||||||
|
@ -34,5 +43,6 @@ export {
|
||||||
resizePreview,
|
resizePreview,
|
||||||
enableExec,
|
enableExec,
|
||||||
origin,
|
origin,
|
||||||
darkMode
|
darkMode,
|
||||||
|
settings
|
||||||
};
|
};
|
||||||
|
|
|
@ -71,15 +71,12 @@ export default {
|
||||||
window.removeEventListener("keydown", this.keyEvent);
|
window.removeEventListener("keydown", this.keyEvent);
|
||||||
},
|
},
|
||||||
unmounted() {
|
unmounted() {
|
||||||
if (state.showShell) {
|
|
||||||
mutations.toggleShell(); // Use mutation
|
|
||||||
}
|
|
||||||
mutations.replaceRequest({}); // Use mutation
|
mutations.replaceRequest({}); // Use mutation
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async fetchData() {
|
async fetchData() {
|
||||||
// Set loading to true and reset the error.
|
// Set loading to true and reset the error.
|
||||||
mutations.setLoading(true);
|
mutations.setLoading("files", true);
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
// Reset view information using mutations
|
// Reset view information using mutations
|
||||||
|
@ -106,9 +103,10 @@ export default {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.error = e;
|
this.error = e;
|
||||||
|
mutations.replaceRequest(null);
|
||||||
} finally {
|
} finally {
|
||||||
mutations.setLoading(false);
|
|
||||||
mutations.replaceRequest(data);
|
mutations.replaceRequest(data);
|
||||||
|
mutations.setLoading("files", false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyEvent(event) {
|
keyEvent(event) {
|
||||||
|
|
|
@ -31,7 +31,7 @@ import editorBar from "./bars/EditorBar.vue";
|
||||||
import defaultBar from "./bars/Default.vue";
|
import defaultBar from "./bars/Default.vue";
|
||||||
import listingBar from "./bars/ListingBar.vue";
|
import listingBar from "./bars/ListingBar.vue";
|
||||||
import Prompts from "@/components/prompts/Prompts.vue";
|
import Prompts from "@/components/prompts/Prompts.vue";
|
||||||
import Sidebar from "@/components/Sidebar.vue";
|
import Sidebar from "@/components/sidebar/Sidebar.vue";
|
||||||
import Search from "@/components/Search.vue";
|
import Search from "@/components/Search.vue";
|
||||||
import fileSelection from "@/components/FileSelection.vue";
|
import fileSelection from "@/components/FileSelection.vue";
|
||||||
|
|
||||||
|
@ -69,9 +69,7 @@ export default {
|
||||||
return getters.isLoggedIn();
|
return getters.isLoggedIn();
|
||||||
},
|
},
|
||||||
moveWithSidebar() {
|
moveWithSidebar() {
|
||||||
return (
|
return getters.isSidebarVisible() && getters.isStickySidebar();
|
||||||
getters.isSidebarVisible() && !getters.isMobile() && state.user?.stickySidebar
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
closePopUp() {
|
closePopUp() {
|
||||||
return closePopUp;
|
return closePopUp;
|
||||||
|
@ -127,13 +125,6 @@ export default {
|
||||||
mutations.closeSidebar();
|
mutations.closeSidebar();
|
||||||
mutations.closeHovers();
|
mutations.closeHovers();
|
||||||
},
|
},
|
||||||
getTitle() {
|
|
||||||
let title = "Title";
|
|
||||||
if (state.route.path.startsWith("/settings/")) {
|
|
||||||
title = "Settings";
|
|
||||||
}
|
|
||||||
return title;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -161,13 +152,13 @@ main::-webkit-scrollbar {
|
||||||
/* Header */
|
/* Header */
|
||||||
.dark-mode-header {
|
.dark-mode-header {
|
||||||
color: white;
|
color: white;
|
||||||
background: var(--surfacePrimary);
|
background-color: rgb(255 255 255 / 50%) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header with backdrop-filter support */
|
/* Header with backdrop-filter support */
|
||||||
@supports (backdrop-filter: none) {
|
@supports (backdrop-filter: none) {
|
||||||
.dark-mode-header {
|
.dark-mode-header {
|
||||||
background: transparent;
|
background-color: rgb(37 49 55 / 33%) !important;
|
||||||
backdrop-filter: blur(16px) invert(0.1);
|
backdrop-filter: blur(16px) invert(0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
<div id="nav">
|
<div class="settings-views">
|
||||||
<div v-if="settingsEnabled" class="wrapper">
|
<div
|
||||||
<ul>
|
v-for="setting in settings"
|
||||||
<router-link to="/settings/profile"
|
:key="setting.id + '-main'"
|
||||||
><li :class="{ active: $route.path === '/settings/profile' }">
|
:id="setting.id + '-main'"
|
||||||
{{ $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="{
|
:class="{
|
||||||
active: $route.path === '/settings/users' || $route.name === 'User',
|
active: active(setting.id + '-main'),
|
||||||
|
clickable: !active(setting.id + '-main'),
|
||||||
}"
|
}"
|
||||||
|
@click="!active(setting.id + '-main') && setView(setting.id + '-main')"
|
||||||
>
|
>
|
||||||
{{ $t("settings.userManagement") }}
|
<!-- Dynamically render the component based on the setting -->
|
||||||
</li></router-link
|
<component :is="setting.component"></component>
|
||||||
>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -41,30 +26,74 @@
|
||||||
<span>{{ $t("files.loading") }}</span>
|
<span>{{ $t("files.loading") }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<router-view></router-view>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { state, mutations } from "@/store";
|
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";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "settings",
|
name: "settings",
|
||||||
mounted() {
|
components: {
|
||||||
// Update the req name property
|
GlobalSettings,
|
||||||
mutations.replaceRequest({ name: "Settings" });
|
UserDefaultSettings,
|
||||||
|
UserColumnSettings,
|
||||||
|
ProfileSettings,
|
||||||
|
SharesSettings,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
settings, // Initialize the settings array in data
|
||||||
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
loading() {
|
loading() {
|
||||||
return state.loading;
|
return getters.isLoading();
|
||||||
},
|
},
|
||||||
user() {
|
user() {
|
||||||
return state.user;
|
return state.user;
|
||||||
},
|
},
|
||||||
settingsEnabled() {
|
currentHash() {
|
||||||
return state.user.disableSettings == false;
|
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);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</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
|
return state.req; // Access state directly from the store
|
||||||
},
|
},
|
||||||
loading() {
|
loading() {
|
||||||
return state.loading; // Access state directly from the store
|
return getters.isLoading(); // Access state directly from the store
|
||||||
},
|
},
|
||||||
multiple() {
|
multiple() {
|
||||||
return state.multiple; // Access state directly from the store
|
return state.multiple; // Access state directly from the store
|
||||||
|
@ -257,7 +257,7 @@ export default {
|
||||||
},
|
},
|
||||||
async fetchData() {
|
async fetchData() {
|
||||||
// Set loading to true and reset the error.
|
// Set loading to true and reset the error.
|
||||||
mutations.setLoading(true);
|
mutations.setLoading("share", true);
|
||||||
this.error = null;
|
this.error = null;
|
||||||
// Reset view information.
|
// Reset view information.
|
||||||
if (!getters.isLoggedIn()) {
|
if (!getters.isLoggedIn()) {
|
||||||
|
@ -278,7 +278,7 @@ export default {
|
||||||
this.token = file.token || "";
|
this.token = file.token || "";
|
||||||
mutations.updateRequest(file);
|
mutations.updateRequest(file);
|
||||||
document.title = `${file.name} - ${document.title}`;
|
document.title = `${file.name} - ${document.title}`;
|
||||||
mutations.setLoading(false);
|
mutations.setLoading("share", false);
|
||||||
},
|
},
|
||||||
keyEvent(event) {
|
keyEvent(event) {
|
||||||
// Esc!
|
// Esc!
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<action icon="close" :label="$t('buttons.close')" @action="close()" />
|
<action icon="close" :label="$t('buttons.close')" @action="close()" />
|
||||||
<title class="topTitle">{{ req.name }}</title>
|
<title v-if="isSettings" class="topTitle">Settings</title>
|
||||||
|
<title v-else class="topTitle">{{ req.name }}</title>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -15,7 +16,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import url from "@/utils/url"
|
import url from "@/utils/url"
|
||||||
import { state, mutations } from "@/store";
|
import router from "@/router";
|
||||||
|
import { state, mutations, getters } from "@/store";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import Action from "@/components/header/Action.vue";
|
import Action from "@/components/header/Action.vue";
|
||||||
import css from "@/utils/css";
|
import css from "@/utils/css";
|
||||||
|
@ -27,7 +29,7 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
columnWidth: 280,
|
columnWidth: 350,
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
itemWeight: 0,
|
itemWeight: 0,
|
||||||
viewModes: ["list", "compact", "normal", "gallery"],
|
viewModes: ["list", "compact", "normal", "gallery"],
|
||||||
|
@ -35,6 +37,9 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
isSettings() {
|
||||||
|
return getters.isSettings();
|
||||||
|
},
|
||||||
// Map state and getters
|
// Map state and getters
|
||||||
req() {
|
req() {
|
||||||
return state.req;
|
return state.req;
|
||||||
|
@ -45,10 +50,6 @@ export default {
|
||||||
selected() {
|
selected() {
|
||||||
return state.selected;
|
return state.selected;
|
||||||
},
|
},
|
||||||
|
|
||||||
isSettings() {
|
|
||||||
return state.route.path.includes("/settings/");
|
|
||||||
},
|
|
||||||
nameSorted() {
|
nameSorted() {
|
||||||
return state.req.sorting.by === "name";
|
return state.req.sorting.by === "name";
|
||||||
},
|
},
|
||||||
|
@ -201,7 +202,7 @@ export default {
|
||||||
);
|
);
|
||||||
let items = css(["#listingView .item", "#listingView .item"]);
|
let items = css(["#listingView .item", "#listingView .item"]);
|
||||||
if (columns === 0) columns = 1;
|
if (columns === 0) columns = 1;
|
||||||
items.style.width = `calc(${100 / columns}% - 1em)`;
|
items.style.width = `calc(${100 / columns}%)`;
|
||||||
},
|
},
|
||||||
action() {
|
action() {
|
||||||
if (this.show) {
|
if (this.show) {
|
||||||
|
@ -211,15 +212,16 @@ export default {
|
||||||
this.$emit("action");
|
this.$emit("action");
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
if (this.isSettings) {
|
if (getters.isSettings()) {
|
||||||
// Use this.isSettings to access the computed property
|
// Use this.isSettings to access the computed property
|
||||||
this.$router.push({ path: "/files/" }, () => {});
|
router.push({ path: "/files/",hash: "" });
|
||||||
mutations.closeHovers();
|
mutations.closeHovers();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mutations.replaceRequest({});
|
mutations.replaceRequest({});
|
||||||
let uri = url.removeLastDir(state.route.path) + "/";
|
let uri = url.removeLastDir(state.route.path) + "/";
|
||||||
this.$router.push({ path: uri });
|
router.push({ path: uri });
|
||||||
|
mutations.closeHovers();
|
||||||
},
|
},
|
||||||
base64(name) {
|
base64(name) {
|
||||||
return window.btoa(unescape(encodeURIComponent(name)));
|
return window.btoa(unescape(encodeURIComponent(name)));
|
||||||
|
|
|
@ -30,7 +30,7 @@ import { state, mutations } from "@/store";
|
||||||
import { eventBus } from "@/store/eventBus";
|
import { eventBus } from "@/store/eventBus";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { showError } from "@/notify";
|
import { showError, showSuccess } from "@/notify";
|
||||||
|
|
||||||
import Action from "@/components/header/Action.vue";
|
import Action from "@/components/header/Action.vue";
|
||||||
|
|
||||||
|
@ -108,9 +108,10 @@ export default {
|
||||||
try {
|
try {
|
||||||
eventBus.emit("handleEditorValueRequest", "data");
|
eventBus.emit("handleEditorValueRequest", "data");
|
||||||
buttons.success(button);
|
buttons.success(button);
|
||||||
|
showSuccess("File Saved!");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
buttons.done(button);
|
buttons.done(button);
|
||||||
showError(e);
|
showError("Error saving file: ", e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
:class="listingViewMode + ' file-icons'"
|
:class="listingViewMode + ' file-icons'"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div class="header" :class="{ 'dark-mode-item-header': isDarkMode }" >
|
<div class="header" :class="{ 'dark-mode-item-header': isDarkMode }">
|
||||||
<p
|
<p
|
||||||
:class="{ active: nameSorted }"
|
:class="{ active: nameSorted }"
|
||||||
class="name"
|
class="name"
|
||||||
|
@ -79,12 +79,16 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="numDirs > 0" >
|
<div v-if="numDirs > 0">
|
||||||
<div class="header-items">
|
<div class="header-items">
|
||||||
<h2>{{ $t("files.folders") }}</h2>
|
<h2>{{ $t("files.folders") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
</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
|
<item
|
||||||
v-for="item in dirs"
|
v-for="item in dirs"
|
||||||
:key="base64(item.name)"
|
:key="base64(item.name)"
|
||||||
|
@ -154,7 +158,6 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
|
@ -172,12 +175,22 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
sortField: "name",
|
sortField: "name",
|
||||||
columnWidth: 280,
|
columnWidth: 250 + state.user.gallerySize * 50,
|
||||||
dragCounter: 0,
|
dragCounter: 0,
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
gallerySize() {
|
||||||
|
this.columnWidth = 250 + state.user.gallerySize * 50; // Update columnWidth based on new gallery size\
|
||||||
|
this.colunmsResize();
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
// Create a computed property that references the Vuex state
|
||||||
|
gallerySize() {
|
||||||
|
return state.user.gallerySize;
|
||||||
|
},
|
||||||
isDarkMode() {
|
isDarkMode() {
|
||||||
return state.user?.darkMode;
|
return state.user?.darkMode;
|
||||||
},
|
},
|
||||||
|
@ -197,30 +210,13 @@ export default {
|
||||||
return state.req.sorting.asc;
|
return state.req.sorting.asc;
|
||||||
},
|
},
|
||||||
items() {
|
items() {
|
||||||
if (state.user == null) {
|
return getters.reqItems();
|
||||||
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() {
|
numDirs() {
|
||||||
return state.req.numDirs;
|
return getters.reqNumDirs();
|
||||||
},
|
},
|
||||||
numFiles() {
|
numFiles() {
|
||||||
return state.req.numFiles;
|
return getters.reqNumFiles();
|
||||||
},
|
},
|
||||||
dirs() {
|
dirs() {
|
||||||
return this.items.dirs;
|
return this.items.dirs;
|
||||||
|
@ -259,7 +255,8 @@ export default {
|
||||||
return icons[state.user.viewMode];
|
return icons[state.user.viewMode];
|
||||||
},
|
},
|
||||||
listingViewMode() {
|
listingViewMode() {
|
||||||
return state.user?.viewMode;
|
this.colunmsResize();
|
||||||
|
return state.user.viewMode;
|
||||||
},
|
},
|
||||||
|
|
||||||
selectedCount() {
|
selectedCount() {
|
||||||
|
@ -269,7 +266,7 @@ export default {
|
||||||
return state.req;
|
return state.req;
|
||||||
},
|
},
|
||||||
loading() {
|
loading() {
|
||||||
return state.loading;
|
return getters.isLoading();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -396,12 +393,12 @@ export default {
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
mutations.setLoading("listing", true);
|
||||||
let action = (overwrite, rename) => {
|
let action = (overwrite, rename) => {
|
||||||
api
|
api
|
||||||
.copy(items, overwrite, rename)
|
.copy(items, overwrite, rename)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutations.setLoading(true);
|
mutations.setLoading("listing", false);
|
||||||
})
|
})
|
||||||
.catch(showError);
|
.catch(showError);
|
||||||
};
|
};
|
||||||
|
@ -412,7 +409,7 @@ export default {
|
||||||
.move(items, overwrite, rename)
|
.move(items, overwrite, rename)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.clipboard = {};
|
this.clipboard = {};
|
||||||
mutations.setLoading(true);
|
mutations.setLoading("listing", false);
|
||||||
})
|
})
|
||||||
.catch(showError);
|
.catch(showError);
|
||||||
};
|
};
|
||||||
|
@ -449,6 +446,11 @@ export default {
|
||||||
let items = css(["#listingView .item", "#listingView .item"]);
|
let items = css(["#listingView .item", "#listingView .item"]);
|
||||||
if (columns === 0) columns = 1;
|
if (columns === 0) columns = 1;
|
||||||
items.style.width = `calc(${100 / columns}% - 1em)`;
|
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() {
|
dragEnter() {
|
||||||
this.dragCounter++;
|
this.dragCounter++;
|
||||||
|
|
|
@ -188,10 +188,12 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
prev() {
|
prev() {
|
||||||
|
mutations.setLoading("preview-img", true);
|
||||||
this.hoverNav = false;
|
this.hoverNav = false;
|
||||||
this.$router.replace({ path: this.previousLink });
|
this.$router.replace({ path: this.previousLink });
|
||||||
},
|
},
|
||||||
next() {
|
next() {
|
||||||
|
mutations.setLoading("preview-img", true);
|
||||||
this.hoverNav = false;
|
this.hoverNav = false;
|
||||||
this.$router.replace({ path: this.nextLink });
|
this.$router.replace({ path: this.nextLink });
|
||||||
},
|
},
|
||||||
|
@ -231,23 +233,28 @@ export default {
|
||||||
|
|
||||||
this.previousLink = "";
|
this.previousLink = "";
|
||||||
this.nextLink = "";
|
this.nextLink = "";
|
||||||
|
const path = state.req.path;
|
||||||
|
const directoryPath = path.substring(0, path.lastIndexOf("/"));
|
||||||
for (let i = 0; i < this.listing.length; i++) {
|
for (let i = 0; i < this.listing.length; i++) {
|
||||||
if (this.listing[i].name !== this.name) {
|
if (this.listing[i].name !== this.name) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let j = i - 1; j >= 0; j--) {
|
for (let j = i - 1; j >= 0; j--) {
|
||||||
if (mediaTypes.includes(this.listing[j].type)) {
|
let composedListing = this.listing[j];
|
||||||
this.previousLink = this.listing[j].url;
|
composedListing.path = directoryPath + "/" + composedListing.name;
|
||||||
this.previousRaw = this.prefetchUrl(this.listing[j]);
|
if (mediaTypes.includes(composedListing.type)) {
|
||||||
|
this.previousLink = composedListing.url;
|
||||||
|
this.previousRaw = this.prefetchUrl(composedListing);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let j = i + 1; j < this.listing.length; j++) {
|
for (let j = i + 1; j < this.listing.length; j++) {
|
||||||
if (mediaTypes.includes(this.listing[j].type)) {
|
let composedListing = this.listing[j];
|
||||||
this.nextLink = this.listing[j].url;
|
composedListing.path = directoryPath + "/" + composedListing.name;
|
||||||
this.nextRaw = this.prefetchUrl(this.listing[j]);
|
if (mediaTypes.includes(composedListing.type)) {
|
||||||
|
this.nextLink = composedListing.url;
|
||||||
|
this.nextRaw = this.prefetchUrl(composedListing);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -259,7 +266,6 @@ export default {
|
||||||
if (item.type !== "image") {
|
if (item.type !== "image") {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.fullSize
|
return this.fullSize
|
||||||
? api.getDownloadURL(item, true)
|
? api.getDownloadURL(item, true)
|
||||||
: api.getPreviewURL(item, "big");
|
: api.getPreviewURL(item, "big");
|
||||||
|
@ -284,7 +290,7 @@ export default {
|
||||||
this.showNav = false || this.hoverNav;
|
this.showNav = false || this.hoverNav;
|
||||||
this.navTimeout = null;
|
this.navTimeout = null;
|
||||||
}, 1500);
|
}, 1500);
|
||||||
}, 500),
|
}, 100),
|
||||||
close() {
|
close() {
|
||||||
mutations.replaceRequest({}); // Reset request data
|
mutations.replaceRequest({}); // Reset request data
|
||||||
let uri = url.removeLastDir(state.route.path) + "/";
|
let uri = url.removeLastDir(state.route.path) + "/";
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<errors v-if="error" :errorCode="error.status" />
|
<errors v-if="error" :errorCode="error.status" />
|
||||||
<div class="row" v-else-if="!loading">
|
|
||||||
<div class="column">
|
|
||||||
<form class="card" @submit.prevent="save">
|
<form class="card" @submit.prevent="save">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2>{{ $t("settings.globalSettings") }}</h2>
|
<h2>{{ $t("settings.globalSettings") }}</h2>
|
||||||
|
@ -9,12 +7,12 @@
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p>
|
<p>
|
||||||
<input type="checkbox" v-model="settings.signup" />
|
<input type="checkbox" v-model="selectedSettings.signup" />
|
||||||
{{ $t("settings.allowSignup") }}
|
{{ $t("settings.allowSignup") }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<input type="checkbox" v-model="settings.createUserDir" />
|
<input type="checkbox" v-model="selectedSettings.createUserDir" />
|
||||||
{{ $t("settings.createUserDir") }}
|
{{ $t("settings.createUserDir") }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -23,24 +21,13 @@
|
||||||
<input
|
<input
|
||||||
class="input input--block"
|
class="input input--block"
|
||||||
type="text"
|
type="text"
|
||||||
v-model="settings.userHomeBasePath"
|
v-model="selectedSettings.userHomeBasePath"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>{{ $t("settings.rules") }}</h3>
|
<h3>{{ $t("settings.rules") }}</h3>
|
||||||
<p class="small">{{ $t("settings.globalRules") }}</p>
|
<p class="small">{{ $t("settings.globalRules") }}</p>
|
||||||
<rules :rules="settings.rules" @update:rules="updateRules" />
|
<rules :rules="selectedSettings.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>
|
<h3>{{ $t("settings.branding") }}</h3>
|
||||||
|
|
||||||
|
@ -56,7 +43,7 @@
|
||||||
<p>
|
<p>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="settings.frontend.disableExternal"
|
v-model="selectedSettings.frontend.disableExternal"
|
||||||
id="branding-links"
|
id="branding-links"
|
||||||
/>
|
/>
|
||||||
{{ $t("settings.disableExternalLinks") }}
|
{{ $t("settings.disableExternalLinks") }}
|
||||||
|
@ -65,7 +52,7 @@
|
||||||
<p>
|
<p>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="settings.frontend.disableUsedPercentage"
|
v-model="selectedSettings.frontend.disableUsedPercentage"
|
||||||
id="branding-links"
|
id="branding-links"
|
||||||
/>
|
/>
|
||||||
{{ $t("settings.disableUsedDiskPercentage") }}
|
{{ $t("settings.disableUsedDiskPercentage") }}
|
||||||
|
@ -76,7 +63,7 @@
|
||||||
<input
|
<input
|
||||||
class="input input--block"
|
class="input input--block"
|
||||||
type="text"
|
type="text"
|
||||||
v-model="settings.frontend.name"
|
v-model="selectedSettings.frontend.name"
|
||||||
id="branding-name"
|
id="branding-name"
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
@ -86,111 +73,29 @@
|
||||||
<input
|
<input
|
||||||
class="input input--block"
|
class="input input--block"
|
||||||
type="text"
|
type="text"
|
||||||
v-model="settings.frontend.files"
|
v-model="selectedSettings.frontend.files"
|
||||||
id="branding-files"
|
id="branding-files"
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<input
|
<input class="button button--flat" type="submit" :value="$t('buttons.update')" />
|
||||||
class="button button--flat"
|
|
||||||
type="submit"
|
|
||||||
:value="$t('buttons.update')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="column">
|
|
||||||
<form class="card" @submit.prevent="save">
|
|
||||||
<div class="card-title">
|
|
||||||
<h2>{{ $t("settings.userDefaults") }}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-content">
|
|
||||||
<p class="small">{{ $t("settings.defaultUserDescription") }}</p>
|
|
||||||
|
|
||||||
<user-form
|
|
||||||
:isNew="false"
|
|
||||||
:isDefault="true"
|
|
||||||
:user="settings.defaults"
|
|
||||||
@update:user="updateUser"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-action">
|
|
||||||
<input
|
|
||||||
class="button button--flat"
|
|
||||||
type="submit"
|
|
||||||
:value="$t('buttons.update')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { showSuccess } from "@/notify";
|
import { showSuccess, showError } from "@/notify";
|
||||||
import { state, mutations } from "@/store";
|
import { state, mutations, getters } from "@/store";
|
||||||
import { settings as api } from "@/api";
|
import { settings as api } from "@/api";
|
||||||
import { enableExec } from "@/utils/constants";
|
import { enableExec } from "@/utils/constants";
|
||||||
import UserForm from "@/components/settings/UserForm.vue";
|
|
||||||
import Rules from "@/components/settings/Rules.vue";
|
import Rules from "@/components/settings/Rules.vue";
|
||||||
import Errors from "@/views/Errors.vue";
|
import Errors from "@/views/Errors.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "settings",
|
name: "settings",
|
||||||
components: {
|
components: {
|
||||||
UserForm,
|
|
||||||
Rules,
|
Rules,
|
||||||
Errors,
|
Errors,
|
||||||
},
|
},
|
||||||
|
@ -198,12 +103,12 @@ export default {
|
||||||
return {
|
return {
|
||||||
error: null,
|
error: null,
|
||||||
originalSettings: null,
|
originalSettings: null,
|
||||||
settings: null,
|
selectedSettings: state.settings,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
loading() {
|
loading() {
|
||||||
return state.loading;
|
return getters.isLoading();
|
||||||
},
|
},
|
||||||
user() {
|
user() {
|
||||||
return state.user;
|
return state.user;
|
||||||
|
@ -211,32 +116,14 @@ export default {
|
||||||
isExecEnabled: () => enableExec,
|
isExecEnabled: () => enableExec,
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
try {
|
mutations.setLoading("settings", true);
|
||||||
mutations.setLoading(true);
|
|
||||||
const original = await api.get();
|
const original = await api.get();
|
||||||
let settings = { ...original, commands: [] };
|
mutations.setSettings(original);
|
||||||
|
mutations.setLoading("settings", false);
|
||||||
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: {
|
methods: {
|
||||||
updateRules(updatedRules) {
|
updateRules(updatedRules) {
|
||||||
this.settings.rules = updatedRules;
|
this.selectedSettings = { ...this.selectedSettings, rules: updatedRules };
|
||||||
},
|
|
||||||
updateUser(updatedUser) {
|
|
||||||
this.settings.defaults = updatedUser;
|
|
||||||
},
|
},
|
||||||
capitalize(name, where = "_") {
|
capitalize(name, where = "_") {
|
||||||
if (where === "caps") where = /(?=[A-Z])/;
|
if (where === "caps") where = /(?=[A-Z])/;
|
||||||
|
@ -250,21 +137,9 @@ export default {
|
||||||
return name.slice(0, -1);
|
return name.slice(0, -1);
|
||||||
},
|
},
|
||||||
async save() {
|
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 {
|
try {
|
||||||
await api.update(settings);
|
mutations.setSettings(this.selectedSettings);
|
||||||
|
await api.update(state.settings);
|
||||||
showSuccess(this.$t("settings.settingsUpdated"));
|
showSuccess(this.$t("settings.settingsUpdated"));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(e);
|
showError(e);
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="row">
|
<div class="card" id="profile-main" :class="{ active: active }">
|
||||||
<div class="column">
|
|
||||||
<form class="card" @submit="updateSettings">
|
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2>{{ $t("settings.profileSettings") }}</h2>
|
<h2>{{ $t("settings.profileSettings") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<form @submit="updateSettings">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p>
|
<p>
|
||||||
<input type="checkbox" v-model="darkMode" />
|
<input type="checkbox" v-model="darkMode" />
|
||||||
|
@ -29,6 +28,23 @@
|
||||||
:viewMode="viewMode"
|
:viewMode="viewMode"
|
||||||
@update:viewMode="updateViewMode"
|
@update:viewMode="updateViewMode"
|
||||||
></ViewMode>
|
></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>
|
<h3>{{ $t("settings.language") }}</h3>
|
||||||
<Languages
|
<Languages
|
||||||
class="input input--block"
|
class="input input--block"
|
||||||
|
@ -45,10 +61,8 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
<hr />
|
||||||
|
<form v-if="!user.lockPassword" @submit="updatePassword">
|
||||||
<div class="column">
|
|
||||||
<form class="card" v-if="!user.lockPassword" @submit="updatePassword">
|
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2>{{ $t("settings.changePassword") }}</h2>
|
<h2>{{ $t("settings.changePassword") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -106,9 +120,16 @@ export default {
|
||||||
darkMode: false,
|
darkMode: false,
|
||||||
viewMode: "list",
|
viewMode: "list",
|
||||||
locale: "",
|
locale: "",
|
||||||
|
gallerySize: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return state.settings;
|
||||||
|
},
|
||||||
|
active() {
|
||||||
|
return state.activeSettingsView === "profile-main";
|
||||||
|
},
|
||||||
user() {
|
user() {
|
||||||
return state.user;
|
return state.user;
|
||||||
},
|
},
|
||||||
|
@ -127,22 +148,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
mutations.setLoading(false);
|
|
||||||
this.darkMode = state.user.darkMode;
|
this.darkMode = state.user.darkMode;
|
||||||
this.locale = state.user.locale;
|
this.locale = state.user.locale;
|
||||||
this.viewMode = state.user.viewMode;
|
this.viewMode = state.user.viewMode;
|
||||||
this.hideDotfiles = state.user.hideDotfiles;
|
this.hideDotfiles = state.user.hideDotfiles;
|
||||||
this.singleClick = state.user.singleClick;
|
this.singleClick = state.user.singleClick;
|
||||||
this.dateFormat = state.user.dateFormat;
|
this.dateFormat = state.user.dateFormat;
|
||||||
|
this.gallerySize = state.user.gallerySize;
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
user() {
|
gallerySize(newValue) {
|
||||||
this.darkMode = state.user.darkMode;
|
this.gallerySize = parseInt(newValue, 0); // Update the user object
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -174,6 +190,7 @@ export default {
|
||||||
hideDotfiles: this.hideDotfiles,
|
hideDotfiles: this.hideDotfiles,
|
||||||
singleClick: this.singleClick,
|
singleClick: this.singleClick,
|
||||||
dateFormat: this.dateFormat,
|
dateFormat: this.dateFormat,
|
||||||
|
gallerySize: this.gallerySize,
|
||||||
};
|
};
|
||||||
const shouldReload =
|
const shouldReload =
|
||||||
rtlLanguages.includes(data.locale) !== rtlLanguages.includes(i18n.locale);
|
rtlLanguages.includes(data.locale) !== rtlLanguages.includes(i18n.locale);
|
||||||
|
@ -184,6 +201,7 @@ export default {
|
||||||
"hideDotfiles",
|
"hideDotfiles",
|
||||||
"singleClick",
|
"singleClick",
|
||||||
"dateFormat",
|
"dateFormat",
|
||||||
|
"gallerySize",
|
||||||
]);
|
]);
|
||||||
mutations.updateUser(data);
|
mutations.updateUser(data);
|
||||||
if (shouldReload) {
|
if (shouldReload) {
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<errors v-if="error" :errorCode="error.status" />
|
<errors v-if="error" :errorCode="error.status" />
|
||||||
<div class="row" v-else-if="!loading">
|
<div class="card" id="shares-main" :class="{ active: active }">
|
||||||
<div class="column">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2>{{ $t("settings.shareManagement") }}</h2>
|
<h2>{{ $t("settings.shareManagement") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,14 +52,12 @@
|
||||||
<span>{{ $t("files.lonely") }}</span>
|
<span>{{ $t("files.lonely") }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { showSuccess, showError } from "@/notify";
|
import { showSuccess, showError } from "@/notify";
|
||||||
import { share as api, users } from "@/api";
|
import { share as api, users } from "@/api";
|
||||||
import { state, mutations } from "@/store";
|
import { state, mutations, getters } from "@/store";
|
||||||
import { fromNow } from "@/utils/moment";
|
import { fromNow } from "@/utils/moment";
|
||||||
import Clipboard from "clipboard";
|
import Clipboard from "clipboard";
|
||||||
import Errors from "@/views/Errors.vue";
|
import Errors from "@/views/Errors.vue";
|
||||||
|
@ -79,7 +75,7 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
mutations.setLoading(true);
|
mutations.setLoading("shares", true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let links = await api.list();
|
let links = await api.list();
|
||||||
|
@ -93,7 +89,7 @@ export default {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.error = e;
|
this.error = e;
|
||||||
} finally {
|
} finally {
|
||||||
mutations.setLoading(false);
|
mutations.setLoading("shares", false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -106,11 +102,17 @@ export default {
|
||||||
this.clip.destroy();
|
this.clip.destroy();
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return state.settings;
|
||||||
|
},
|
||||||
|
active() {
|
||||||
|
return state.activeSettingsView === "shares-main";
|
||||||
|
},
|
||||||
user() {
|
user() {
|
||||||
return state.user;
|
return state.user;
|
||||||
},
|
},
|
||||||
loading() {
|
loading() {
|
||||||
return state.loading;
|
return getters.isLoading();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<errors v-if="error" :errorCode="error.status" />
|
<errors v-if="error" :errorCode="error.status" />
|
||||||
<div class="row" v-else-if="!loading">
|
<form @submit="save" id="user-main" class="card">
|
||||||
<div class="column">
|
|
||||||
<form @submit="save" class="card">
|
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2 v-if="user.id === 0">{{ $t("settings.newUser") }}</h2>
|
<h2 v-if="user.id === 0">{{ $t("settings.newUser") }}</h2>
|
||||||
<h2 v-else>{{ $t("settings.user") }} {{ user.username }}</h2>
|
<h2 v-else>{{ $t("settings.user") }} {{ user.username }}</h2>
|
||||||
|
@ -33,11 +31,9 @@
|
||||||
<input class="button button--flat" type="submit" :value="$t('buttons.save')" />
|
<input class="button button--flat" type="submit" :value="$t('buttons.save')" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mutations,state } from "@/store";
|
import { mutations, state } from "@/store";
|
||||||
import { users as api, settings } from "@/api";
|
import { users as api, settings } from "@/api";
|
||||||
import UserForm from "@/components/settings/UserForm.vue";
|
import UserForm from "@/components/settings/UserForm.vue";
|
||||||
import Errors from "@/views/Errors.vue";
|
import Errors from "@/views/Errors.vue";
|
||||||
|
@ -53,7 +49,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
error: null,
|
error: null,
|
||||||
originalUser: null,
|
originalUser: null,
|
||||||
user: { perm: {admin: false} },
|
user: { perm: { admin: false } },
|
||||||
showDelete: false,
|
showDelete: false,
|
||||||
createUserDir: false,
|
createUserDir: false,
|
||||||
loading: false, // Replaces Vuex state `loading`
|
loading: false, // Replaces Vuex state `loading`
|
||||||
|
@ -65,6 +61,9 @@ export default {
|
||||||
this.fetchData();
|
this.fetchData();
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return state.settings;
|
||||||
|
},
|
||||||
isNew() {
|
isNew() {
|
||||||
return state.route.path === "/settings/users/new";
|
return state.route.path === "/settings/users/new";
|
||||||
},
|
},
|
||||||
|
@ -74,7 +73,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async fetchData() {
|
async fetchData() {
|
||||||
mutations.setLoading(true);
|
mutations.setLoading("users", true);
|
||||||
try {
|
try {
|
||||||
if (this.isNew) {
|
if (this.isNew) {
|
||||||
let { defaults, createUserDir } = await settings.get();
|
let { defaults, createUserDir } = await settings.get();
|
||||||
|
@ -92,10 +91,10 @@ export default {
|
||||||
this.user = { ...(await api.get(id)) };
|
this.user = { ...(await api.get(id)) };
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(e)
|
showError(e);
|
||||||
this.error = e;
|
this.error = e;
|
||||||
} finally {
|
} finally {
|
||||||
mutations.setLoading(false);
|
mutations.setLoading("users", false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deletePrompt() {
|
deletePrompt() {
|
||||||
|
@ -116,6 +115,7 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
await api.update(user);
|
await api.update(user);
|
||||||
if (user.id === state.user.id) {
|
if (user.id === state.user.id) {
|
||||||
|
consoel.log("set user");
|
||||||
mutations.setUser(user);
|
mutations.setUser(user);
|
||||||
}
|
}
|
||||||
showSuccess(this.$t("settings.userUpdated"));
|
showSuccess(this.$t("settings.userUpdated"));
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
<template>
|
||||||
|
<errors v-if="error" :errorCode="error.status" />
|
||||||
|
<div v-if="isExecEnabled" class="card" id="userColumn-main">
|
||||||
|
<form @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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { showSuccess } from "@/notify";
|
||||||
|
import { state, getters } 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,
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
error: null,
|
||||||
|
originalSettings: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return state.settings;
|
||||||
|
},
|
||||||
|
loading() {
|
||||||
|
return getters.isLoading();
|
||||||
|
},
|
||||||
|
user() {
|
||||||
|
return state.user;
|
||||||
|
},
|
||||||
|
isExecEnabled: () => enableExec,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateRules(updatedRules) {
|
||||||
|
this.settings.rules = updatedRules;
|
||||||
|
},
|
||||||
|
capitalize(name, where = "_") {
|
||||||
|
if (where === "caps") where = /(?=[A-Z])/;
|
||||||
|
let splitted = name.split(where);
|
||||||
|
name = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < splitted.length; i++) {
|
||||||
|
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
return name.slice(0, -1);
|
||||||
|
},
|
||||||
|
async save() {
|
||||||
|
try {
|
||||||
|
await api.update(state.settings);
|
||||||
|
showSuccess(this.$t("settings.settingsUpdated"));
|
||||||
|
} catch (e) {
|
||||||
|
showError(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,87 @@
|
||||||
|
<template>
|
||||||
|
<errors v-if="error" :errorCode="error.status" />
|
||||||
|
<div class="card" id="user-defaults-main">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ $t("settings.userDefaults") }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<p class="small">{{ $t("settings.defaultUserDescription") }}</p>
|
||||||
|
|
||||||
|
<user-form
|
||||||
|
:isNew="false"
|
||||||
|
:isDefault="true"
|
||||||
|
:user="settings.defaults"
|
||||||
|
@update:user="updateUser"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<input class="button button--flat" type="submit" :value="$t('buttons.update')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { showSuccess } from "@/notify";
|
||||||
|
import { state, getters } 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,
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
error: null,
|
||||||
|
originalSettings: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return state.settings;
|
||||||
|
},
|
||||||
|
loading() {
|
||||||
|
return getters.isLoading();
|
||||||
|
},
|
||||||
|
user() {
|
||||||
|
return state.user;
|
||||||
|
},
|
||||||
|
isExecEnabled: () => enableExec,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateRules(updatedRules) {
|
||||||
|
state.settings.rules = updatedRules;
|
||||||
|
},
|
||||||
|
updateUser(updatedUser) {
|
||||||
|
state.settings.defaults = updatedUser;
|
||||||
|
},
|
||||||
|
capitalize(name, where = "_") {
|
||||||
|
if (where === "caps") where = /(?=[A-Z])/;
|
||||||
|
let splitted = name.split(where);
|
||||||
|
name = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < splitted.length; i++) {
|
||||||
|
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
return name.slice(0, -1);
|
||||||
|
},
|
||||||
|
async save() {
|
||||||
|
try {
|
||||||
|
await api.update(state.settings);
|
||||||
|
showSuccess(this.$t("settings.settingsUpdated"));
|
||||||
|
} catch (e) {
|
||||||
|
showError(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -1,8 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<errors v-if="error" :errorCode="error.status" />
|
<errors v-if="error" :errorCode="error.status" />
|
||||||
<div class="row" v-else-if="!loading">
|
<div class="card" id="users-main">
|
||||||
<div class="column">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2>{{ $t("settings.users") }}</h2>
|
<h2>{{ $t("settings.users") }}</h2>
|
||||||
<router-link to="/settings/users/new"
|
<router-link to="/settings/users/new"
|
||||||
|
@ -37,15 +35,13 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { state, mutations } from "@/store";
|
import { state, mutations, getters } from "@/store";
|
||||||
import { getAllUsers } from "@/api/users";
|
import { getAllUsers } from "@/api/users";
|
||||||
import Errors from "@/views/Errors.vue";
|
import Errors from "@/views/Errors.vue";
|
||||||
import { showError } from "@/notify";
|
import { showError } from "@/notify";
|
||||||
|
mutations.setLoading("users", true);
|
||||||
export default {
|
export default {
|
||||||
name: "users",
|
name: "users",
|
||||||
components: {
|
components: {
|
||||||
|
@ -59,7 +55,7 @@ export default {
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
// Set loading state to true
|
// Set loading state to true
|
||||||
mutations.setLoading(true);
|
|
||||||
try {
|
try {
|
||||||
// Fetch all users from the API
|
// Fetch all users from the API
|
||||||
this.users = await getAllUsers();
|
this.users = await getAllUsers();
|
||||||
|
@ -68,14 +64,16 @@ export default {
|
||||||
// Handle errors
|
// Handle errors
|
||||||
this.error = e;
|
this.error = e;
|
||||||
} finally {
|
} finally {
|
||||||
// Set loading state to false
|
mutations.setLoading("users", false);
|
||||||
mutations.setLoading(false);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return state.settings;
|
||||||
|
},
|
||||||
// Access the loading state directly from the store
|
// Access the loading state directly from the store
|
||||||
loading() {
|
loading() {
|
||||||
return state.loading;
|
return getters.isLoading();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { test, expect } from "./fixtures/auth";
|
||||||
|
|
||||||
|
test("redirect to login", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
await expect(page).toHaveURL(/\/login/);
|
||||||
|
|
||||||
|
await page.goto("/files/");
|
||||||
|
await expect(page).toHaveURL(/\/login\?redirect=\/files\//);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login", async ({ authPage, page, context }) => {
|
||||||
|
await authPage.goto();
|
||||||
|
await expect(page).toHaveTitle(/Login - File Browser$/);
|
||||||
|
|
||||||
|
await authPage.loginAs("fake", "fake");
|
||||||
|
await expect(authPage.wrongCredentials).toBeVisible();
|
||||||
|
|
||||||
|
await authPage.loginAs();
|
||||||
|
await expect(authPage.wrongCredentials).toBeHidden();
|
||||||
|
// await page.waitForURL("**/files/", { timeout: 5000 });
|
||||||
|
await expect(page).toHaveTitle(/.*Files - File Browser$/);
|
||||||
|
|
||||||
|
let cookies = await context.cookies();
|
||||||
|
expect(cookies.find((c) => c.name == "auth")?.value).toBeDefined();
|
||||||
|
|
||||||
|
// await authPage.logout();
|
||||||
|
// await page.waitForURL("**/login", { timeout: 5000 });
|
||||||
|
// await expect(page).toHaveTitle(/Login - File Browser$/);
|
||||||
|
// cookies = await context.cookies();
|
||||||
|
// expect(cookies.find((c) => c.name == "auth")?.value).toBeUndefined();
|
||||||
|
});
|
|
@ -0,0 +1,40 @@
|
||||||
|
import {
|
||||||
|
type Page,
|
||||||
|
type Locator,
|
||||||
|
test as base,
|
||||||
|
expect,
|
||||||
|
} from "@playwright/test";
|
||||||
|
|
||||||
|
export class AuthPage {
|
||||||
|
public readonly wrongCredentials: Locator;
|
||||||
|
|
||||||
|
constructor(public readonly page: Page) {
|
||||||
|
this.wrongCredentials = this.page.locator("div.wrong");
|
||||||
|
}
|
||||||
|
|
||||||
|
async goto() {
|
||||||
|
await this.page.goto("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
async loginAs(username = "admin", password = "admin") {
|
||||||
|
await this.page.getByPlaceholder("Username").fill(username);
|
||||||
|
await this.page.getByPlaceholder("Password").fill(password);
|
||||||
|
await this.page.getByRole("button", { name: "Login" }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
//async logout() {
|
||||||
|
// await this.page.getByRole("button", { name: "Logout" }).click();
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
const test = base.extend<{ authPage: AuthPage }>({
|
||||||
|
authPage: async ({ page }, use) => {
|
||||||
|
const authPage = new AuthPage(page);
|
||||||
|
await authPage.goto();
|
||||||
|
await authPage.loginAs();
|
||||||
|
await use(authPage);
|
||||||
|
// await authPage.logout();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { test, expect };
|
20
makefile
20
makefile
|
@ -1,22 +1,30 @@
|
||||||
setup:
|
setup:
|
||||||
cd frontend && npm i
|
cd frontend && npm i && npx playwright install
|
||||||
if [ ! -f backend/test__config.yaml ]; then \
|
if [ ! -f backend/test__config.yaml ]; then \
|
||||||
cp backend/filebrowser.yaml backend/test_config.yaml; \
|
cp backend/filebrowser.yaml backend/test_config.yaml; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
build:
|
build:
|
||||||
docker build -t gtstef/filebrowser .
|
docker build --build-arg="VERSION=testing" --build-arg="REVISION=n/a" -t gtstef/filebrowser .
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
# Kill processes matching exe/filebrowser, ignore errors if process does not exist
|
# Kill processes matching exe/filebrowser, ignore errors if process does not exist
|
||||||
-pkill -f "exe/filebrowser" || true
|
-pkill -f "exe/filebrowser" || true
|
||||||
# Start backend and frontend concurrently
|
# Start backend and frontend concurrently
|
||||||
cd backend && FILEBROWSER_NO_EMBEDED=true go run . -c test_config.yaml & BACKEND_PID=$$!; \
|
cd backend && FILEBROWSER_NO_EMBEDED=true go run \
|
||||||
|
--ldflags="-w -s -X 'github.com/gtsteffaniak/filebrowser/version.CommitSHA=testingCommit' -X 'github.com/gtsteffaniak/filebrowser/version.Version=testing'" \
|
||||||
|
. -c test_config.yaml & BACKEND_PID=$$!; \
|
||||||
cd frontend && npm run watch & FRONTEND_PID=$$!; \
|
cd frontend && npm run watch & FRONTEND_PID=$$!; \
|
||||||
wait $$BACKEND_PID $$FRONTEND_PID
|
wait $$BACKEND_PID $$FRONTEND_PID
|
||||||
|
|
||||||
make lint-frontend:
|
lint-frontend:
|
||||||
cd frontend && npm run lint
|
cd frontend && npm run lint
|
||||||
|
|
||||||
make lint-backend:
|
lint-backend:
|
||||||
cd backend && golangci-lint run
|
cd backend && golangci-lint run --path-prefix=backend
|
||||||
|
|
||||||
|
test-backend:
|
||||||
|
cd backend && go test -race ./...
|
||||||
|
|
||||||
|
test-frontend:
|
||||||
|
docker build -t gtstef/filebrowser-tests -f Dockerfile.playwright .
|
||||||
|
|
10
roadmap.md
10
roadmap.md
|
@ -1,11 +1,17 @@
|
||||||
# Planned Roadmap
|
# Planned Roadmap
|
||||||
|
|
||||||
Next version :
|
next 0.2.x release:
|
||||||
|
|
||||||
- Theme configuration from settings
|
- Theme configuration from settings
|
||||||
|
- Better media and file viewer support
|
||||||
|
|
||||||
|
|
||||||
|
initial 0.3.0 release :
|
||||||
|
|
||||||
|
- drop in replace backend db with pocketbas
|
||||||
- Add Job status to the sidebar
|
- Add Job status to the sidebar
|
||||||
- index status.
|
- index status.
|
||||||
- new jobs as they come via pocketbase
|
- Job status from users
|
||||||
|
|
||||||
Future releases:
|
Future releases:
|
||||||
- Replace http routes for gorilla/mux with pocketbase
|
- Replace http routes for gorilla/mux with pocketbase
|
||||||
|
|
Loading…
Reference in New Issue