v0.2.8 release (#193)

This commit is contained in:
Graham Steffaniak 2024-08-24 17:02:33 -05:00 committed by GitHub
parent 5ebaf2a45b
commit 9e9109984d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 2505 additions and 1685 deletions

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

50
.github/workflows/release_dev.yaml vendored Normal file
View File

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

View File

@ -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

2
.gitignore vendored
View File

@ -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

View File

@ -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

View File

@ -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/

19
Dockerfile.playwright Normal file
View File

@ -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

View File

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

232
backend/.golangci.yml Normal file
View File

@ -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

View File

@ -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:

View File

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

View File

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

View File

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

View File

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

View File

@ -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
} }
return i.Path func GetRealPath(relativePath ...string) (string, error) {
combined := []string{settings.Config.Server.Root}
for _, path := range relativePath {
combined = append(combined, strings.TrimPrefix(path, settings.Config.Server.Root))
}
joinedPath := filepath.Join(combined...)
// Convert relative path to absolute path
absolutePath, err := filepath.Abs(joinedPath)
if err != nil {
return "", err
}
if !Exists(absolutePath) {
return absolutePath, nil // return without error
}
// Resolve symlinks and get the real path
return resolveSymlinks(absolutePath)
}
func DeleteFiles(absPath string, opts FileOptions) error {
err := os.RemoveAll(absPath)
if err != nil {
return err
}
parentDir := filepath.Dir(absPath)
opts.Path = parentDir
updated := RefreshFileInfo(opts)
if !updated {
return errors.ErrEmptyKey
}
return nil
}
func WriteDirectory(opts FileOptions) error {
// Ensure the parent directories exist
err := os.MkdirAll(opts.Path, 0775)
if err != nil {
return err
}
opts.Path = filepath.Dir(opts.Path)
updated := RefreshFileInfo(opts)
if !updated {
return errors.ErrEmptyKey
}
return nil
}
func WriteFile(opts FileOptions, in io.Reader) error {
fmt.Println("writing file", opts.Path)
dst := opts.Path
parentDir := filepath.Dir(dst)
// Split the directory from the destination path
dir := filepath.Dir(dst)
// Create the directory and all necessary parents
err := os.MkdirAll(dir, 0775)
if err != nil {
return err
}
// Open the file for writing (create if it doesn't exist, truncate if it does)
file, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
if err != nil {
return err
}
defer file.Close()
// Copy the contents from the reader to the file
_, err = io.Copy(file, in)
if err != nil {
return err
}
fmt.Println("refreshing info for ", parentDir)
opts.Path = parentDir
updated := RefreshFileInfo(opts)
if !updated {
return errors.ErrEmptyKey
}
return nil
}
// resolveSymlinks resolves symlinks in the given path
func resolveSymlinks(path string) (string, error) {
for {
// Get the file info
info, err := os.Lstat(path)
if err != nil {
return "", err
}
// Check if it's a symlink
if info.Mode()&os.ModeSymlink != 0 {
// Read the symlink target
target, err := os.Readlink(path)
if err != nil {
return "", err
}
// Resolve the target relative to the symlink's directory
path = filepath.Join(filepath.Dir(path), target)
} else {
// Not a symlink, so we are done
return path, nil
}
}
} }
// addContent reads and sets content based on the file type. // 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
}

View File

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

View File

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

View File

@ -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]

View File

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

View File

@ -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.

View File

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

View File

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

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

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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{}

View File

@ -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 {

View File

@ -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

View File

@ -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{}
} }

View File

@ -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 {

View File

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

View File

@ -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 {

View File

@ -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)"
) )

View File

@ -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`)

View File

@ -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",

View File

@ -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"] },
},
],
});

View File

@ -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();
} }

View File

@ -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 });
}, },
}, },
}; };

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">&mdash;</p> <p v-if="isDir" class="size" data-order="-1">&mdash;</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>

View File

@ -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");

View File

@ -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);
}, },
}, },
}; };

View File

@ -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

View File

@ -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>

View File

@ -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;
}, },
}, },
}; };

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View File

@ -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;
} }

View File

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

View File

@ -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;

View File

@ -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,
},
},
],
}, },
], ],
}, },

View File

@ -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

View File

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

View File

@ -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"
}

View File

@ -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
}; };

View File

@ -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) {

View File

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

View File

@ -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>

View File

@ -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!

View File

@ -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)));

View File

@ -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() {

View File

@ -84,7 +84,11 @@
<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++;

View File

@ -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) + "/";

View File

@ -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);

View File

@ -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) {

View File

@ -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: {

View File

@ -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,8 +31,6 @@
<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";
@ -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"));

View File

@ -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>

View File

@ -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>

View File

@ -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();
}, },
}, },
}; };

View File

@ -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();
});

40
frontend/tests/fixtures/auth.ts vendored Normal file
View File

@ -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 };

View File

@ -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 .

View File

@ -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