Compare commits

...
48 Commits
Author SHA1 Message Date
Graham Steffaniak 415158d730 Adjusted path tweak (#212) 2024-09-17 12:14:48 -05:00
Graham Steffaniak cd192744c2 Merge branch 'main' into v0.2.9 2024-09-16 20:27:36 -05:00
Graham Steffaniak 906ac19be1 Hotfix v0.2.9 (#210) 2024-09-16 20:25:44 -05:00
Graham Steffaniak c272eb1e61 hotfix: search path adjustment 2024-09-16 20:19:25 -05:00
Graham Steffaniak bae39c8586 Update main.yaml workflow
add fetch depth
2024-09-16 17:26:57 -05:00
Graham Steffaniak 62d1cd88a1 V0.2.9 release (#205) 2024-09-16 16:01:16 -05:00
Graham Steffaniak 8767a43ee8 mention a new fix 2024-09-16 14:59:49 -05:00
Graham Steffaniak c1e846067e updated more quantum 2024-09-16 14:55:44 -05:00
Graham Steffaniak 9bdc332ffd adjust settings menu (#204) 2024-09-16 14:52:51 -05:00
Graham Steffaniak bfa3b6a7d8 Add context menu (#203) 2024-09-07 13:03:51 -05:00
Graham Steffaniak 66d09a4b75 echo workflow 2024-09-06 21:12:20 -05:00
Graham Steffaniak 3cb9308254 updated 2024-09-06 21:08:29 -05:00
Graham Steffaniak b01d663bbe updated 2024-09-06 21:01:04 -05:00
Graham Steffaniak 5998f82e54 workflow 2024-09-06 20:56:41 -05:00
Graham Steffaniak df7624fcfc updated again 2024-09-06 20:54:02 -05:00
Graham Steffaniak 8a72da8b30 updated approach 2024-09-06 20:48:56 -05:00
Graham Steffaniak 125c0d6108 Merge branch 'dev_v0.2.9' of github.com:gtsteffaniak/filebrowser into dev_v0.2.9 2024-09-06 20:45:09 -05:00
Graham Steffaniak f7f08bba2a updated release 2024-09-06 20:44:47 -05:00
Graham Steffaniak b49e5dbc81 updated workflow (#202) 2024-09-06 20:42:59 -05:00
Graham Steffaniak abd5464b80 Metadata changes (#200) 2024-09-02 19:17:15 -05:00
Graham Steffaniak 97dc393c9b Update share views (#199) 2024-09-02 18:42:17 -05:00
Graham Steffaniak a3f684ff82 Update CHANGELOG.md 2024-08-31 19:48:20 -05:00
Graham Steffaniak 876aabfcec Update CHANGELOG.md 2024-08-31 19:47:50 -05:00
Graham Steffaniak e22abd3a4b Update CHANGELOG.md 2024-08-31 19:47:12 -05:00
Graham Steffaniak 01e82e282c go mod tidy 2024-08-27 16:19:52 -05:00
Graham Steffaniak 58a4e07bf4 updated dependencies 2024-08-27 16:18:47 -05:00
Graham Steffaniak 7c2ebb7801 Fix drag to upload (#198) 2024-08-27 14:48:03 -05:00
Graham Steffaniak ab0fdc0e17 updated logic (#197) 2024-08-27 09:22:39 -05:00
Graham Steffaniak c2a627d5c1 updated with arrow keys logic 2024-08-25 13:19:24 -05:00
Graham Steffaniak 0bad14b51e workflow 2024-08-24 19:20:54 -05:00
Graham Steffaniak 74ccfab070 Update search behavior (#195) 2024-08-24 19:12:47 -05:00
Graham Steffaniak 5f185597c9 fix main workflow 2024-08-24 17:06:55 -05:00
Graham Steffaniak 9e9109984d v0.2.8 release (#193) 2024-08-24 17:02:33 -05:00
Graham Steffaniak 5ebaf2a45b State fix (#183) 2024-08-04 14:02:09 -05:00
Graham Steffaniak c7cce0694b workflow update and go releaser (#177) 2024-08-04 12:50:35 -05:00
Graham Steffaniak 43b45203d0 workflow update (#169) 2024-08-04 10:28:38 -05:00
Graham Steffaniak af407068b6 Fix workflows (#165) 2024-08-04 09:56:18 -05:00
Graham Steffaniak 865fb4e243 ismobile fix (#163) 2024-08-03 15:43:08 -05:00
Graham Steffaniak c616b1473a updated ismobile 2024-08-03 15:38:43 -05:00
Graham Steffaniak 0479670c52 fix overlay bug, share bug (#161) 2024-08-03 15:04:50 -05:00
Graham Steffaniak fa3ed6b948 v0.2.7 update 2024-08-03 10:34:12 -05:00
Graham Steffaniak d8085c1f1b updated default config (#152) 2024-07-30 13:13:47 -05:00
Graham Steffaniak dc5da5662f fix default config (#151) 2024-07-30 13:09:34 -05:00
Graham Steffaniak b4bba3391d V0.2.6 (#150) 2024-07-30 12:45:27 -05:00
Graham Steffaniak 62f3953aea V0.2.5 (#125) 2024-06-12 14:51:13 -05:00
dependabot[bot] bc87dcd32c Bump express from 4.18.2 to 4.19.2 in /frontend (#123)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-09 11:07:56 -05:00
dependabot[bot] cdb49680e3 Bump webpack-dev-middleware from 5.3.3 to 5.3.4 in /frontend (#124)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-09 11:07:47 -05:00
Graham Steffaniak d9e583fea7 v0.2.4 release (#116) (#117) 2024-02-09 18:13:02 -06:00
190 changed files with 8120 additions and 15419 deletions

No files matched your search

-5
View File
@@ -1,5 +0,0 @@
# These owners will be the default owners for everything in the repo.
# Unless a later match takes precedence, @o1egl will be requested for
# review when someone opens a pull request.
* @o1egl
-76
View File
@@ -1,76 +0,0 @@
name: dev
on:
push:
branches:
- 'dev_v*'
jobs:
test-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: 1.21.1
- run: cd backend && go test -race -v ./...
lint-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: 1.21.1
- run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2
- run: cd backend && golangci-lint run
format-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: 1.21.1
- run: cd backend && go fmt ./...
lint-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '20'
- run: cd frontend && npm i eslint
- run: cd frontend && npm run lint
push_dev_to_registry:
name: Push dev image
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
# Workaround to fix error:
# failed to push: failed to copy: io: read/write on closed pipe
# See https://github.com/docker/build-push-action/issues/761
with:
driver-opts: |
image=moby/buildkit:v0.10.6
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: gtstef/filebrowser
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+12 -45
View File
@@ -1,63 +1,27 @@
name: main job
name: main release
on:
push:
branches:
- 'main'
- "main"
jobs:
test-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: 1.21.1
- run: cd backend && go test -race -v ./...
lint-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: 1.21.1
- run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2
- run: cd backend && golangci-lint run
format-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: 1.21.1
- run: cd backend && go fmt ./...
lint-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '20'
- run: cd frontend && npm i eslint
- run: cd frontend && npm run lint
push_latest_to_registry:
needs: [lint-frontend, lint-backend, test-backend, format-backend]
name: Push latest
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Find latest tag
run: |
echo "LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV
echo "latest tag is $LATEST_TAG"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
# Workaround to fix error:
# failed to push: failed to copy: io: read/write on closed pipe
# See https://github.com/docker/build-push-action/issues/761
with:
driver-opts: |
image=moby/buildkit:v0.10.6
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
@@ -65,13 +29,16 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
uses: docker/metadata-action@v5
with:
images: gtstef/filebrowser
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
build-args: |
VERSION=${{ env.LATEST_TAG }}
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
file: ./Dockerfile
push: true
-79
View File
@@ -1,79 +0,0 @@
name: pr-merge
on:
pull_request:
branches:
- 'main'
- 'dev_v*'
- 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
test-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: 1.21.1
- run: cd backend && go test -race -v ./...
lint-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: 1.21.1
- run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2
- run: cd backend && golangci-lint run
format-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: 1.21.1
- run: cd backend && go fmt ./...
lint-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '20'
- run: cd frontend && npm i eslint
- run: cd frontend && npm run lint
push_pr_to_registry:
name: Push PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
# Workaround to fix error:
# failed to push: failed to copy: io: read/write on closed pipe
# See https://github.com/docker/build-push-action/issues/761
with:
driver-opts: |
image=moby/buildkit:v0.10.6
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: gtstef/filebrowser
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+56
View File
@@ -0,0 +1,56 @@
name: pr-request
on:
pull_request:
branches:
- "main"
- "v[0-9]+.[0-9]+.[0-9]+"
jobs:
test_frontend:
name: Push release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- name: Build
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile.playwright
push: false
push_pr_to_registry:
name: Push PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: gtstef/filebrowser
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=${{ steps.meta.outputs.version }}
REVISION=${{ steps.meta.outputs.revision }}
+44
View File
@@ -0,0 +1,44 @@
name: dev tests
on:
push:
branches:
- "*"
jobs:
test-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- working-directory: backend
run: go test -race -v ./...
lint-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22.5'
- uses: golangci/golangci-lint-action@v5
with:
version: v1.60
working-directory: backend
format-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- working-directory: backend
run: go fmt ./...
lint-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- working-directory: frontend
run: npm i eslint && npm run lint
+32 -42
View File
@@ -1,47 +1,41 @@
name: release
name: version release
on:
push:
branches:
- 'v[0-9]+.[0-9]+.[0-9]+'
- "v[0-9]+.[0-9]+.[0-9]+"
permissions:
contents: write
jobs:
test-backend:
create_release:
name: Create Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
- name: Checkout code
uses: actions/checkout@v4
with:
go-version: 1.21.1
- run: cd backend && go test -race -v ./...
lint-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
token: ${{ secrets.PAT }}
- name: Extract branch name
shell: bash
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
id: extract_branch
- uses: actions/setup-go@v5
with:
go-version: 1.21.1
- run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2
- run: cd backend && golangci-lint run
format-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
go-version: 1.22.2
- name: Create Release
uses: softprops/action-gh-release@v2
with:
go-version: 1.21.1
- run: cd backend && go fmt ./...
lint-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '20'
- run: cd frontend && npm i eslint
- run: cd frontend && npm run lint
target_commitish: ${{ steps.extract_branch.outputs.branch }}
token: ${{ secrets.PAT }}
tag_name: ${{ steps.extract_branch.outputs.branch }}
prerelease: false
draft: false
generate_release_notes: true
name: ${{ steps.extract_branch.outputs.branch }}
push_release_to_registry:
needs: [lint-frontend, lint-backend, test-backend, format-backend]
name: Push release
runs-on: ubuntu-latest
steps:
@@ -51,21 +45,14 @@ jobs:
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
# Workaround to fix error:
# failed to push: failed to copy: io: read/write on closed pipe
# See https://github.com/docker/build-push-action/issues/761
with:
driver-opts: |
image=moby/buildkit:v0.10.6
- name: Login to Docker Hub
# Only push to Docker Hub when making a release
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
uses: docker/metadata-action@v5
with:
images: gtstef/filebrowser
- name: Strip v from version number
@@ -76,11 +63,14 @@ jobs:
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
build-args: |
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
file: ./Dockerfile
push: true
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
labels: ${{ steps.meta.outputs.labels }}
labels: ${{ steps.meta.outputs.labels }}
+50
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_ENV
- 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: ${{ env.CLEANED_TAG }}
labels: ${{ steps.meta.outputs.labels }}
-24
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
+31
View File
@@ -0,0 +1,31 @@
name: tag
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
update_tag:
name: Update Release tag
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
- name: Install dependencies and build frontend
run: npm i && npm run build
working-directory: frontend
- name: Install UPX
run: sudo apt-get install -y upx
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
args: release --clean
workdir: backend
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+5
View File
@@ -6,8 +6,13 @@ rice-box.go
/filebrowser
/filebrowser.exe
/frontend/dist
/frontend/pkg
/frontend/test-results
/frontend/package-lock.json
/backend/vendor
/backend/*.cov
/backend/test_config.yaml
/backend/srv
.DS_Store
node_modules
+73
View File
@@ -2,6 +2,79 @@
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.9
This release focused on UI navigation experience. Improving keyboard navigation and adds right click context menu.
**New Features**:
- listing view items are middle-clickable on selected listing or when in single-click mode.
- listing view items can be navigated via arrow keys.
- listing view can jump to items using letters and number keys to cycle through files that start with that character.
- You can use the enter key and backspace key to navigate backwards and forwards on selected items.
- ctr-space will open/close the search (leaving ctr-f to browser default find prompt)
- Added right-click context menu to replace the file selection prompt.
**Bugfixes**:
- Fixed drag to upload not working.
- Fixed shared video link issues.
- Fixed user edit bug related to other user.
- Fixed password reset bug.
- Fixed loading state getting stuck.
## v0.2.8
- **Feature**: New gallary view scaling options (closes [#141](https://github.com/gtsteffaniak/filebrowser/issues/141))
- **Change**: Refactored backend files functions
- **Change**: Improved UI response to filesystem changes
- **Change**: Added frontend tests for deployment integrity
- **Fix**: move/replace file prompt issue
- **Fix**: opening files from search
- **Fix**: Display count issue when hideDotFile is enabled.
## v0.2.7
- **Change**: New sidebar style and behavior
- **Change**: make search view and button behavior more consistent.
- **Fix**: [upload file bug](https://github.com/gtsteffaniak/filebrowser/issues/153)
- **Fix**: user lock out bug introduced in 0.2.6
- **Fix**: many minor state related issues.
## v0.2.6
This change focuses on minimizing and simplifying build process.
- **Change**: Migrated to Vite / Vue 3
- **Change**: removed npm modules
- replaced vuex with custom state management via src/store
- replaced noty with simple card popup notifications
- replaced moment with simple date formatter where needed
- replaced vue-simple-progress with vue component
- **Feature**: improved error logging
- backend errors show the root function that called them during the error
- frontend errors print errors to console that fail try/catch
- all frontend errors via popup notification & print to console as well
- **Fix**: Allow editing blank text based files in editor
- tweaked listing styles
- Feature: Allow disabling the index via configuration yaml
## v0.2.5
- Fix: delete user prompt works using native hovers.
## v0.2.4
- Feature: [create-folder-feature](https://github.com/gtsteffaniak/filebrowser/pull/105)
- Feature: [playable shared video](https://github.com/filebrowser/filebrowser/issues/2537)
- Feature: photos, videos, and audio get embedded preview on share instead of icon
- Fix: sharable link bug, now uses special publicUser
- Bump go version to 1.22
- In prep for vue3 migration, npm modules removed:
- js-base64
- pretty-bytes
- whatwg-fetch
- lodash.throttle
- lodash.clonedeep
## v0.2.3
- Feature: token expiration time now configurable
+15 -14
View File
@@ -1,22 +1,23 @@
FROM node:slim as nbuild
FROM golang:1.22-alpine AS base
ARG VERSION
ARG REVISION
WORKDIR /app
COPY ./frontend/package*.json ./
RUN npm ci --maxsockets 1
COPY ./frontend/ ./
RUN npm run build
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 golang:1.21-alpine as base
FROM node:slim AS nbuild
WORKDIR /app
COPY ./backend ./
RUN go get -u golang.org/x/net
RUN go build -ldflags="-w -s" -o filebrowser .
COPY ./frontend/package.json ./
RUN npm i --maxsockets 1
COPY ./frontend/ ./
RUN npm run build-docker
FROM alpine:latest
ARG app="/app/filebrowser"
RUN apk --no-cache add \
ca-certificates \
mailcap
WORKDIR /
ENV FILEBROWSER_NO_EMBEDED="true"
RUN apk --no-cache add ca-certificates mailcap
COPY --from=base /app/filebrowser* ./
COPY --from=nbuild /app/dist/ ./frontend/dist/
ENTRYPOINT [ "./filebrowser" ]
+19
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
+142 -47
View File
@@ -2,56 +2,78 @@
<a href="https://opensource.org/license/apache-2-0/"><img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg" alt="License: Apache-2.0"></a>
</p>
<p align="center">
<img src="frontend/public/img/icons/favicon-256x256.png" width="100" title="Login With Custom URL">
<img src="frontend/public/img/icons/favicon-256x256.png" width="100" title="Login With Custom URL">
</p>
<h3 align="center">Filebrowser - A modern file manager for the web</h3>
<h3 align="center">FileBrowser Quantum - A modern web-based file manager</h3>
<p align="center">
<img width="800" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/899152cf-3e69-4179-aa82-752af2df3fc6" title="Main Screenshot">
<img width="800" src="https://github.com/user-attachments/assets/8ba93582-aba2-4996-8ac3-25f763a2e596" title="Main Screenshot">
</p>
> [!NOTE]
> Only intended to be used with docker.
> [!WARNING]
> Starting with v0.2.0, *ALL* configuration is done via `filebrowser.yaml` configuration file.
> Starting with v0.2.0, *ALL* configuration is done via `filebrowser.yaml`
> Configuration file.
> Starting with v0.2.4 *ALL* share links need to be re-created (due to
> security fix).
This fork makes the following significant changes to filebrowser for origin:
FileBrowser Quantum is a fork of the filebrowser opensource project with the
following changes:
1. [x] Enhanced lightning fast indexed search
- Real-time results as you type
- Works with more type filters
- Enhanced interactive results page.
2. [x] Revamped and simplified GUI navbar and sidebar menu.
- Additional compact view mode as well as refreshed view mode
styles.
3. [x] Revamped and simplified configuration via `filebrowser.yml` config file.
4. [x] Faster listing browsing
- Switching view modes is instant
- Changing Sort order is instant
- The entire directory is loaded in 1/3 the time
1. [x] Better search.
- Lightning fast
- realtime results as you type
- Works with more type filters
- interactive results page.
1. [x] Revamped and simplified GUI navbar and sidebar menu.
1. [x] **IMPORTANT** Revamped configuration via `filebrowser.yml` config file.
1. [x] More configurations possible at a per-user level
- <img width="450" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/625bd7c4-5ee9-4011-aaae-2a388ab0813b">
1. [x] Additional compact view mode as well as refreshed view mode styles.
## About
Filebrowser provides a file managing interface within a specified directory
and it can be used to upload, delete, preview, rename and edit your files.
It allows the creation of multiple users and each user can have its own
directory. It can be used as a standalone app.
FileBrowser Quantum provides a file managing interface within a specified directory
and can be used to upload, delete, preview, rename, and edit your files.
It allows the creation of multiple users and each user can have its
directory.
This repository is a fork, a collection of changes that make this program
work better in terms of asthetics and performance. Improved search,
simplified ui (without removing features) and more secure and up-to-date
build are just a few examples.
This repository is a fork of the original [filebrowser](https://github.com/filebrowser/filebrowser)
with a collection of changes that make this program work better in terms of
aesthetics and performance. Improved search, simplified ui
(without removing features) and more secure and up-to-date
build are just a few examples.
FileBrowser Quantum differs significantly to the original.
There are hundreds of thousands of lines changed and they are generally
no longer compatible with each other. This has been intentional -- the
focus of this fork is on a few key principles:
- Simplicity and improved user experience
- Improving performance and faster feedback when making changes.
- Minimize external dependencies and standard library usage.
- Of course -- adding much-needed features.
## Look
<p align="center">
<img width="500" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/35cdeb3b-ab79-4b04-8001-8f51f6ea06bb" title="Dark mode">
<img width="500" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/55fa4f5c-440e-4a97-b711-96139208a163">
<img width="500" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/c76f4100-949b-4e17-a3e6-e410fb8ec08f">
<img width="500" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/0bde26f3-fa90-411e-bd0b-abaa47506d62">
<img width="560" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/71d8f2b8-6fe6-4fdc-8aac-503d08c28d86">
One way you can observe the improved user experience is how I changed
the UI. The Navbar is simplified to a three-component system :
1. (Left) The slide-out action panel button
2. (Middle) The powerful search bar.
3. (Right) The view change toggle.
All other functions are moved either into the action menu or popup menus.
If the action does not depend on context, it will exist in the slide-out
action panel. If the action is available based on context, it will show up as
a popup menu.
<p align="center">
<img width="800" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/899152cf-3e69-4179-aa82-752af2df3fc6" title="Main Screenshot">
<img width="800" src="https://github.com/user-attachments/assets/18c02d03-5c60-4e15-9c32-3cfe058a0c49" title="Main Screenshot">
<img width="800" src="https://github.com/user-attachments/assets/75226dc4-9802-46f0-9e3c-e4403d3275da" title="Main Screenshot">
</p>
## Install
Using docker:
@@ -59,10 +81,10 @@ Using docker:
1. docker run (no persistent db):
```
docker run -it -v /path/to/folder:/srv -p 80:8080 gtstef/filebrowser
docker run -it -v /path/to/folder:/srv -p 80:80 gtstef/filebrowser
```
1. docker-compose:
1. docker compose:
- with local storage
@@ -75,7 +97,7 @@ services:
- './database:/database' # optional if you want db to persist - configure a path under "database" dir in config file.
- './filebrowser.yaml:/filebrowser.yaml' # required
ports:
- '80:8080'
- '80:80'
image: gtstef/filebrowser
restart: always
```
@@ -91,7 +113,7 @@ services:
- './database:/database' # optional if you want db to persist - configure a path under "database" dir in config file.
- './filebrowser.yaml:/filebrowser.yaml' # required
ports:
- '80:8080'
- '80:80'
image: gtstef/filebrowser
restart: always
volumes:
@@ -103,23 +125,96 @@ volumes:
```
Not using docker (not recommended), download your binary from releases and run with your custom config file:
```
./filebrowser -c <filebrowser.yml or other /path/to/config.yaml>
```
## Configuration
All configuration is now done via a single configuration file: `filebrowser.yaml`, here is an example minimal [configuration file](./backend/filebrowser.yaml).
All configuration is now done via a single configuration file:
`filebrowser.yaml`, here is an example of minimal [configuration
file](./backend/filebrowser.yaml).
View the [Configuration Help Page](./configuration.md) for available configuration options and other help.
View the [Configuration Help Page](./configuration.md) for available
configuration options and other help.
### background & help
The original project filebrowser/filebrowser used multiple different ways to configure the server.
This was confusing and difficult to work with from a user and from a developer's perspective.
So I completely redesigned the program to use one single human-readable config file.
## Migration from filebrowser/filebrowser
I understand many coming from the original fork may notice differences which make using this improved version more difficult. If you notice issues that you believe should be fixed, please open an issue here and it will very likely be addressed with a PR within a few weeks.
If you currently use the original opensource filebrowser
but want to try using this. I recommend you start fresh without
reusing the database, but there are a few things you'll need to do if you
must migrate:
This version of filebrowser is going through a configuration overhaul as mentioned above. Certain features related to rules and commands may not work as they do on the original filebrowser. The purpose of this is to create a more consistent experience where configuration is done via files rather than running commands, so that it's very clear what the current state of the configuration is. When running commands its not clear what the configuration is.
1. Create a configuration file as mentioned above.
2. Copy your database file from the original filebrowser to the path of
the new one.
3. Update the configuration file to use the database (under server in
filebrowser.yml)
4. If you are using docker, update the docker-compose file or docker run
command to use the config file as described in the install section
above.
5. If you are not using docker, just make sure you run filebrowser -c
filebrowser.yml and have a valid filebrowser config.
The filebrowser Quantum application should run with the same user and rules that
you have from the original. But keep in mind the differences that are
mentioned at the top of this readme.
## 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
see [Roadmap Page](./roadmap.md)
+232
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
+43
View File
@@ -0,0 +1,43 @@
# .goreleaser.yaml
project_name: filebrowser
version: 2
builds:
# Build configuration for darwin and linux
- id: default
ldflags: &ldflags
- -s -w -X github.com/gtsteffaniak/filebrowser/version.Version={{ .Version }} -X github.com/gtsteffaniak/filebrowser/version.CommitSHA={{ .ShortCommit }}
main: main.go
binary: filebrowser
goos:
- darwin
- linux
goarch:
- amd64
- arm
- arm64
goarm:
- 6
- 7
hooks:
post:
- upx {{ .Path }} # Compress the binary with UPX
# Build configuration for windows without arm
- id: windows
ldflags: *ldflags
main: main.go
binary: filebrowser
goos:
- windows
goarch:
- amd64
- arm64
archives:
- name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
format: binary
checksum:
disable: true
+1 -2
View File
@@ -145,7 +145,6 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
}
if u == nil {
log.Println("creds", a.Cred.Password)
// create user with the provided credentials
d := &users.User{
Username: a.Cred.Username,
@@ -188,7 +187,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
func (a *HookAuth) GetUser(d *users.User) *users.User {
// adds all permissions when user is admin
isAdmin := d.Perm.Admin
perms := users.Permissions{
perms := settings.Permissions{
Admin: isAdmin,
Execute: isAdmin || d.Perm.Execute,
Create: isAdmin || d.Perm.Create,
+15 -27
View File
@@ -5,49 +5,37 @@
? github.com/gtsteffaniak/filebrowser/auth [no test files]
? github.com/gtsteffaniak/filebrowser/cmd [no test files]
PASS
ok github.com/gtsteffaniak/filebrowser/diskcache 0.003s
ok github.com/gtsteffaniak/filebrowser/diskcache 0.004s
? github.com/gtsteffaniak/filebrowser/errors [no test files]
goos: linux
goarch: amd64
pkg: github.com/gtsteffaniak/filebrowser/files
cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz
BenchmarkFillIndex-8 10 3295862 ns/op 230448 B/op 1927 allocs/op
BenchmarkSearchAllIndexes-8 10 30033386 ns/op 19647893 B/op 298702 allocs/op
BenchmarkFillIndex-8 10 3559830 ns/op 274639 B/op 2026 allocs/op
BenchmarkSearchAllIndexes-8 10 31912612 ns/op 20545741 B/op 312477 allocs/op
PASS
ok github.com/gtsteffaniak/filebrowser/files 0.392s
ok github.com/gtsteffaniak/filebrowser/files 0.417s
PASS
ok github.com/gtsteffaniak/filebrowser/fileutils 0.003s
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 h: 401 <nil>
2023/11/24 13:57:20 h: 401 <nil>
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 h: 401 <nil>
2023/11/24 13:57:20 h: 401 <nil>
2023/11/24 13:57:20 h: 401 <nil>
2023/11/24 13:57:20 h: 401 <nil>
ok github.com/gtsteffaniak/filebrowser/fileutils 0.002s
2024/08/27 16:16:13 h: 401 <nil>
2024/08/27 16:16:13 h: 401 <nil>
2024/08/27 16:16:13 h: 401 <nil>
2024/08/27 16:16:13 h: 401 <nil>
2024/08/27 16:16:13 h: 401 <nil>
2024/08/27 16:16:13 h: 401 <nil>
PASS
ok github.com/gtsteffaniak/filebrowser/http 0.208s
ok github.com/gtsteffaniak/filebrowser/http 0.100s
PASS
ok github.com/gtsteffaniak/filebrowser/img 0.118s
ok github.com/gtsteffaniak/filebrowser/img 0.124s
PASS
ok github.com/gtsteffaniak/filebrowser/rules 0.002s
PASS
ok github.com/gtsteffaniak/filebrowser/runner 0.003s
PASS
ok github.com/gtsteffaniak/filebrowser/settings 0.005s
ok github.com/gtsteffaniak/filebrowser/settings 0.004s
? github.com/gtsteffaniak/filebrowser/share [no test files]
? github.com/gtsteffaniak/filebrowser/storage [no test files]
? github.com/gtsteffaniak/filebrowser/storage/bolt [no test files]
PASS
ok github.com/gtsteffaniak/filebrowser/users 0.003s
ok github.com/gtsteffaniak/filebrowser/users 0.002s
? github.com/gtsteffaniak/filebrowser/version [no test files]
+4
View File
@@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore
+63 -32
View File
@@ -3,6 +3,7 @@ package cmd
import (
"crypto/tls"
"flag"
"fmt"
"io/fs"
"log"
"net"
@@ -12,9 +13,10 @@ import (
"strconv"
"syscall"
"embed"
"github.com/spf13/pflag"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/auth"
@@ -24,8 +26,14 @@ import (
"github.com/gtsteffaniak/filebrowser/img"
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/users"
"github.com/gtsteffaniak/filebrowser/version"
)
//go:embed dist/*
var assets embed.FS
var nonEmbededFS = os.Getenv("FILEBROWSER_NO_EMBEDED") == "true"
type dirFS struct {
http.Dir
}
@@ -40,7 +48,8 @@ func init() {
// Bind the flags to the pflag command line parser
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse()
log.Println("Initializing with config file:", *configFlag)
log.Printf("Initializing FileBrowser Quantum (%v) with config file: %v \n", version.Version, *configFlag)
log.Println("Embeded Frontend:", !nonEmbededFS)
settings.Initialize(*configFlag)
}
@@ -55,58 +64,81 @@ var rootCmd = &cobra.Command{
log.Fatal("Image resize workers count could not be < 1")
}
imgSvc := img.New(serverConfig.NumImageProcessors)
var fileCache diskcache.Interface = diskcache.NewNoOp()
cacheDir := "/tmp"
var fileCache diskcache.Interface
// Use file cache if cacheDir is specified
if cacheDir != "" {
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet,gomnd
log.Fatalf("can't make directory %s: %s", cacheDir, err)
var err error
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)
go files.InitializeIndex(serverConfig.IndexingInterval, true)
go files.InitializeIndex(serverConfig.IndexingInterval, serverConfig.Indexing)
_, err := os.Stat(serverConfig.Root)
checkErr(err)
checkErr(fmt.Sprint("cmd os.Stat ", serverConfig.Root), err)
var listener net.Listener
address := serverConfig.Address + ":" + strconv.Itoa(serverConfig.Port)
switch {
case serverConfig.Socket != "":
listener, err = net.Listen("unix", serverConfig.Socket)
checkErr(err)
checkErr("net.Listen", err)
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
checkErr(err)
checkErr("cmd.Flags().GetUint32", err)
err = os.Chmod(serverConfig.Socket, os.FileMode(socketPerm))
checkErr(err)
checkErr("os.Chmod", err)
case serverConfig.TLSKey != "" && serverConfig.TLSCert != "":
cer, err := tls.LoadX509KeyPair(serverConfig.TLSCert, serverConfig.TLSKey) //nolint:govet
checkErr(err)
checkErr("tls.LoadX509KeyPair", err)
listener, err = tls.Listen("tcp", address, &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cer}},
)
checkErr(err)
checkErr("tls.Listen", err)
default:
listener, err = net.Listen("tcp", address)
checkErr(err)
checkErr("net.Listen", err)
}
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
go cleanupHandler(listener, sigc)
assetsFs := dirFS{Dir: http.Dir("frontend/dist")}
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, &serverConfig, assetsFs)
checkErr(err)
defer listener.Close()
log.Println("Listening on", listener.Addr().String())
//nolint: gosec
if err := http.Serve(listener, handler); err != nil {
log.Fatal(err)
if !nonEmbededFS {
assetsFs, err := fs.Sub(assets, "dist")
if err != nil {
log.Fatal("Could not embed frontend. Does backend/cmd/dist exist? Must be built and exist first")
}
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, &serverConfig, assetsFs)
checkErr("fbhttp.NewHandler", err)
defer listener.Close()
log.Println("Listening on", listener.Addr().String())
//nolint: gosec
if err := http.Serve(listener, handler); err != nil {
log.Fatalf("Could not start server on port %d: %v", serverConfig.Port, err)
}
} else {
assetsFs := dirFS{Dir: http.Dir("frontend/dist")}
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, &serverConfig, assetsFs)
checkErr("fbhttp.NewHandler", err)
defer listener.Close()
log.Println("Listening on", listener.Addr().String())
//nolint: gosec
if err := http.Serve(listener, handler); err != nil {
log.Fatalf("Could not start server on port %d: %v", serverConfig.Port, err)
}
}
}, pythonConfig{allowNoDB: true}),
}
func StartFilebrowser() {
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
log.Fatal("Error starting filebrowser:", err)
}
}
@@ -121,18 +153,17 @@ func quickSetup(d pythonData) {
settings.Config.Auth.Key = generateKey()
if settings.Config.Auth.Method == "noauth" {
err := d.store.Auth.Save(&auth.NoAuth{})
checkErr(err)
checkErr("d.store.Auth.Save", err)
} else {
settings.Config.Auth.Method = "password"
err := d.store.Auth.Save(&auth.JSONAuth{})
checkErr(err)
checkErr("d.store.Auth.Save", err)
}
err := d.store.Settings.Save(&settings.Config)
checkErr(err)
checkErr("d.store.Settings.Save", err)
err = d.store.Settings.SaveServer(&settings.Config.Server)
checkErr(err)
user := &users.User{}
settings.Config.UserDefaults.Apply(user)
checkErr("d.store.Settings.SaveServer", err)
user := users.ApplyDefaults(users.User{})
user.Username = settings.Config.Auth.AdminUsername
user.Password = settings.Config.Auth.AdminPassword
user.Perm.Admin = true
@@ -140,7 +171,7 @@ func quickSetup(d pythonData) {
user.DarkMode = true
user.ViewMode = "normal"
user.LockPassword = false
user.Perm = users.Permissions{
user.Perm = settings.Permissions{
Create: true,
Rename: true,
Modify: true,
@@ -149,6 +180,6 @@ func quickSetup(d pythonData) {
Download: true,
Admin: true,
}
err = d.store.Users.Save(user)
checkErr(err)
err = d.store.Users.Save(&user)
checkErr("d.store.Users.Save", err)
}
+4 -4
View File
@@ -42,23 +42,23 @@ including 'index_end'.`,
},
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
i, err := strconv.Atoi(args[0])
checkErr(err)
checkErr("strconv.Atoi", err)
f := i
if len(args) == 2 { //nolint:gomnd
f, err = strconv.Atoi(args[1])
checkErr(err)
checkErr("strconv.Atoi", err)
}
user := func(u *users.User) {
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
err := d.store.Users.Save(u)
checkErr(err)
checkErr("d.store.Users.Save", err)
}
global := func(s *settings.Settings) {
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
err := d.store.Settings.Save(s)
checkErr(err)
checkErr("d.store.Settings.Save", err)
}
runRules(d.store, cmd, user, global)
+2 -7
View File
@@ -33,7 +33,7 @@ func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User)
id := getUserIdentifier(cmd.Flags())
if id != nil {
user, err := st.Users.Get("", id)
checkErr(err)
checkErr("st.Users.Get", err)
if usersFn != nil {
usersFn(user)
@@ -44,7 +44,7 @@ func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User)
}
s, err := st.Settings.Get()
checkErr(err)
checkErr("st.Settings.Get", err)
if globalFn != nil {
globalFn(s)
@@ -67,11 +67,6 @@ func getUserIdentifier(flags *pflag.FlagSet) interface{} {
}
func printRules(rulez []rules.Rule, id interface{}) {
if id == nil {
fmt.Printf("Global Rules:\n\n")
} else {
fmt.Printf("Rules for user %v:\n\n", id)
}
for id, rule := range rulez {
fmt.Printf("(%d) ", id)
+2 -2
View File
@@ -44,13 +44,13 @@ var rulesAddCmd = &cobra.Command{
user := func(u *users.User) {
u.Rules = append(u.Rules, rule)
err := d.store.Users.Save(u)
checkErr(err)
checkErr("d.store.Users.Save", err)
}
global := func(s *settings.Settings) {
s.Rules = append(s.Rules, rule)
err := d.store.Settings.Save(s)
checkErr(err)
checkErr("d.store.Settings.Save", err)
}
runRules(d.store, cmd, user, global)
+4 -4
View File
@@ -22,19 +22,19 @@ var usersAddCmd = &cobra.Command{
LockPassword: mustGetBool(cmd.Flags(), "lockPassword"),
}
servSettings, err := d.store.Settings.GetServer()
checkErr(err)
checkErr("d.store.Settings.GetServer()", err)
// since getUserDefaults() polluted s.Defaults.Scope
// which makes the Scope not the one saved in the db
// we need the right s.Defaults.Scope here
s2, err := d.store.Settings.Get()
checkErr(err)
checkErr("d.store.Settings.Get()", err)
userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root)
checkErr(err)
checkErr("s2.MakeUserDir", err)
user.Scope = userHome
err = d.store.Users.Save(user)
checkErr(err)
checkErr("d.store.Users.Save", err)
printUsers([]*users.User{user})
}, pythonConfig{}),
}
+2 -2
View File
@@ -16,9 +16,9 @@ path to the file where you want to write the users.`,
Args: jsonYamlArg,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
list, err := d.store.Users.Gets("")
checkErr(err)
checkErr("d.store.Users.Gets", err)
err = marshal(args[0], list)
checkErr(err)
checkErr("marshal", err)
}, pythonConfig{}),
}
+1 -1
View File
@@ -46,6 +46,6 @@ var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) {
list, err = d.store.Users.Gets("")
}
checkErr(err)
checkErr("findUsers", err)
printUsers(list)
}, pythonConfig{})
+10 -13
View File
@@ -27,28 +27,23 @@ list or set it to 0.`,
Args: jsonYamlArg,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
fd, err := os.Open(args[0])
checkErr(err)
checkErr("os.Open", err)
defer fd.Close()
list := []*users.User{}
err = unmarshal(args[0], &list)
checkErr(err)
for _, user := range list {
err = user.Clean("")
checkErr(err)
}
checkErr("unmarshal", err)
if mustGetBool(cmd.Flags(), "replace") {
oldUsers, err := d.store.Users.Gets("")
checkErr(err)
checkErr("d.store.Users.Gets", err)
err = marshal("users.backup.json", list)
checkErr(err)
checkErr("marshal users.backup.json", err)
for _, user := range oldUsers {
err = d.store.Users.Delete(user.ID)
checkErr(err)
checkErr("d.store.Users.Delete", err)
}
}
@@ -60,7 +55,8 @@ list or set it to 0.`,
// User exists in DB.
if err == nil {
if !overwrite {
checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registred"))
newErr := errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered")
checkErr("", newErr)
}
// If the usernames mismatch, check if there is another one in the DB
@@ -68,7 +64,8 @@ list or set it to 0.`,
// operation
if user.Username != onDB.Username {
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
newErr := usernameConflictError(user.Username, conflictuous.ID, user.ID)
checkErr("usernameConflictError", newErr)
}
}
} else {
@@ -78,7 +75,7 @@ list or set it to 0.`,
}
err = d.store.Users.Save(user)
checkErr(err)
checkErr("d.store.Users.Save", err)
}
}, pythonConfig{}),
}
+3 -3
View File
@@ -1,7 +1,7 @@
package cmd
import (
"fmt"
"log"
"github.com/spf13/cobra"
)
@@ -25,7 +25,7 @@ var usersRmCmd = &cobra.Command{
err = d.store.Users.Delete(id)
}
checkErr(err)
fmt.Println("user deleted successfully")
checkErr("usersRmCmd", err)
log.Println("user deleted successfully")
}, pythonConfig{}),
}
+2 -2
View File
@@ -29,10 +29,10 @@ options you want to change.`,
} else {
user, err = d.store.Users.Get("", username)
}
checkErr(err)
checkErr("d.store.Users.Get", err)
err = d.store.Users.Update(user)
checkErr(err)
checkErr("d.store.Users.Update", err)
printUsers([]*users.User{user})
}, pythonConfig{}),
}
+12 -10
View File
@@ -3,6 +3,7 @@ package cmd
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"path/filepath"
@@ -17,33 +18,33 @@ import (
"github.com/gtsteffaniak/filebrowser/storage/bolt"
)
func checkErr(err error) {
func checkErr(source string, err error) {
if err != nil {
log.Fatal(err)
log.Fatalf("%s: %v", source, err)
}
}
func mustGetString(flags *pflag.FlagSet, flag string) string {
s, err := flags.GetString(flag)
checkErr(err)
checkErr("mustGetString", err)
return s
}
func mustGetBool(flags *pflag.FlagSet, flag string) bool {
b, err := flags.GetBool(flag)
checkErr(err)
checkErr("mustGetBool", err)
return b
}
func mustGetUint(flags *pflag.FlagSet, flag string) uint {
b, err := flags.GetUint(flag)
checkErr(err)
checkErr("mustGetUint", err)
return b
}
func generateKey() []byte {
k, err := settings.GenerateKey()
checkErr(err)
checkErr("generateKey", err)
return k
}
@@ -96,18 +97,19 @@ func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
data.hadDB = exists
db, err := storm.Open(path)
checkErr(err)
checkErr(fmt.Sprintf("storm.Open path %v", path), err)
defer db.Close()
data.store, err = bolt.NewStorage(db)
checkErr(err)
checkErr("bolt.NewStorage", err)
fn(cmd, args, data)
}
}
func marshal(filename string, data interface{}) error {
fd, err := os.Create(filename)
checkErr(err)
checkErr("os.Create", err)
defer fd.Close()
switch ext := filepath.Ext(filename); ext {
@@ -125,7 +127,7 @@ func marshal(filename string, data interface{}) error {
func unmarshal(filename string, data interface{}) error {
fd, err := os.Open(filename)
checkErr(err)
checkErr("os.Open", err)
defer fd.Close()
switch ext := filepath.Ext(filename); ext {
+1 -1
View File
@@ -16,6 +16,6 @@ var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA)
fmt.Println("File Browser " + version.Version + "/" + version.CommitSHA)
},
}
+19 -13
View File
@@ -10,13 +10,17 @@ import (
"os"
"path/filepath"
"sync"
"github.com/spf13/afero"
)
type FileCache struct {
fs afero.Fs
// Cache interface for caching operations
type Cache interface {
Get(key string) ([]byte, error)
Set(key string, value []byte) error
}
// FileCache struct for file-based caching
type FileCache struct {
dir string
// granular locks
scopedLocks struct {
sync.Mutex
@@ -25,10 +29,12 @@ type FileCache struct {
}
}
func New(fs afero.Fs, root string) *FileCache {
return &FileCache{
fs: afero.NewBasePathFs(fs, root),
// NewFileCache creates a new FileCache
func NewFileCache(dir string) (*FileCache, error) {
if err := os.MkdirAll(dir, 0700); err != nil {
return nil, fmt.Errorf("can't make directory %s: %v", dir, err)
}
return &FileCache{dir: dir}, nil
}
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()
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
}
if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil { //nolint:gomnd
if err := os.WriteFile(fileName, value, 0600); err != nil {
return err
}
@@ -68,15 +74,15 @@ func (f *FileCache) Delete(ctx context.Context, key string) error {
defer mu.Unlock()
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 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)
file, err := f.fs.Open(fileName)
file, err := os.Open(fileName)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, false, nil
@@ -106,5 +112,5 @@ func (f *FileCache) getFileName(key string) string {
hasher := sha1.New() //nolint:gosec
_, _ = hasher.Write([]byte(key))
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))
}
+23 -11
View File
@@ -2,10 +2,10 @@ package diskcache
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
)
@@ -15,35 +15,39 @@ func TestFileCache(t *testing.T) {
key = "key"
value = "some text"
newValue = "new text"
cacheRoot = "/cache"
cacheRoot = "cache"
cachedFilePath = "a/62/a62f2225bf70bfaccbc7f1ef2a397836717377de"
)
fs := afero.NewMemMapFs()
cache := New(fs, "/cache")
// Create temporary directory for the cache
cacheDir, err := os.MkdirTemp("", cacheRoot)
require.NoError(t, err)
defer os.RemoveAll(cacheDir) // Clean up
cache, err := NewFileCache(cacheDir)
require.NoError(t, err)
// store new key
err := cache.Store(ctx, key, []byte(value))
err = cache.Store(ctx, key, []byte(value))
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
err = cache.Store(ctx, key, []byte(newValue))
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
err = cache.Delete(ctx, key)
require.NoError(t, err)
exists, err := afero.Exists(fs, filepath.Join(cacheRoot, cachedFilePath))
require.NoError(t, err)
exists := fileExists(filepath.Join(cacheDir, cachedFilePath))
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()
// check actual file content
b, err := afero.ReadFile(fs, fileFullPath)
b, err := os.ReadFile(fileFullPath)
require.NoError(t, err)
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.Equal(t, wantValue, string(b))
}
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
+2 -2
View File
@@ -1,7 +1,7 @@
server:
port: 8080
port: 80
baseURL: "/"
root: "/srv"
root: "./srv"
auth:
method: password
signup: false
+1 -2
View File
@@ -29,10 +29,9 @@ var textTypes = []string{
".yaml",
".yml",
".json",
".bashrc",
".zshrc",
".env",
}
var compressedFile = []string{
".7z",
".rar",
+243 -121
View File
@@ -1,25 +1,26 @@
package files
import (
"crypto/md5" //nolint:gosec
"crypto/sha1" //nolint:gosec
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"fmt"
"hash"
"io"
"mime"
"net/http"
"os"
filepath "path/filepath"
"path/filepath"
"strings"
"sync"
"time"
"github.com/spf13/afero"
"unicode/utf8"
"github.com/gtsteffaniak/filebrowser/errors"
"github.com/gtsteffaniak/filebrowser/rules"
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/users"
)
@@ -32,7 +33,6 @@ var (
// FileInfo describes a file.
type FileInfo struct {
*Listing
Fs afero.Fs `json:"-"`
Path string `json:"path,omitempty"`
Name string `json:"name"`
Size int64 `json:"size"`
@@ -51,8 +51,8 @@ type FileInfo struct {
// FileOptions are the options when getting a file info.
type FileOptions struct {
Fs afero.Fs
Path string
Path string // realpath
IsDir bool
Modify bool
Expand bool
ReadHeader bool
@@ -84,13 +84,13 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
if !opts.Checker.Check(opts.Path) {
return nil, os.ErrPermission
}
file, err := stat(opts.Path, opts) // Pass opts.Path here
file, err := stat(opts)
if err != nil {
return nil, err
}
if opts.Expand {
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 file, nil
@@ -102,7 +102,6 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
}
return file, err
}
func FileInfoFaster(opts FileOptions) (*FileInfo, error) {
// Lock access for the specific path
pathMutex := getMutex(opts.Path)
@@ -112,108 +111,83 @@ func FileInfoFaster(opts FileOptions) (*FileInfo, error) {
return nil, os.ErrPermission
}
index := GetIndex(rootPath)
trimmed := strings.TrimPrefix(opts.Path, "/")
if trimmed == "" {
trimmed = "/"
adjustedPath := index.makeIndexPath(opts.Path, opts.IsDir)
if opts.IsDir {
info, exists := index.GetMetadataInfo(adjustedPath)
if exists && !opts.Content {
// Let's not refresh if less than a second has passed
if time.Since(info.CacheTime) > time.Second {
go RefreshFileInfo(opts) //nolint:errcheck
}
// refresh cache after
return &info, nil
}
}
// don't bother caching content
if opts.Content {
file, err := NewFileInfo(opts)
return file, err
}
err := RefreshFileInfo(opts)
if err != nil {
file, err := NewFileInfo(opts)
return file, err
}
adjustedPath := makeIndexPath(trimmed, index.Root)
var info FileInfo
info, exists := index.GetMetadataInfo(adjustedPath)
if exists {
// Check if the cache time is less than 1 second
if time.Since(info.CacheTime) > time.Second {
go refreshFileInfo(opts)
}
// refresh cache after
return &info, nil
} else {
updated := refreshFileInfo(opts)
if !updated {
file, err := NewFileInfo(opts)
return file, err
}
info, exists = index.GetMetadataInfo(adjustedPath)
if !exists || info.Name == "" {
return &FileInfo{}, errors.ErrEmptyKey
}
return &info, nil
if !exists || info.Name == "" {
return &FileInfo{}, errors.ErrEmptyKey
}
return &info, nil
}
func refreshFileInfo(opts FileOptions) bool {
func RefreshFileInfo(opts FileOptions) error {
if !opts.Checker.Check(opts.Path) {
return false
return fmt.Errorf("permission denied: %s", opts.Path)
}
file, err := stat(opts.Path, opts) // Pass opts.Path here
if err != nil {
return false
}
index := GetIndex(rootPath)
trimmed := strings.TrimPrefix(opts.Path, "/")
if trimmed == "" {
trimmed = "/"
adjustedPath := index.makeIndexPath(opts.Path, opts.IsDir)
file, err := stat(opts)
if err != nil {
return fmt.Errorf("File/folder does not exist to refresh data: %s", opts.Path)
}
adjustedPath := makeIndexPath(trimmed, index.Root)
_ = file.detectType(opts.Path, true, opts.Content, opts.ReadHeader)
if file.IsDir {
err := file.readListing(opts.Path, opts.Checker, opts.ReadHeader)
if err != nil {
return false
return fmt.Errorf("Dir info could not be read: %s", opts.Path)
}
//_, exists := index.GetFileMetadata(adjustedPath)
return index.UpdateFileMetadata(adjustedPath, *file)
} else {
//_, exists := index.GetFileMetadata(adjustedPath)
return index.UpdateFileMetadata(adjustedPath, *file)
}
result := index.UpdateFileMetadata(adjustedPath, *file)
if !result {
return fmt.Errorf("File/folder does not exist in metadata: %s", adjustedPath)
}
return nil
}
func stat(path string, opts FileOptions) (*FileInfo, error) {
var file *FileInfo
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
info, _, err := lstaterFs.LstatIfPossible(path)
if err == nil {
file = &FileInfo{
Fs: opts.Fs,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
Token: opts.Token,
}
if info.IsDir() {
file.IsDir = true
}
if info.Mode()&os.ModeSymlink != 0 {
file.IsSymlink = true
}
}
func stat(opts FileOptions) (*FileInfo, error) {
info, err := os.Lstat(opts.Path)
if err != nil {
return nil, err
}
if file == nil || file.IsSymlink {
info, err := opts.Fs.Stat(opts.Path)
if err != nil {
return nil, err
}
if file != nil && file.IsSymlink {
file.Size = info.Size()
file.IsDir = info.IsDir()
return file, nil
}
file = &FileInfo{
Fs: opts.Fs,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
Token: opts.Token,
file := &FileInfo{
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
Token: opts.Token,
}
if info.IsDir() {
file.IsDir = true
}
if info.Mode()&os.ModeSymlink != 0 {
file.IsSymlink = true
targetInfo, err := os.Stat(opts.Path)
if err == nil {
file.Size = targetInfo.Size()
file.IsDir = targetInfo.IsDir()
}
}
@@ -231,7 +205,7 @@ func (i *FileInfo) Checksum(algo string) error {
i.Checksums = map[string]string{}
}
reader, err := i.Fs.Open(i.Path)
reader, err := os.Open(i.Path)
if err != nil {
return err
}
@@ -260,24 +234,149 @@ func (i *FileInfo) Checksum(algo string) error {
// RealPath gets the real path for the file, resolving symlinks if supported.
func (i *FileInfo) RealPath() string {
if realPathFs, ok := i.Fs.(interface {
RealPath(name string) (fPath string, err error)
}); ok {
realPath, err := realPathFs.RealPath(i.Path)
if err == nil {
return realPath
}
realPath, err := filepath.EvalSymlinks(i.Path)
if err == nil {
return realPath
}
return i.Path
}
func GetRealPath(relativePath ...string) (string, bool, 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 "", false, err
}
if !Exists(absolutePath) {
return absolutePath, false, 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
}
opts.Path = filepath.Dir(absPath)
err = RefreshFileInfo(opts)
if err != nil {
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)
err = RefreshFileInfo(opts)
if err != nil {
return errors.ErrEmptyKey
}
return nil
}
func WriteFile(opts FileOptions, in io.Reader) error {
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
}
return i.Path
// 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
}
opts.Path = parentDir
err = RefreshFileInfo(opts)
if err != nil {
return errors.ErrEmptyKey
}
return nil
}
// resolveSymlinks resolves symlinks in the given path
func resolveSymlinks(path string) (string, bool, error) {
for {
// Get the file info
info, err := os.Lstat(path)
if err != nil {
return "", false, 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 "", false, err
}
// Resolve the target relative to the symlink's directory
path = filepath.Join(filepath.Dir(path), target)
} else {
// Not a symlink, so return the resolved path and check if it's a directory
return path, info.IsDir(), nil
}
}
}
// addContent reads and sets content based on the file type.
func (i *FileInfo) addContent(path string) error {
if !i.IsDir {
content, err := os.ReadFile(path)
if err != nil {
return err
}
stringContent := string(content)
if !utf8.ValidString(stringContent) {
return nil
}
if stringContent == "" {
i.Content = "empty-file-x6OlSil"
return nil
}
i.Content = stringContent
}
return nil
}
// detectType detects the file type.
func (i *FileInfo) detectType(path string, modify, saveContent, readHeader bool) error {
if IsNamedPipe(i.Mode) {
i.Type = "blob"
if i.IsDir {
return nil
}
if IsNamedPipe(i.Mode) {
i.Type = "blob"
if saveContent {
return i.addContent(path)
}
return nil
}
var buffer []byte
if readHeader {
buffer = i.readFirstBytes()
@@ -286,23 +385,20 @@ func (i *FileInfo) detectType(path string, modify, saveContent, readHeader bool)
http.DetectContentType(buffer)
}
}
ext := filepath.Ext(i.Name)
for _, fileType := range AllFiletypeOptions {
if IsMatchingType(ext, fileType) {
i.Type = fileType
}
switch i.Type {
case "text":
if !modify {
i.Type = "textImmutable"
}
if saveContent {
afs := &afero.Afero{Fs: i.Fs}
content, err := afs.ReadFile(path)
if err != nil {
return err
}
i.Content = string(content)
return i.addContent(path)
}
case "video":
parentDir := strings.TrimRight(path, i.Name)
@@ -310,26 +406,34 @@ func (i *FileInfo) detectType(path string, modify, saveContent, readHeader bool)
case "doc":
if ext == ".pdf" {
i.Type = "pdf"
return nil
}
if saveContent {
return i.addContent(path)
}
}
}
if i.Type == "" {
i.Type = "blob"
if saveContent {
return i.addContent(path)
}
}
return nil
}
// readFirstBytes reads the first bytes of the file.
func (i *FileInfo) readFirstBytes() []byte {
reader, err := i.Fs.Open(i.Path)
file, err := os.Open(i.Path)
if err != nil {
i.Type = "blob"
return nil
}
defer reader.Close()
defer file.Close()
buffer := make([]byte, 512) //nolint:gomnd
n, err := reader.Read(buffer)
n, err := file.Read(buffer)
if err != nil && err != io.EOF {
i.Type = "blob"
return nil
@@ -350,7 +454,8 @@ func (i *FileInfo) detectSubtitles(parentDir string) {
// Directory must have been deleted, remove it from the index
return
}
// Read the directory contents
defer dir.Close() // Ensure directory handle is closed
files, err := dir.Readdir(-1)
if err != nil {
return
@@ -375,8 +480,13 @@ func (i *FileInfo) detectSubtitles(parentDir string) {
// readListing reads the contents of a directory and fills the listing.
func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bool) error {
afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path)
dir, err := os.Open(i.Path)
if err != nil {
return err
}
defer dir.Close()
files, err := dir.Readdir(-1)
if err != nil {
return err
}
@@ -388,7 +498,7 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
NumFiles: 0,
}
for _, f := range dir {
for _, f := range files {
name := f.Name()
fPath := filepath.Join(i.Path, name)
@@ -399,7 +509,7 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
isSymlink, isInvalidLink := false, false
if IsSymlink(f.Mode()) {
isSymlink = true
info, err := i.Fs.Stat(fPath)
info, err := os.Stat(fPath)
if err == nil {
f = info
} else {
@@ -441,6 +551,7 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
i.Listing = listing
return nil
}
func IsNamedPipe(mode os.FileMode) bool {
return mode&os.ModeNamedPipe != 0
}
@@ -461,3 +572,14 @@ func getMutex(path string) *sync.Mutex {
return pathMutexes[path]
}
func Exists(path string) bool {
_, err := os.Stat(path)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return false
}
+76
View File
@@ -0,0 +1,76 @@
package files
import (
"os"
"path/filepath"
"strings"
"testing"
)
func Test_GetRealPath(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
return
}
trimPrefix := filepath.Dir(filepath.Dir(cwd)) + "/"
tests := []struct {
name string
paths []string
want struct {
path string
isDir bool
}
}{
{
name: "current directory",
paths: []string{
"./",
},
want: struct {
path string
isDir bool
}{
path: "backend/files",
isDir: true,
},
},
{
name: "current directory",
paths: []string{
"./file.go",
},
want: struct {
path string
isDir bool
}{
path: "backend/files/file.go",
isDir: false,
},
},
{
name: "other test case",
paths: []string{
"/mnt/doesnt/exist",
},
want: struct {
path string
isDir bool
}{
path: "/mnt/doesnt/exist",
isDir: false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
realPath, isDir, err := GetRealPath(tt.paths...)
adjustedRealPath := strings.TrimPrefix(realPath, trimPrefix)
if tt.want.path != adjustedRealPath || tt.want.isDir != isDir {
t.Errorf("expected %v:%v but got: %v:%v", tt.want.path, tt.want.isDir, adjustedRealPath, isDir)
}
if err != nil {
t.Error("got error", err)
}
})
}
}
+15 -7
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"log"
"os"
"path/filepath"
"strings"
"sync"
"time"
@@ -15,6 +16,7 @@ type Directory struct {
Metadata map[string]FileInfo
Files string
}
type File struct {
Name string
IsDir bool
@@ -80,8 +82,7 @@ func indexingScheduler(intervalMinutes uint32) {
// Define a function to recursively index files and directories
func (si *Index) indexFiles(path string) error {
// Check if the current directory has been modified since the last indexing
path = strings.TrimSuffix(path, "/")
adjustedPath := makeIndexPath(path, si.Root)
adjustedPath := si.makeIndexPath(path, true)
dir, err := os.Open(path)
if err != nil {
// Directory must have been deleted, remove it from the index
@@ -114,7 +115,7 @@ func (si *Index) indexFiles(path string) error {
}
func (si *Index) InsertFiles(path string) {
adjustedPath := makeIndexPath(path, si.Root)
adjustedPath := si.makeIndexPath(path, true)
subDirectory := Directory{}
buffer := bytes.Buffer{}
@@ -130,9 +131,9 @@ func (si *Index) InsertFiles(path string) {
}
func (si *Index) InsertDirs(path string) {
adjustedPath := makeIndexPath(path, si.Root)
for _, f := range si.GetQuickList() {
if f.IsDir {
adjustedPath := si.makeIndexPath(path, true)
if _, exists := si.Directories[adjustedPath]; exists {
si.UpdateCount("dirs")
// Add or update the directory in the map
@@ -154,14 +155,21 @@ func (si *Index) InsertDirs(path string) {
}
}
func makeIndexPath(path string, root string) string {
if path == root {
func (si *Index) makeIndexPath(subPath string, isDir bool) string {
if si.Root == subPath {
return "/"
}
adjustedPath := strings.TrimPrefix(path, root+"/")
// clean path
subPath = strings.TrimSuffix(subPath, "/")
// remove index prefix
adjustedPath := strings.TrimPrefix(subPath, si.Root)
// remove trailing slash
adjustedPath = strings.TrimSuffix(adjustedPath, "/")
// add leading slash for root of index
if adjustedPath == "" {
adjustedPath = "/"
} else if !isDir {
adjustedPath = filepath.Dir(adjustedPath)
}
return adjustedPath
}
+1 -1
View File
@@ -65,7 +65,7 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
if count > maxSearchResults {
break
}
fullName := pathName + file
fullName := strings.TrimLeft(pathName+file, "/")
fileTypes := map[string]bool{}
matches, fileType := containsSearchTerm(fullName, searchTerm, *searchOptions, isDir, fileTypes)
+10 -30
View File
@@ -8,43 +8,25 @@ import (
"github.com/gtsteffaniak/filebrowser/settings"
)
// GetFileMetadata retrieves the FileInfo from the specified directory in the index.
func (si *Index) GetFileMetadata(adjustedPath string) (FileInfo, bool) {
si.mu.RLock()
dir, exists := si.Directories[adjustedPath]
si.mu.RUnlock()
if exists {
// Initialize the Metadata map if it is nil
if dir.Metadata == nil {
dir.Metadata = make(map[string]FileInfo)
si.SetDirectoryInfo(adjustedPath, dir)
return FileInfo{}, false
} else {
return dir.Metadata[adjustedPath], true
}
}
return FileInfo{}, false
}
// UpdateFileMetadata updates the FileInfo for the specified directory in the index.
func (si *Index) UpdateFileMetadata(adjustedPath string, info FileInfo) bool {
si.mu.RLock()
si.mu.Lock()
defer si.mu.Unlock()
dir, exists := si.Directories[adjustedPath]
si.mu.RUnlock()
if exists {
if !exists || exists && dir.Metadata == nil {
// Initialize the Metadata map if it is nil
if dir.Metadata == nil {
dir.Metadata = make(map[string]FileInfo)
}
si.Directories[adjustedPath] = dir
// Release the read lock before calling SetFileMetadata
}
return si.SetFileMetadata(adjustedPath, info)
}
// SetFileMetadata sets the FileInfo for the specified directory in the index.
// internal use only
func (si *Index) SetFileMetadata(adjustedPath string, info FileInfo) bool {
si.mu.Lock()
defer si.mu.Unlock()
_, exists := si.Directories[adjustedPath]
if !exists {
return false
@@ -56,6 +38,7 @@ func (si *Index) SetFileMetadata(adjustedPath string, info FileInfo) bool {
// GetMetadataInfo retrieves the FileInfo from the specified directory in the index.
func (si *Index) GetMetadataInfo(adjustedPath string) (FileInfo, bool) {
fi := FileInfo{}
si.mu.RLock()
dir, exists := si.Directories[adjustedPath]
si.mu.RUnlock()
@@ -64,11 +47,11 @@ func (si *Index) GetMetadataInfo(adjustedPath string) (FileInfo, bool) {
if dir.Metadata == nil {
dir.Metadata = make(map[string]FileInfo)
si.SetDirectoryInfo(adjustedPath, dir)
} else {
fi = dir.Metadata[adjustedPath]
}
info, metadataExists := dir.Metadata[adjustedPath]
return info, metadataExists
}
return FileInfo{}, false
return fi, exists
}
// SetDirectoryInfo sets the directory information in the index.
@@ -83,10 +66,7 @@ func (si *Index) GetDirectoryInfo(adjustedPath string) (Directory, bool) {
si.mu.RLock()
dir, exists := si.Directories[adjustedPath]
si.mu.RUnlock()
if exists {
return dir, true
}
return Directory{}, false
return dir, exists
}
func (si *Index) RemoveDirectory(path string) {
+220
View File
@@ -0,0 +1,220 @@
package files
import (
"io/fs"
"os"
"testing"
"time"
)
// Mock for fs.FileInfo
type mockFileInfo struct {
name string
isDir bool
}
func (m mockFileInfo) Name() string { return m.name }
func (m mockFileInfo) Size() int64 { return 0 }
func (m mockFileInfo) Mode() os.FileMode { return 0 }
func (m mockFileInfo) ModTime() time.Time { return time.Now() }
func (m mockFileInfo) IsDir() bool { return m.isDir }
func (m mockFileInfo) Sys() interface{} { return nil }
var testIndex Index
// Test for GetFileMetadata
//func TestGetFileMetadata(t *testing.T) {
// t.Parallel()
// tests := []struct {
// name string
// adjustedPath string
// fileName string
// expectedName string
// expectedExists bool
// }{
// {
// name: "testpath exists",
// adjustedPath: "/testpath",
// fileName: "testfile.txt",
// expectedName: "testfile.txt",
// expectedExists: true,
// },
// {
// name: "testpath not exists",
// adjustedPath: "/testpath",
// fileName: "nonexistent.txt",
// expectedName: "",
// expectedExists: false,
// },
// {
// name: "File exists in /anotherpath",
// adjustedPath: "/anotherpath",
// fileName: "afile.txt",
// expectedName: "afile.txt",
// expectedExists: true,
// },
// {
// name: "File does not exist in /anotherpath",
// adjustedPath: "/anotherpath",
// fileName: "nonexistentfile.txt",
// expectedName: "",
// expectedExists: false,
// },
// {
// name: "Directory does not exist",
// adjustedPath: "/nonexistentpath",
// fileName: "testfile.txt",
// expectedName: "",
// expectedExists: false,
// },
// }
//
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// fileInfo, exists := testIndex.GetFileMetadata(tt.adjustedPath)
// if exists != tt.expectedExists || fileInfo.Name != tt.expectedName {
// t.Errorf("expected %v:%v but got: %v:%v", tt.expectedName, tt.expectedExists, //fileInfo.Name, exists)
// }
// })
// }
//}
// Test for UpdateFileMetadata
func TestUpdateFileMetadata(t *testing.T) {
index := &Index{
Directories: map[string]Directory{
"/testpath": {
Metadata: map[string]FileInfo{
"testfile.txt": {Name: "testfile.txt"},
"anotherfile.txt": {Name: "anotherfile.txt"},
},
},
},
}
info := FileInfo{Name: "testfile.txt"}
success := index.UpdateFileMetadata("/testpath", info)
if !success {
t.Fatalf("expected UpdateFileMetadata to succeed")
}
dir, exists := index.Directories["/testpath"]
if !exists || dir.Metadata["testfile.txt"].Name != "testfile.txt" {
t.Fatalf("expected testfile.txt to be updated in the directory metadata")
}
}
// Test for GetDirMetadata
func TestGetDirMetadata(t *testing.T) {
t.Parallel()
_, exists := testIndex.GetMetadataInfo("/testpath")
if !exists {
t.Fatalf("expected GetDirMetadata to return initialized metadata map")
}
_, exists = testIndex.GetMetadataInfo("/nonexistent")
if exists {
t.Fatalf("expected GetDirMetadata to return false for nonexistent directory")
}
}
// Test for SetDirectoryInfo
func TestSetDirectoryInfo(t *testing.T) {
index := &Index{
Directories: map[string]Directory{
"/testpath": {
Metadata: map[string]FileInfo{
"testfile.txt": {Name: "testfile.txt"},
"anotherfile.txt": {Name: "anotherfile.txt"},
},
},
},
}
dir := Directory{Metadata: map[string]FileInfo{"testfile.txt": {Name: "testfile.txt"}}}
index.SetDirectoryInfo("/newPath", dir)
storedDir, exists := index.Directories["/newPath"]
if !exists || storedDir.Metadata["testfile.txt"].Name != "testfile.txt" {
t.Fatalf("expected SetDirectoryInfo to store directory info correctly")
}
}
// Test for GetDirectoryInfo
func TestGetDirectoryInfo(t *testing.T) {
t.Parallel()
dir, exists := testIndex.GetDirectoryInfo("/testpath")
if !exists || dir.Metadata["testfile.txt"].Name != "testfile.txt" {
t.Fatalf("expected GetDirectoryInfo to return correct directory info")
}
_, exists = testIndex.GetDirectoryInfo("/nonexistent")
if exists {
t.Fatalf("expected GetDirectoryInfo to return false for nonexistent directory")
}
}
// Test for RemoveDirectory
func TestRemoveDirectory(t *testing.T) {
index := &Index{
Directories: map[string]Directory{
"/testpath": {},
},
}
index.RemoveDirectory("/testpath")
_, exists := index.Directories["/testpath"]
if exists {
t.Fatalf("expected directory to be removed")
}
}
// Test for UpdateCount
func TestUpdateCount(t *testing.T) {
index := &Index{}
index.UpdateCount("files")
if index.NumFiles != 1 {
t.Fatalf("expected NumFiles to be 1 after UpdateCount('files')")
}
if index.NumFiles != 1 {
t.Fatalf("expected NumFiles to be 1 after UpdateCount('files')")
}
index.UpdateCount("dirs")
if index.NumDirs != 1 {
t.Fatalf("expected NumDirs to be 1 after UpdateCount('dirs')")
}
index.UpdateCount("unknown")
// Just ensure it does not panic or update any counters
if index.NumFiles != 1 || index.NumDirs != 1 {
t.Fatalf("expected counts to remain unchanged for unknown type")
}
index.resetCount()
if index.NumFiles != 0 || index.NumDirs != 0 || !index.inProgress {
t.Fatalf("expected resetCount to reset counts and set inProgress to true")
}
}
func init() {
testIndex = Index{
NumFiles: 10,
NumDirs: 5,
inProgress: false,
Directories: map[string]Directory{
"/testpath": {
Metadata: map[string]FileInfo{
"testfile.txt": {Name: "testfile.txt"},
"anotherfile.txt": {Name: "anotherfile.txt"},
},
},
"/anotherpath": {
Metadata: map[string]FileInfo{
"afile.txt": {Name: "afile.txt"},
},
},
},
}
files := []fs.FileInfo{
mockFileInfo{name: "file1.txt", isDir: false},
mockFileInfo{name: "dir1", isDir: true},
}
testIndex.UpdateQuickList(files)
}
+10 -10
View File
@@ -2,23 +2,23 @@ package fileutils
import (
"os"
"path"
"github.com/spf13/afero"
"path/filepath"
)
// Copy copies a file or folder from one place to another.
func Copy(fs afero.Fs, src, dst string) error {
if src = path.Clean("/" + src); src == "" {
func Copy(src, dst string) error {
src = filepath.Clean(src)
if src == "" {
return os.ErrNotExist
}
if dst = path.Clean("/" + dst); dst == "" {
dst = filepath.Clean(dst)
if dst == "" {
return os.ErrNotExist
}
if src == "/" || dst == "/" {
// Prohibit copying from or to the virtual root directory.
// Prohibit copying from or to the root directory.
return os.ErrInvalid
}
@@ -26,14 +26,14 @@ func Copy(fs afero.Fs, src, dst string) error {
return os.ErrInvalid
}
info, err := fs.Stat(src)
info, err := os.Stat(src)
if err != nil {
return err
}
if info.IsDir() {
return CopyDir(fs, src, dst)
return CopyDir(src, dst)
}
return CopyFile(fs, src, dst)
return CopyFile(src, dst)
}
+15 -10
View File
@@ -2,27 +2,32 @@ package fileutils
import (
"errors"
"github.com/spf13/afero"
"os"
"path/filepath"
)
// CopyDir copies a directory from source to dest and all
// of its sub-directories. It doesn't stop if it finds an error
// during the copy. Returns an error if any.
func CopyDir(fs afero.Fs, source, dest string) error {
func CopyDir(source, dest string) error {
// Get properties of source.
srcinfo, err := fs.Stat(source)
srcinfo, err := os.Stat(source)
if err != nil {
return err
}
// Create the destination directory.
err = fs.MkdirAll(dest, srcinfo.Mode())
err = os.MkdirAll(dest, srcinfo.Mode())
if err != nil {
return err
}
dir, _ := fs.Open(source)
dir, err := os.Open(source)
if err != nil {
return err
}
defer dir.Close()
obs, err := dir.Readdir(-1)
if err != nil {
return err
@@ -31,18 +36,18 @@ func CopyDir(fs afero.Fs, source, dest string) error {
var errs []error
for _, obj := range obs {
fsource := source + "/" + obj.Name()
fdest := dest + "/" + obj.Name()
fsource := filepath.Join(source, obj.Name())
fdest := filepath.Join(dest, obj.Name())
if obj.IsDir() {
// Create sub-directories, recursively.
err = CopyDir(fs, fsource, fdest)
err = CopyDir(fsource, fdest)
if err != nil {
errs = append(errs, err)
}
} else {
// Perform the file copy.
err = CopyFile(fs, fsource, fdest)
err = CopyFile(fsource, fdest)
if err != nil {
errs = append(errs, err)
}
+24 -38
View File
@@ -5,24 +5,22 @@ import (
"os"
"path"
"path/filepath"
"github.com/spf13/afero"
)
// MoveFile moves file from src to dst.
// By default the rename filesystem system call is used. If src and dst point to different volumes
// the file copy is used as a fallback
func MoveFile(fs afero.Fs, src, dst string) error {
if fs.Rename(src, dst) == nil {
// MoveFile moves a file from src to dst.
// By default, the rename system call is used. If src and dst point to different volumes,
// the file copy is used as a fallback.
func MoveFile(src, dst string) error {
if os.Rename(src, dst) == nil {
return nil
}
// fallback
err := CopyFile(fs, src, dst)
err := CopyFile(src, dst)
if err != nil {
_ = fs.Remove(dst)
_ = os.Remove(dst)
return err
}
if err := fs.Remove(src); err != nil {
if err := os.Remove(src); err != nil {
return err
}
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
// an error if any.
func CopyFile(fs afero.Fs, source, dest string) error {
func CopyFile(source, dest string) error {
// Open the source file.
src, err := fs.Open(source)
src, err := os.Open(source)
if err != nil {
return err
}
defer src.Close()
// Makes the directory needed to create the dst
// file.
err = fs.MkdirAll(filepath.Dir(dest), 0666) //nolint:gomnd
// Makes the directory needed to create the dst file.
err = os.MkdirAll(filepath.Dir(dest), 0775) //nolint:gomnd
if err != nil {
return err
}
// 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 {
return err
}
@@ -58,12 +55,12 @@ func CopyFile(fs afero.Fs, source, dest string) error {
return err
}
// Copy the mode
info, err := fs.Stat(source)
// Copy the mode.
info, err := os.Stat(source)
if err != nil {
return err
}
err = fs.Chmod(dest, info.Mode())
err = os.Chmod(dest, info.Mode())
if err != nil {
return err
}
@@ -71,7 +68,7 @@ func CopyFile(fs afero.Fs, source, dest string) error {
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 {
// Handle special cases.
switch len(paths) {
@@ -81,30 +78,19 @@ func CommonPrefix(sep byte, paths ...string) string {
return path.Clean(paths[0])
}
// Note, we treat string as []byte, not []rune as is often
// done in Go. (And sep as byte, not rune). This is because
// most/all supported OS' treat paths as string of non-zero
// bytes. A filename may be displayed as a sequence of Unicode
// runes (typically encoded as UTF-8) but paths are
// not required to be valid UTF-8 or in any normalized form
// (e.g. "é" (U+00C9) and "é" (U+0065,U+0301) are different
// file names.
// Treat string as []byte, not []rune as is often done in Go.
c := []byte(path.Clean(paths[0]))
// We add a trailing sep to handle the case where the
// common prefix directory is included in the path list
// (e.g. /home/user1, /home/user1/foo, /home/user1/bar).
// path.Clean will have cleaned off trailing / separators with
// the exception of the root directory, "/" (in which case we
// make it "//", but this will get fixed up to "/" bellow).
// Add a trailing sep to handle the case where the common prefix directory
// is included in the path list.
c = append(c, sep)
// Ignore the first path since it's already in c
// Ignore the first path since it's already in c.
for _, v := range paths[1:] {
// Clean up each path before testing it
// Clean up each path before testing it.
v = path.Clean(v) + string(sep)
// Find the first non-common byte and truncate c
// Find the first non-common byte and truncate c.
if len(v) < len(c) {
c = c[:len(v)]
}
@@ -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-- {
if c[i] == sep {
c = c[:i]
+6
View File
@@ -0,0 +1,6 @@
{
"name": "frontend",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+30 -30
View File
@@ -1,56 +1,56 @@
module github.com/gtsteffaniak/filebrowser
go 1.21
go 1.22.5
require (
github.com/asdine/storm/v3 v3.2.1
github.com/disintegration/imaging v1.6.2
github.com/dsoprea/go-exif/v3 v3.0.1
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568
github.com/goccy/go-yaml v1.11.0
github.com/goccy/go-yaml v1.12.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/go-cmp v0.5.9
github.com/gorilla/mux v1.8.0
github.com/google/go-cmp v0.6.0
github.com/gorilla/mux v1.8.1
github.com/marusama/semaphore/v2 v2.5.0
github.com/mholt/archiver/v3 v3.5.1
github.com/shirou/gopsutil/v3 v3.23.8
github.com/spf13/afero v1.9.5
github.com/spf13/cobra v1.7.0
github.com/shirou/gopsutil/v3 v3.24.5
github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.4
github.com/stretchr/testify v1.9.0
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
golang.org/x/crypto v0.17.0
golang.org/x/image v0.12.0
golang.org/x/text v0.14.0
golang.org/x/crypto v0.26.0
golang.org/x/image v0.19.0
golang.org/x/text v0.17.0
)
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/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // 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/fatih/color v1.10.0 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
github.com/golang/snappy v0.0.2 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.11.4 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/nwaples/rardecode v1.1.0 // indirect
github.com/pierrec/lz4/v4 v4.1.2 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/ulikunitz/xz v0.5.9 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.etcd.io/bbolt v1.3.4 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/bbolt v1.3.11 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+69 -489
View File
@@ -1,61 +1,13 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
@@ -80,122 +32,59 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003142440-7a1927d49d9d/go.mod h1:LV
github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU=
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw=
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
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.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
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/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/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/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54=
github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng=
github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM=
github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -203,413 +92,104 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/marusama/semaphore/v2 v2.5.0 h1:o/1QJD9DBYOWRnDhPwDVAXQn6mQYD0gZaS1Tpx6DJGM=
github.com/marusama/semaphore/v2 v2.5.0/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM=
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/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
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/shirou/gopsutil/v3 v3.23.8 h1:xnATPiybo6GgdRoC4YoGnxXZFRc3dqQTGi73oLvvBrE=
github.com/shirou/gopsutil/v3 v3.23.8/go.mod h1:7hmCaBn+2ZwaZOr6jmPBZDfawwMGuo1id3C6aM8EDqQ=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
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/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
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/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
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.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/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-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/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.1-0.20180807135948-17ff2d5776d2/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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/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-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
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.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+5 -9
View File
@@ -2,7 +2,6 @@ package http
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
@@ -127,11 +126,10 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
return http.StatusBadRequest, nil
}
user := &users.User{
Username: info.Username,
Password: info.Password,
}
settings.Config.UserDefaults.Apply(user)
user := users.ApplyDefaults(users.User{})
user.Username = info.Username
user.Password = info.Password
userHome, err := d.settings.MakeUserDir(user.Username, user.Scope, d.server.Root)
if err != nil {
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
@@ -139,8 +137,7 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
}
user.Scope = userHome
log.Printf("new user: %s, home dir: [%s].", user.Username, userHome)
settings.Config.UserDefaults.Apply(user)
err = d.store.Users.Save(user)
err = d.store.Users.Save(&user)
if err == errors.ErrExist {
return http.StatusConflict, err
} else if err != nil {
@@ -157,7 +154,6 @@ var renewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data
func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (int, error) {
duration, err := time.ParseDuration(settings.Config.Auth.TokenExpirationTime)
if err != nil {
fmt.Println("Error parsing duration:", err)
duration = time.Hour * 2
}
claims := &authToken{
-1
View File
@@ -33,7 +33,6 @@ func (d *data) Check(path string) bool {
allow = rule.Allow
}
}
for _, rule := range d.user.Rules {
if rule.Matches(path) {
allow = rule.Allow
+1
View File
@@ -69,6 +69,7 @@ func NewHandler(
Handler(monkey(previewHandler(imgSvc, fileCache, server.EnableThumbnails, server.ResizePreview), "/api/preview")).Methods("GET")
api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET")
public := api.PathPrefix("/public").Subrouter()
public.Handle("/publicUser", monkey(publicUserGetHandler, "")).Methods("GET")
public.PathPrefix("/dl").Handler(monkey(publicDlHandler, "/api/public/dl/")).Methods("GET")
public.PathPrefix("/share").Handler(monkey(publicShareHandler, "/api/public/share/")).Methods("GET")
return stripPrefix(server.BaseURL, r), nil
+2 -2
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"os"
"github.com/gorilla/mux"
@@ -46,7 +47,6 @@ func previewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, re
}
file, err := files.FileInfoFaster(files.FileOptions{
Fs: d.user.Fs,
Path: "/" + vars["path"],
Modify: d.user.Perm.Modify,
Expand: true,
@@ -110,7 +110,7 @@ func handleImagePreview(
func createPreview(imgSvc ImgService, fileCache FileCache,
file *files.FileInfo, previewSize PreviewSize) ([]byte, error) {
fd, err := file.Fs.Open(file.Path)
fd, err := os.Open(file.Path)
if err != nil {
return nil, err
}
+34 -58
View File
@@ -2,45 +2,44 @@ package http
import (
"errors"
"fmt"
"net/http"
"net/url"
"path"
"path/filepath"
"strings"
"github.com/spf13/afero"
"golang.org/x/crypto/bcrypt"
"github.com/gtsteffaniak/filebrowser/files"
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/share"
"github.com/gtsteffaniak/filebrowser/users"
)
var withHashFile = func(fn handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
id, ifPath := ifPathWithName(r)
id, path := ifPathWithName(r)
link, err := d.store.Share.GetByHash(id)
if err != nil {
return errToStatus(err), err
}
status, err := authenticateShareRequest(r, link)
if status != 0 || err != nil {
return status, err
if link.Hash != "" {
var status int
status, err = authenticateShareRequest(r, link)
if err != nil || status != 0 {
return status, err
}
}
user, err := d.store.Users.Get(d.server.Root, link.UserID)
d.user = &users.PublicUser
realPath, isDir, err := files.GetRealPath(d.user.Scope, link.Path, path)
if err != nil {
return errToStatus(err), err
return http.StatusNotFound, err
}
d.user = user
file, err := files.FileInfoFaster(files.FileOptions{
Fs: d.user.Fs,
Path: link.Path,
Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify,
Expand: false,
Expand: true,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
Token: link.Token,
@@ -48,66 +47,44 @@ var withHashFile = func(fn handleFunc) handleFunc {
if err != nil {
return errToStatus(err), err
}
// share base path
basePath := link.Path
// file relative path
filePath := ""
if file.IsDir {
basePath = filepath.Dir(basePath)
filePath = ifPath
}
// set fs root to the shared file/folder
d.user.Fs = afero.NewBasePathFs(d.user.Fs, basePath)
file, err = files.FileInfoFaster(files.FileOptions{
Fs: d.user.Fs,
Path: filePath,
Modify: d.user.Perm.Modify,
Expand: true,
Checker: d,
Token: link.Token,
})
if err != nil {
return errToStatus(err), err
}
d.raw = file
return fn(w, r, d)
}
}
// ref to https://github.com/filebrowser/filebrowser/pull/727
// `/api/public/dl/MEEuZK-v/file-name.txt` for old browsers to save file with correct name
func ifPathWithName(r *http.Request) (id, filePath string) {
pathElements := strings.Split(r.URL.Path, "/")
// prevent maliciously constructed parameters like `/api/public/dl/XZzCDnK2_not_exists_hash_name`
// len(pathElements) will be 1, and golang will panic `runtime error: index out of range`
switch len(pathElements) {
case 1:
return r.URL.Path, "/"
default:
return pathElements[0], path.Join("/", path.Join(pathElements[1:]...))
}
id = pathElements[0]
allButFirst := path.Join(pathElements[1:]...)
return id, allButFirst
}
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)
if file.IsDir {
file.Listing.Sorting = users.Sorting{By: "name", Asc: false}
return renderJSON(w, r, file)
}
return renderJSON(w, r, file)
})
var publicUserGetHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
// Call the actual handler logic here (e.g., renderJSON, etc.)
// You may need to replace `fn` with the actual handler logic.
return renderJSON(w, r, users.PublicUser)
}
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 {
return rawFileHandler(w, r, file)
}
@@ -138,7 +115,6 @@ func authenticateShareRequest(r *http.Request, l *share.Link) (int, error) {
}
return 0, err
}
return 0, nil
}
+1 -10
View File
@@ -8,7 +8,6 @@ import (
"testing"
"github.com/asdine/storm/v3"
"github.com/spf13/afero"
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/share"
@@ -26,7 +25,7 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
expectedStatusCode int
}{
"Public share, no auth required": {
share: &share.Link{Hash: "h", UserID: 1},
share: &share.Link{Hash: "h"},
req: newHTTPRequest(t),
expectedStatusCode: 200,
},
@@ -82,9 +81,6 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
if err := storage.Share.Save(tc.share); err != nil {
t.Fatalf("failed to save share: %v", err)
}
if err := storage.Users.Save(&users.User{Username: "username", Password: "pw"}); err != nil {
t.Fatalf("failed to save user: %v", err)
}
if err := storage.Settings.Save(&settings.Settings{
Auth: settings.Auth{
Key: []byte("key"),
@@ -95,12 +91,10 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
storage.Users = &customFSUser{
Store: storage.Users,
fs: &afero.MemMapFs{},
}
recorder := httptest.NewRecorder()
handler := handle(handler, "", storage, &settings.Server{})
handler.ServeHTTP(recorder, tc.req)
result := recorder.Result()
defer result.Body.Close()
@@ -126,7 +120,6 @@ func newHTTPRequest(t *testing.T, requestModifiers ...func(*http.Request)) *http
type customFSUser struct {
users.Store
fs afero.Fs
}
func (cu *customFSUser) Get(baseScope string, id interface{}) (*users.User, error) {
@@ -134,7 +127,5 @@ func (cu *customFSUser) Get(baseScope string, id interface{}) (*users.User, erro
if err != nil {
return nil, err
}
user.Fs = cu.fs
return user, nil
}
+10 -8
View File
@@ -5,6 +5,7 @@ import (
"log"
"net/http"
"net/url"
"os"
gopath "path"
"path/filepath"
"strings"
@@ -80,10 +81,13 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
if !d.user.Perm.Download {
return http.StatusAccepted, nil
}
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil {
return http.StatusInternalServerError, err
}
file, err := files.FileInfoFaster(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: d.server.TypeDetectionByHeader,
@@ -92,7 +96,6 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
if err != nil {
return errToStatus(err), err
}
if files.IsNamedPipe(file.Mode) {
setContentDisposition(w, r, file)
return 0, nil
@@ -109,8 +112,7 @@ func addFile(ar archiver.Writer, d *data, path, commonPath string) error {
if !d.Check(path) {
return nil
}
info, err := d.user.Fs.Stat(path)
info, err := os.Stat(path)
if err != nil {
return err
}
@@ -119,7 +121,7 @@ func addFile(ar archiver.Writer, d *data, path, commonPath string) error {
return nil
}
file, err := d.user.Fs.Open(path)
file, err := os.Open(path)
if err != nil {
return err
}
@@ -200,7 +202,7 @@ func rawDirHandler(w http.ResponseWriter, r *http.Request, d *data, file *files.
}
func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo) (int, error) {
fd, err := file.Fs.Open(file.Path)
fd, err := os.Open(file.Path)
if err != nil {
return http.StatusInternalServerError, err
}
+75 -131
View File
@@ -3,7 +3,6 @@ package http
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
@@ -12,7 +11,6 @@ import (
"strings"
"github.com/shirou/gopsutil/v3/disk"
"github.com/spf13/afero"
"github.com/gtsteffaniak/filebrowser/errors"
"github.com/gtsteffaniak/filebrowser/files"
@@ -20,31 +18,32 @@ import (
)
var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil {
return http.StatusNotFound, err
}
file, err := files.FileInfoFaster(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify,
Expand: true,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
Content: true,
Content: r.URL.Query().Get("content") == "true",
})
if err != nil {
return errToStatus(err), err
}
if file.IsDir {
file.Listing.Sorting = d.user.Sorting
return renderJSON(w, r, file)
}
if checksum := r.URL.Query().Get("checksum"); checksum != "" {
err := file.Checksum(checksum)
if err == errors.ErrInvalidOption {
return http.StatusBadRequest, nil
} else if err != nil {
return http.StatusInternalServerError, err
if !file.IsDir {
if checksum := r.URL.Query().Get("checksum"); checksum != "" {
err := file.Checksum(checksum)
if err == errors.ErrInvalidOption {
return http.StatusBadRequest, nil
} else if err != nil {
return http.StatusInternalServerError, err
}
}
}
return renderJSON(w, r, file)
})
@@ -53,15 +52,19 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
if r.URL.Path == "/" || !d.user.Perm.Delete {
return http.StatusForbidden, nil
}
file, err := files.FileInfoFaster(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil {
return http.StatusNotFound, err
}
fileOpts := files.FileOptions{
Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
})
}
file, err := files.FileInfoFaster(fileOpts)
if err != nil {
return errToStatus(err), err
}
@@ -72,14 +75,10 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
return errToStatus(err), err
}
err = d.RunHook(func() error {
return d.user.Fs.RemoveAll(r.URL.Path)
}, "delete", r.URL.Path, "", d.user)
err = files.DeleteFiles(realPath, fileOpts)
if err != nil {
return errToStatus(err), err
}
return http.StatusOK, nil
})
}
@@ -89,21 +88,27 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
return http.StatusForbidden, nil
}
// Directories creation on POST.
if strings.HasSuffix(r.URL.Path, "/") {
err := d.user.Fs.MkdirAll(r.URL.Path, 0775) //nolint:gomnd
return errToStatus(err), err
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil {
return http.StatusNotFound, err
}
file, err := files.FileInfoFaster(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
fileOpts := files.FileOptions{
Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
})
}
// Directories creation on POST.
if strings.HasSuffix(r.URL.Path, "/") {
err = files.WriteDirectory(fileOpts) // Assign to the existing `err` variable
if err != nil {
return errToStatus(err), err
}
return http.StatusOK, nil
}
file, err := files.FileInfoFaster(fileOpts)
if err == nil {
if r.URL.Query().Get("override") != "true" {
return http.StatusConflict, nil
@@ -119,22 +124,7 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
return errToStatus(err), err
}
}
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)
}
err = files.WriteFile(fileOpts, r.Body)
return errToStatus(err), err
})
}
@@ -149,28 +139,23 @@ var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
return http.StatusMethodNotAllowed, nil
}
exists, err := afero.Exists(d.user.Fs, r.URL.Path)
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil {
return http.StatusInternalServerError, err
return http.StatusNotFound, err
}
if !exists {
return http.StatusNotFound, nil
fileOpts := files.FileOptions{
Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
}
err = d.RunHook(func() error {
info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body)
if writeErr != nil {
return writeErr
}
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
w.Header().Set("ETag", etag)
return nil
}, "save", r.URL.Path, "", d.user)
err = files.WriteFile(fileOpts, r.Body)
return errToStatus(err), err
})
// TODO fix and verify this function still works in tests
func resourcePatchHandler(fileCache FileCache) handleFunc {
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
src := r.URL.Path
@@ -186,28 +171,20 @@ func resourcePatchHandler(fileCache FileCache) handleFunc {
if dst == "/" || src == "/" {
return http.StatusForbidden, nil
}
err = checkParent(src, dst)
if err != nil {
return http.StatusBadRequest, err
}
override := r.URL.Query().Get("override") == "true"
rename := r.URL.Query().Get("rename") == "true"
if !override && !rename {
if _, err = d.user.Fs.Stat(dst); err == nil {
if _, err = os.Stat(dst); err == nil {
return http.StatusConflict, nil
}
}
if rename {
dst = addVersionSuffix(dst, d.user.Fs)
dst = addVersionSuffix(dst)
}
// Permission for overwriting the file
if override && !d.user.Perm.Modify {
return http.StatusForbidden, nil
}
err = d.RunHook(func() error {
return patchAction(r.Context(), action, src, dst, d, fileCache)
}, action, src, dst, d.user)
@@ -216,65 +193,22 @@ func resourcePatchHandler(fileCache FileCache) handleFunc {
})
}
func checkParent(src, dst string) error {
rel, err := filepath.Rel(src, dst)
if err != nil {
return err
}
rel = filepath.ToSlash(rel)
if !strings.HasPrefix(rel, "../") && rel != ".." && rel != "." {
return errors.ErrSourceIsParent
}
return nil
}
func addVersionSuffix(source string, fs afero.Fs) string {
func addVersionSuffix(source string) string {
counter := 1
dir, name := path.Split(source)
ext := filepath.Ext(name)
base := strings.TrimSuffix(name, ext)
for {
if _, err := fs.Stat(source); err != nil {
if _, err := os.Stat(source); err != nil {
break
}
renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext)
source = path.Join(dir, renamed)
counter++
}
return source
}
func writeFile(fs afero.Fs, dst string, in io.Reader) (os.FileInfo, error) {
dir, _ := path.Split(dst)
err := fs.MkdirAll(dir, 0775) //nolint:gomnd
if err != nil {
return nil, err
}
file, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) //nolint:gomnd
if err != nil {
return nil, err
}
defer file.Close()
_, err = io.Copy(file, in)
if err != nil {
return nil, err
}
// Gets the info about the file.
info, err := file.Stat()
if err != nil {
return nil, err
}
return info, nil
}
func delThumbs(ctx context.Context, fileCache FileCache, file *files.FileInfo) error {
for _, previewSizeName := range PreviewSizeNames() {
size, _ := ParsePreviewSize(previewSizeName)
@@ -294,17 +228,24 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
return errors.ErrPermissionDenied
}
return fileutils.Copy(d.user.Fs, src, dst)
return fileutils.Copy(src, dst)
case "rename":
if !d.user.Perm.Rename {
return errors.ErrPermissionDenied
}
src = path.Clean("/" + src)
dst = path.Clean("/" + dst)
realDest, _, err := files.GetRealPath(d.user.Scope, dst)
if err != nil {
return err
}
realSrc, isDir, err := files.GetRealPath(d.user.Scope, src)
if err != nil {
return err
}
file, err := files.FileInfoFaster(files.FileOptions{
Fs: d.user.Fs,
Path: src,
Path: realSrc,
IsDir: isDir,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: false,
@@ -320,7 +261,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
return err
}
return fileutils.MoveFile(d.user.Fs, src, dst)
return fileutils.MoveFile(realSrc, realDest)
default:
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
}
@@ -332,9 +273,13 @@ type DiskUsageResponse struct {
}
var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil {
return http.StatusNotFound, err
}
file, err := files.FileInfoFaster(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: false,
@@ -350,7 +295,6 @@ var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (
Used: 0,
})
}
usage, err := disk.UsageWithContext(r.Context(), fPath)
if err != nil {
return errToStatus(err), err
+4 -8
View File
@@ -15,20 +15,17 @@ type settingsData struct {
Defaults settings.UserDefaults `json:"defaults"`
Rules []rules.Rule `json:"rules"`
Frontend settings.Frontend `json:"frontend"`
Shell []string `json:"shell"`
Commands map[string][]string `json:"commands"`
}
var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
data := &settingsData{
Signup: settings.Config.Auth.Signup,
CreateUserDir: settings.Config.Server.CreateUserDir,
UserHomeBasePath: settings.Config.Server.UserHomeBasePath,
Signup: d.settings.Auth.Signup,
CreateUserDir: d.settings.Server.CreateUserDir,
UserHomeBasePath: d.settings.Server.UserHomeBasePath,
Defaults: d.settings.UserDefaults,
Rules: d.settings.Rules,
Frontend: d.settings.Frontend,
Shell: d.settings.Shell,
Commands: d.settings.Commands,
}
return renderJSON(w, r, data)
@@ -46,8 +43,7 @@ var settingsPutHandler = withAdmin(func(w http.ResponseWriter, r *http.Request,
d.settings.UserDefaults = req.Defaults
d.settings.Rules = req.Rules
d.settings.Frontend = req.Frontend
d.settings.Shell = req.Shell
d.settings.Commands = req.Commands
d.settings.Auth.Signup = req.Signup
err = d.store.Settings.Save(d.settings)
return errToStatus(err), err
})
+10 -6
View File
@@ -22,7 +22,6 @@ func withPermShare(fn handleFunc) handleFunc {
if !d.user.Perm.Share {
return http.StatusForbidden, nil
}
return fn(w, r, d)
})
}
@@ -51,7 +50,6 @@ var shareListHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
}
return s[i].Expire < s[j].Expire
})
return renderJSON(w, r, s)
})
@@ -77,10 +75,15 @@ var shareDeleteHandler = withPermShare(func(w http.ResponseWriter, r *http.Reque
}
err := d.store.Share.Delete(hash)
if err != nil {
return errToStatus(err), err
}
return errToStatus(err), err
})
var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
var s *share.Link
var body share.CreateBody
if r.Body != nil {
@@ -126,7 +129,7 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
if err != nil {
return status, err
}
stringHash := ""
var token string
if len(hash) > 0 {
tokenBuffer := make([]byte, 24) //nolint:gomnd
@@ -134,14 +137,14 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
return http.StatusInternalServerError, err
}
token = base64.URLEncoding.EncodeToString(tokenBuffer)
stringHash = string(hash)
}
s = &share.Link{
Path: r.URL.Path,
Path: strings.TrimSuffix(r.URL.Path, "/"),
Hash: str,
Expire: expire,
UserID: d.user.ID,
PasswordHash: string(hash),
PasswordHash: stringHash,
Token: token,
}
@@ -153,6 +156,7 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
})
func getSharePasswordHash(body share.CreateBody) (data []byte, statuscode int, err error) {
if body.Password == "" {
return nil, 0, nil
}
+6 -2
View File
@@ -34,6 +34,7 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
"Color": d.settings.Frontend.Color,
"BaseURL": d.server.BaseURL,
"Version": version.Version,
"CommitSHA": version.CommitSHA,
"StaticURL": path.Join(d.server.BaseURL, "/static"),
"Signup": settings.Config.Auth.Signup,
"NoAuth": d.settings.Auth.Method == "noauth",
@@ -65,7 +66,10 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
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 {
data["ReCaptcha"] = auther.ReCaptcha.Key != "" && auther.ReCaptcha.Secret != ""
@@ -104,7 +108,7 @@ func getStaticHandlers(store *storage.Storage, server *settings.Server, assetsFs
}
w.Header().Set("x-xss-protection", "1; mode=block")
return handleWithStaticData(w, r, d, assetsFs, "index.html", "text/html; charset=utf-8")
return handleWithStaticData(w, r, d, assetsFs, "public/index.html", "text/html; charset=utf-8")
}, "", store, server)
static = handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
+29 -23
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"log"
"net/http"
"reflect"
"sort"
"strconv"
@@ -12,6 +13,7 @@ import (
"golang.org/x/text/language"
"github.com/gtsteffaniak/filebrowser/errors"
"github.com/gtsteffaniak/filebrowser/files"
"github.com/gtsteffaniak/filebrowser/users"
)
@@ -62,7 +64,6 @@ func withSelfOrAdmin(fn handleFunc) handleFunc {
if err != nil {
return http.StatusInternalServerError, err
}
if d.user.ID != id && !d.user.Perm.Admin {
return http.StatusForbidden, nil
}
@@ -129,15 +130,21 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
return http.StatusBadRequest, errors.ErrEmptyPassword
}
newUser := users.ApplyDefaults(*req.Data)
userHome, err := d.settings.MakeUserDir(req.Data.Username, req.Data.Scope, d.server.Root)
if err != nil {
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
return http.StatusInternalServerError, err
}
req.Data.Scope = userHome
newUser.Scope = userHome
log.Printf("user: %s, home dir: [%s].", req.Data.Username, userHome)
err = d.store.Users.Save(req.Data)
_, _, err = files.GetRealPath(d.server.Root, req.Data.Scope)
if err != nil {
log.Println("user path is not valid", req.Data.Scope)
return http.StatusBadRequest, nil
}
err = d.store.Users.Save(&newUser)
if err != nil {
return http.StatusInternalServerError, err
}
@@ -155,31 +162,31 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
if req.Data.ID != d.raw.(uint) {
return http.StatusBadRequest, nil
}
_, _, err = files.GetRealPath(d.server.Root, req.Data.Scope)
if err != nil {
return http.StatusBadRequest, nil
}
if len(req.Which) == 0 || (len(req.Which) == 1 && req.Which[0] == "all") {
if !d.user.Perm.Admin {
return http.StatusForbidden, nil
}
if req.Data.Password != "" {
req.Data.Password, err = users.HashPwd(req.Data.Password)
} else {
var suser *users.User
suser, err = d.store.Users.Get(d.server.Root, d.raw.(uint))
req.Data.Password = suser.Password
}
if err != nil {
return http.StatusInternalServerError, err
}
if len(req.Which) == 0 || req.Which[0] == "all" {
req.Which = []string{}
v := reflect.ValueOf(req.Data)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if field.Name == "Password" && req.Data.Password != "" {
req.Which = append(req.Which, field.Name)
} else if field.Name != "Password" && field.Name != "Fs" {
req.Which = append(req.Which, field.Name)
}
}
}
for k, v := range req.Which {
v = cases.Title(language.English, cases.NoLower).String(v)
req.Which[k] = v
if v == "Password" {
if !d.user.Perm.Admin && d.user.LockPassword {
return http.StatusForbidden, nil
@@ -196,7 +203,6 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
}
}
}
err = d.store.Users.Update(req.Data, req.Which...)
if err != nil {
return http.StatusInternalServerError, err
-1
View File
@@ -13,7 +13,6 @@ import (
func renderJSON(w http.ResponseWriter, _ *http.Request, data interface{}) (int, error) {
marsh, err := json.Marshal(data)
if err != nil {
return http.StatusInternalServerError, err
}
+1
View File
@@ -17,6 +17,7 @@ type Rule struct {
Allow bool `json:"allow"`
Path string `json:"path"`
Regexp *Regexp `json:"regexp"`
Id string `json:"id"`
}
// MatchHidden matches paths with a basename
+1 -1
View File
@@ -9,7 +9,7 @@ checkExit() {
if command -v go &> /dev/null
then
printf "\n == Running tests == \n"
go test -race -v ./...
go test -race -parallel -v ./...
checkExit
else
echo "ERROR: unable to perform tests"
+6 -5
View File
@@ -7,6 +7,7 @@ import (
"os/exec"
"strings"
"github.com/gtsteffaniak/filebrowser/files"
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/users"
)
@@ -19,8 +20,8 @@ type Runner struct {
// RunHook runs the hooks for the before and after event.
func (r *Runner) RunHook(fn func() error, evt, path, dst string, user *users.User) error {
path = user.FullPath(path)
dst = user.FullPath(dst)
path, _, _ = files.GetRealPath(user.Scope, path)
dst, _, _ = files.GetRealPath(user.Scope, dst)
if r.Enabled {
if val, ok := r.Commands["before_"+evt]; ok {
@@ -101,18 +102,18 @@ func (r *Runner) exec(raw, evt, path, dst string, user *users.User) error {
cmd.Stderr = os.Stderr
if !blocking {
log.Printf("[INFO] Nonblocking Command: \"%s\"", strings.Join(command, " "))
log.Printf("Nonblocking Command: \"%s\"", strings.Join(command, " "))
defer func() {
go func() {
err := cmd.Wait()
if err != nil {
log.Printf("[INFO] Nonblocking Command \"%s\" failed: %s", strings.Join(command, " "), err)
log.Printf("Nonblocking Command \"%s\" failed: %s", strings.Join(command, " "), err)
}
}()
}()
return cmd.Start()
}
log.Printf("[INFO] Blocking Command: \"%s\"", strings.Join(command, " "))
log.Printf("Blocking Command: \"%s\"", strings.Join(command, " "))
return cmd.Run()
}
+16 -20
View File
@@ -3,10 +3,9 @@ package settings
import (
"log"
"os"
"strings"
"path/filepath"
"github.com/goccy/go-yaml"
"github.com/gtsteffaniak/filebrowser/users"
)
var Config Settings
@@ -19,7 +18,16 @@ func Initialize(configFile string) {
log.Fatalf("Error unmarshaling YAML data: %v", err)
}
Config.UserDefaults.Perm = Config.UserDefaults.Permissions
Config.Server.Root = strings.TrimSuffix(Config.Server.Root, "/")
// Convert relative path to absolute path
realRoot, err := filepath.Abs(Config.Server.Root)
if err != nil {
log.Fatalf("Error getting root path: %v", err)
}
_, err = os.Stat(realRoot)
if err != nil {
log.Fatalf("ERROR: Configured Root Path does not exist! %v", err)
}
Config.Server.Root = realRoot
}
func loadConfigFile(configFile string) []byte {
@@ -52,12 +60,13 @@ func setDefaults() Settings {
ResizePreview: false,
EnableExec: false,
IndexingInterval: 5,
Port: 8080,
Port: 80,
NumImageProcessors: 4,
BaseURL: "",
Database: "database.db",
Log: "stdout",
Root: "/srv",
Indexing: true,
},
Auth: Auth{
TokenExpirationTime: "2h",
@@ -70,13 +79,15 @@ func setDefaults() Settings {
},
},
UserDefaults: UserDefaults{
StickySidebar: true,
Scope: ".",
LockPassword: false,
HideDotfiles: true,
DarkMode: false,
DisableSettings: false,
ViewMode: "normal",
Locale: "en",
Permissions: users.Permissions{
Permissions: Permissions{
Create: false,
Rename: false,
Modify: false,
@@ -88,18 +99,3 @@ func setDefaults() Settings {
},
}
}
// Apply applies the default options to a user.
func (d *UserDefaults) Apply(u *users.User) {
u.DisableSettings = d.DisableSettings
u.DarkMode = d.DarkMode
u.Scope = d.Scope
u.Locale = d.Locale
u.ViewMode = d.ViewMode
u.SingleClick = d.SingleClick
u.Perm = d.Perm
u.Sorting = d.Sorting
u.Commands = d.Commands
u.HideDotfiles = d.HideDotfiles
u.DateFormat = d.DateFormat
}
+4 -6
View File
@@ -6,16 +6,14 @@ import (
"log"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/spf13/afero"
)
var (
invalidFilenameChars = regexp.MustCompile(`[^0-9A-Za-z@_\-.]`)
dashes = regexp.MustCompile(`[\-]+`)
dashes = regexp.MustCompile(`[\-]+`)
)
// MakeUserDir makes the user directory according to settings.
@@ -32,8 +30,8 @@ func (s *Settings) MakeUserDir(username, userScope, serverRoot string) (string,
userScope = path.Join("/", userScope)
fs := afero.NewBasePathFs(afero.NewOsFs(), serverRoot)
if err := fs.MkdirAll(userScope, os.ModePerm); err != nil {
fullPath := filepath.Join(serverRoot, userScope)
if err := os.MkdirAll(fullPath, os.ModePerm); err != nil {
return "", fmt.Errorf("failed to create user home dir: [%s]: %w", userScope, err)
}
return userScope, nil
-4
View File
@@ -65,10 +65,6 @@ func (s *Storage) Save(set *Settings) error {
set.Rules = []rules.Rule{}
}
if set.Shell == nil {
set.Shell = []string{}
}
if set.Commands == nil {
set.Commands = map[string][]string{}
}
+19 -6
View File
@@ -2,7 +2,6 @@ package settings
import (
"github.com/gtsteffaniak/filebrowser/rules"
"github.com/gtsteffaniak/filebrowser/users"
)
type Settings struct {
@@ -54,6 +53,7 @@ type Server struct {
Root string `json:"root"`
UserHomeBasePath string `json:"userHomeBasePath"`
CreateUserDir bool `json:"createUserDir"`
Indexing bool `json:"indexing"`
}
type Frontend struct {
@@ -67,21 +67,34 @@ type Frontend struct {
// UserDefaults is a type that holds the default values
// for some fields on User.
type UserDefaults struct {
StickySidebar bool `json:"stickySidebar"`
DarkMode bool `json:"darkMode"`
LockPassword bool `json:"lockPassword"`
DisableSettings bool `json:"disableSettings,omitempty"`
Scope string `json:"scope"`
Locale string `json:"locale"`
ViewMode string `json:"viewMode"`
GallerySize int `json:"gallerySize"`
SingleClick bool `json:"singleClick"`
Rules []rules.Rule `json:"rules"`
Sorting struct {
By string `json:"by"`
Asc bool `json:"asc"`
} `json:"sorting"`
Perm users.Permissions `json:"perm"`
Permissions users.Permissions `json:"permissions"`
Commands []string `json:"commands,omitempty"`
HideDotfiles bool `json:"hideDotfiles"`
DateFormat bool `json:"dateFormat"`
Perm Permissions `json:"perm"`
Permissions Permissions `json:"permissions"`
Commands []string `json:"commands,omitempty"`
HideDotfiles bool `json:"hideDotfiles"`
DateFormat bool `json:"dateFormat"`
}
type Permissions struct {
Admin bool `json:"admin"`
Execute bool `json:"execute"`
Create bool `json:"create"`
Rename bool `json:"rename"`
Modify bool `json:"modify"`
Delete bool `json:"delete"`
Share bool `json:"share"`
Download bool `json:"download"`
}
+1 -1
View File
@@ -7,7 +7,7 @@ server:
enableThumbnails: false
resizePreview: true
typeDetectionByHeader: true
port: 8080
port: 80
baseURL: "/"
address: ""
log: "stdout"
+1 -1
View File
@@ -20,7 +20,7 @@ func (s settingsBackend) Save(set *settings.Settings) error {
func (s settingsBackend) GetServer() (*settings.Server, error) {
server := &settings.Server{
Port: 8080,
Port: 80,
NumImageProcessors: 1,
}
return server, get(s.db, "server", server)
+1 -5
View File
@@ -45,11 +45,9 @@ func (st usersBackend) Gets() ([]*users.User, error) {
if err == storm.ErrNotFound {
return nil, errors.ErrNotExist
}
if err != nil {
return allUsers, err
}
return allUsers, err
}
@@ -57,7 +55,6 @@ func (st usersBackend) Update(user *users.User, fields ...string) error {
if len(fields) == 0 {
return st.Save(user)
}
for _, field := range fields {
userField := reflect.ValueOf(user).Elem().FieldByName(field)
if !userField.IsValid() {
@@ -65,10 +62,9 @@ func (st usersBackend) Update(user *users.User, fields ...string) error {
}
val := userField.Interface()
if err := st.db.UpdateField(user, field, val); err != nil {
return err
return fmt.Errorf("Error updating user field: %s, error: %v", field, err.Error())
}
}
return nil
}
+47 -22
View File
@@ -1,11 +1,11 @@
package users
import (
"log"
"sync"
"time"
"github.com/gtsteffaniak/filebrowser/errors"
"github.com/gtsteffaniak/filebrowser/rules"
)
// StorageBackend is the interface to implement for a users storage.
@@ -18,6 +18,7 @@ type StorageBackend interface {
DeleteByUsername(string) error
}
// Store is an interface for user storage.
type Store interface {
Get(baseScope string, id interface{}) (user *User, err error)
Gets(baseScope string) ([]*User, error)
@@ -25,6 +26,8 @@ type Store interface {
Save(user *User) error
Delete(id interface{}) error
LastUpdate(id uint) int64
AddRule(username string, rule rules.Rule) error
DeleteRule(username string, ruleID string) error
}
// Storage is a users storage.
@@ -50,10 +53,7 @@ func (s *Storage) Get(baseScope string, id interface{}) (user *User, err error)
if err != nil {
return
}
if err := user.Clean(baseScope); err != nil {
return nil, err
}
return
return user, err
}
// Gets gets a list of all users.
@@ -62,24 +62,12 @@ func (s *Storage) Gets(baseScope string) ([]*User, error) {
if err != nil {
return nil, err
}
for _, user := range users {
if err := user.Clean(baseScope); err != nil { //nolint:govet
return nil, err
}
}
return users, err
}
// Update updates a user in the database.
func (s *Storage) Update(user *User, fields ...string) error {
err := user.Clean("")
if err != nil {
return err
}
err = s.back.Update(user, fields...)
err := s.back.Update(user, fields...)
if err != nil {
return err
}
@@ -90,13 +78,50 @@ func (s *Storage) Update(user *User, fields ...string) error {
return nil
}
// Save saves the user in a storage.
func (s *Storage) Save(user *User) error {
log.Println("Saving new user:", user.Username)
if err := user.Clean(""); err != nil {
// AddRule adds a rule to the user's rules list and updates the user in the database.
func (s *Storage) AddRule(userID string, rule rules.Rule) error {
user, err := s.Get("", userID)
if err != nil {
return err
}
user.Rules = append(user.Rules, rule)
err = s.Update(user, "Rules")
if err != nil {
return err
}
return nil
}
// DeleteRule deletes a rule specified by ID from the user's rules list and updates the user in the database.
func (s *Storage) DeleteRule(userID string, ruleID string) error {
user, err := s.Get("", userID)
if err != nil {
return err
}
// Find and remove the rule with the specified ID
var updatedRules []rules.Rule
for _, r := range user.Rules {
if r.Id != ruleID {
updatedRules = append(updatedRules, r)
}
}
user.Rules = updatedRules
err = s.Update(user, "Rules")
if err != nil {
return err
}
return nil
}
// Save saves the user in a storage.
func (s *Storage) Save(user *User) error {
return s.back.Save(user)
}
+53 -51
View File
@@ -1,25 +1,12 @@
package users
import (
"path/filepath"
"regexp"
"github.com/spf13/afero"
"github.com/gtsteffaniak/filebrowser/rules"
"github.com/gtsteffaniak/filebrowser/settings"
)
type Permissions struct {
Admin bool `json:"admin"`
Execute bool `json:"execute"`
Create bool `json:"create"`
Rename bool `json:"rename"`
Modify bool `json:"modify"`
Delete bool `json:"delete"`
Share bool `json:"share"`
Download bool `json:"download"`
}
// SortingSettings represents the sorting settings.
type Sorting struct {
By string `json:"by"`
@@ -28,23 +15,41 @@ type Sorting struct {
// User describes a user.
type User struct {
DarkMode bool `json:"darkMode"`
DisableSettings bool `json:"disableSettings"`
ID uint `storm:"id,increment" json:"id"`
Username string `storm:"unique" json:"username"`
Password string `json:"password"`
Scope string `json:"scope"`
Locale string `json:"locale"`
LockPassword bool `json:"lockPassword"`
ViewMode string `json:"viewMode"`
SingleClick bool `json:"singleClick"`
Perm Permissions `json:"perm"`
Commands []string `json:"commands"`
Sorting Sorting `json:"sorting"`
Fs afero.Fs `json:"-" yaml:"-"`
Rules []rules.Rule `json:"rules"`
HideDotfiles bool `json:"hideDotfiles"`
DateFormat bool `json:"dateFormat"`
StickySidebar bool `json:"stickySidebar"`
DarkMode bool `json:"darkMode"`
DisableSettings bool `json:"disableSettings"`
ID uint `storm:"id,increment" json:"id"`
Username string `storm:"unique" json:"username"`
Password string `json:"password"`
Scope string `json:"scope"`
Locale string `json:"locale"`
LockPassword bool `json:"lockPassword"`
ViewMode string `json:"viewMode"`
SingleClick bool `json:"singleClick"`
Perm settings.Permissions `json:"perm"`
Commands []string `json:"commands"`
Sorting Sorting `json:"sorting"`
Rules []rules.Rule `json:"rules"`
HideDotfiles bool `json:"hideDotfiles"`
DateFormat bool `json:"dateFormat"`
GallerySize int `json:"gallerySize"`
}
var PublicUser = User{
Username: "publicUser", // temp user not registered
Password: "publicUser", // temp user not registered
Scope: "./",
ViewMode: "normal",
LockPassword: true,
Perm: settings.Permissions{
Create: false,
Rename: false,
Modify: false,
Delete: false,
Share: true,
Download: true,
Admin: false,
},
}
// GetRules implements rules.Provider.
@@ -52,26 +57,6 @@ func (u *User) GetRules() []rules.Rule {
return u.Rules
}
// Clean cleans up a user and verifies if all its fields
// are alright to be saved.
//
//nolint:gocyclo
func (u *User) Clean(baseScope string) error {
if u.Fs == nil {
scope := u.Scope
scope = filepath.Join(baseScope, filepath.Join("/", scope)) //nolint:gocritic
u.Fs = afero.NewBasePathFs(afero.NewOsFs(), scope)
}
return nil
}
// FullPath gets the full path for a user's relative path.
func (u *User) FullPath(path string) string {
return afero.FullBaseFsPath(u.Fs.(*afero.BasePathFs), path)
}
// CanExecute checks if an user can execute a specific command.
func (u *User) CanExecute(command string) bool {
if !u.Perm.Execute {
@@ -86,3 +71,20 @@ func (u *User) CanExecute(command string) bool {
return false
}
// Apply applies the default options to a user.
func ApplyDefaults(u User) User {
u.StickySidebar = settings.Config.UserDefaults.StickySidebar
u.DisableSettings = settings.Config.UserDefaults.DisableSettings
u.DarkMode = settings.Config.UserDefaults.DarkMode
u.Scope = settings.Config.UserDefaults.Scope
u.Locale = settings.Config.UserDefaults.Locale
u.ViewMode = settings.Config.UserDefaults.ViewMode
u.SingleClick = settings.Config.UserDefaults.SingleClick
u.Perm = settings.Config.UserDefaults.Perm
u.Sorting = settings.Config.UserDefaults.Sorting
u.Commands = settings.Config.UserDefaults.Commands
u.HideDotfiles = settings.Config.UserDefaults.HideDotfiles
u.DateFormat = settings.Config.UserDefaults.DateFormat
return u
}
+3 -4
View File
@@ -1,8 +1,7 @@
package version
var (
// Version is the current File Browser version.
Version = "(0.2.3)"
// CommitSHA is the commmit sha.
CommitSHA = "(unknown)"
// Dynamically updated during build via build args
Version = "untracked"
CommitSHA = "untracked"
)
+6 -11
View File
@@ -11,6 +11,7 @@ server:
CreateUserDir: false
UserHomeBasePath: ""
indexingInterval: 5
indexing: true
numImageProcessors: 4
socket: ""
tlsKey: ""
@@ -18,7 +19,7 @@ server:
enableThumbnails: false
resizePreview: true
typeDetectionByHeader: true
port: 8080
port: 80
baseURL: "/"
address: ""
log: "stdout"
@@ -34,9 +35,7 @@ auth:
tokenExpirationTime: 2h
header: ""
method: json
command: ""
signup: false
shell: ""
frontend:
name: ""
disableExternal: false
@@ -62,7 +61,6 @@ userDefaults:
delete: true
share: true
download: true
commands: []
hideDotfiles: false
dateFormat: false
```
@@ -74,7 +72,7 @@ server:
enableThumbnails: true
enableExec: false
indexingInterval: 5
port: 8080
port: 80
numImageProcessors: 4
baseURL: ""
database: database.db
@@ -113,6 +111,8 @@ userDefaults:
- `indexingInterval`: This is the time in minutes the system waits before checking for filesystem changes. Default: `5`
- `indexing`: This enables or disables indexing. (Note: search will not work without indexing) Default: `true`
- `numImageProcessors`: This is the number of image processors available. Default: `4`
- `socket`: This is the socket configuration.
@@ -127,7 +127,7 @@ userDefaults:
- `typeDetectionByHeader`: This boolean value determines whether type detection is based on headers.
- `port`: This is the port number on which the server is running. Default: `8080`
- `port`: This is the port number on which the server is running. Default: `80`
- `baseURL`: This is the base URL for the server. Default: `""`
@@ -162,10 +162,6 @@ userDefaults:
- `oath` - oath authentication
- `noauth` - no authentication/login required.
- `command`: Deprecated: This is the authentication command.
- `shell`: This is the shell configuration.
- `Signup`: This boolean value indicates whether user signup is enabled on the login page. NOTE: Be mindful of `userDefaults` settings if enabled. Default: `false`
- `AdminUsername`: This is the username of the admin user. Default: `admin`
@@ -224,7 +220,6 @@ userDefaults:
- `download`: This boolean value determines whether download permissions are granted.
- `commands`: Deprecated: This is a list of commands.
- `hideDotfiles`: This boolean value determines whether dotfiles are hidden. (`true` or `false`)
+26
View File
@@ -0,0 +1,26 @@
{
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-typescript"
],
"rules": {
"vue/multi-word-component-names": "off",
"vue/no-mutating-props": [
"error",
{
"shallowOnly": true
}
]
// no-undef is already included in
// @vue/eslint-config-typescript
},
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
}
}
-16
View File
@@ -1,16 +0,0 @@
env:
browser: true
es2021: true
extends:
- eslint:recommended
- plugin:vue/vue3-essential
parserOptions:
ecmaVersion: latest
sourceType: module
plugins:
- vue
rules:
vue/multi-word-component-names: off
vue/no-reserved-component-names: warn
vue/no-mutating-props: off
vue/no-deprecated-v-bind-sync: warn
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
presets: ["@vue/app"],
};
+1
View File
@@ -0,0 +1 @@
frontend
-8652
View File
File diff suppressed because it is too large. Load diff
+31 -36
View File
@@ -1,51 +1,46 @@
{
"name": "filebrowser-frontend",
"version": "2.0.0",
"version": "3.0.0",
"private": true,
"type": "module",
"engines": {
"npm": ">=7.0.0",
"node": ">=18.0.0"
},
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"fix": "npx vue-cli-service lint",
"watch": "vue-cli-service build --watch",
"lint": "eslint --ext .vue,.js src/",
"lint:fix": "eslint --ext .vue,.js --fix src/",
"format": "prettier --write ."
"dev": "vite dev",
"build": "vite build && cp -R dist/ ../backend/cmd/",
"build-docker": "vite build",
"watch": "vite build --watch",
"typecheck": "vue-tsc -p ./tsconfig.json --noEmit",
"lint": "npm run typecheck && eslint src/",
"lint:fix": "eslint --fix src/",
"format": "prettier --write .",
"test": "npx playwright test"
},
"dependencies": {
"ace-builds": "^1.24.2",
"clipboard": "^2.0.4",
"css-vars-ponyfill": "^2.4.3",
"file-loader": "^6.2.0",
"js-base64": "^2.5.1",
"lodash.clonedeep": "^4.5.0",
"lodash.throttle": "^4.1.1",
"material-icons": "^1.10.5",
"moment": "^2.29.4",
"normalize.css": "^8.0.1",
"noty": "^3.2.0-beta",
"pretty-bytes": "^6.0.0",
"qrcode.vue": "^1.7.0",
"utif": "^3.1.0",
"vue": "^2.6.10",
"vue-async-computed": "^3.9.0",
"vue-i18n": "^8.15.3",
"vue-lazyload": "^1.3.3",
"vue-router": "^3.1.3",
"vue-simple-progress": "^1.1.1",
"vuex": "^3.1.2",
"vuex-router-sync": "^5.0.0",
"whatwg-fetch": "^3.6.2"
"qrcode.vue": "^3.4.1",
"vue": "^3.4.21",
"vue-i18n": "^9.10.2",
"vue-lazyload": "^3.0.0",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@vue/cli-service": "^5.0.8",
"compression-webpack-plugin": "^10.0.0",
"eslint": "^8.51.0",
"eslint-plugin-vue": "^9.17.0",
"vue-template-compiler": "^2.6.10"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie < 11"
]
"@playwright/test": "^1.42.1",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-typescript": "^13.0.0",
"eslint": "^8.57.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.24.0",
"vite": "^5.2.7",
"vite-plugin-compression2": "^1.0.0",
"vue-tsc": "^2.0.7"
}
}
+43
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"] },
},
],
});
+7 -5
View File
@@ -6,10 +6,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
[{[ if .ReCaptcha -]}]
<script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit"></script>
<script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit" data-vite-ignore></script>
[{[ end ]}]
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]</title>
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]FileBrowser Quantum[{[ end ]}]</title>
<link rel="icon" type="image/png" sizes="256x256" href="[{[ .StaticURL ]}]/img/icons/favicon-256x256.png">
@@ -33,8 +33,8 @@
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
var dynamicManifest = {
"name": window.FileBrowser.Name || 'File Browser',
"short_name": window.FileBrowser.Name || 'File Browser',
"name": window.FileBrowser.Name || 'FileBrowser Quantum',
"short_name": window.FileBrowser.Name || 'FileBrowser',
"icons": [
{
"src": fullStaticURL + "/img/icons/android-chrome-256x256.png",
@@ -121,7 +121,7 @@
<div id="app"></div>
[{[ if .darkMode -]}]
<div id="loading dark-mode">
<div id="loading" class="dark-mode">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
@@ -136,6 +136,8 @@
<div class="bounce3"></div>
</div>
</div> [{[ end ]}]
<script type="module" src="/src/main.ts"></script>
[{[ if .CSS -]}]
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
[{[ end ]}]
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "File Browser",
"short_name": "File Browser",
"name": "FileBrowser",
"short_name": "FileBrowser",
"icons": [
{
"src": "./img/icons/android-chrome-192x192.png",
+15 -5
View File
@@ -1,15 +1,25 @@
<template>
<router-view></router-view>
<router-view></router-view>
</template>
<script>
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.FileBrowser.StaticURL + "/";
import { onMounted } from "vue";
import { mutations } from "@/store"; // Import your store's mutations
mutations.setLoading("main-app", true);
export default {
name: "app",
computed: {},
setup() {
onMounted(() => {
console.log("static vars", window.FileBrowser);
mutations.setLoading("main-app", false);
// Query the loading element and remove it from the DOM
const loadingDiv = document.getElementById("loading");
if (loadingDiv) {
loadingDiv.remove();
}
});
},
};
</script>
+2 -2
View File
@@ -1,13 +1,13 @@
import { removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import store from "@/store";
import { state } from "@/store";
const ssl = window.location.protocol === "https:";
const protocol = ssl ? "wss:" : "ws:";
export default function command(url, command, onmessage, onclose) {
url = removePrefix(url);
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`;
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${state.jwt}`;
let conn = new window.WebSocket(url);
conn.onopen = () => conn.send(command);
+159 -103
View File
@@ -1,122 +1,151 @@
import { createURL, fetchURL, removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import store from "@/store";
import { state } from "@/store";
import { notify } from "@/notify";
export async function fetch(url) {
url = removePrefix(url);
// Notify if errors occur
export async function fetch(url, content = false) {
try {
url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}`, {});
const res = await fetchURL(`/api/resources${url}?content=${content}`, {});
const data = await res.json();
data.url = `/files${url}`;
let data = await res.json();
data.url = `/files${url}`;
if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => {
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => {
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) {
item.url += "/";
}
if (item.isDir) {
item.url += "/";
}
return item;
});
}
return item;
});
return data;
} catch (err) {
notify.showError(err.message || "Error fetching data");
throw err;
}
return data;
}
async function resourceAction(url, method, content) {
url = removePrefix(url);
try {
url = removePrefix(url);
let opts = { method };
let opts = { method };
if (content) {
opts.body = content;
if (content) {
opts.body = content;
}
const res = await fetchURL(`/api/resources${url}`, opts);
return res;
} catch (err) {
notify.showError(err.message || "Error performing resource action");
throw err;
}
const res = await fetchURL(`/api/resources${url}`, opts);
return res;
}
export async function remove(url) {
return resourceAction(url, "DELETE");
try {
return await resourceAction(url, "DELETE");
} catch (err) {
notify.showError(err.message || "Error deleting resource");
throw err;
}
}
export async function put(url, content = "") {
return resourceAction(url, "PUT", content);
try {
return await resourceAction(url, "PUT", content);
} catch (err) {
notify.showError(err.message || "Error putting resource");
throw err;
}
}
export function download(format, ...files) {
let url = `${baseURL}/api/raw`;
try {
let url = `${baseURL}/api/raw`;
if (files.length === 1) {
url += removePrefix(files[0]) + "?";
} else {
let arg = "";
if (files.length === 1) {
url += removePrefix(files[0]) + "?";
} else {
let arg = "";
for (let file of files) {
arg += removePrefix(file) + ",";
for (let file of files) {
arg += removePrefix(file) + ",";
}
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
}
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
}
if (format) {
url += `algo=${format}&`;
}
if (format) {
url += `algo=${format}&`;
}
if (state.jwt) {
url += `auth=${state.jwt}&`;
}
if (store.state.jwt) {
url += `auth=${store.state.jwt}&`;
window.open(url);
} catch (err) {
notify.showError(err.message || "Error downloading files");
}
window.open(url);
}
export async function post(url, content = "", overwrite = false, onupload) {
url = removePrefix(url);
try {
url = removePrefix(url);
let bufferContent;
if (
content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)
) {
bufferContent = await new Response(content).arrayBuffer();
}
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest();
request.open(
"POST",
`${baseURL}/api/resources${url}?override=${overwrite}`,
true
);
request.setRequestHeader("X-Auth", store.state.jwt);
if (typeof onupload === "function") {
request.upload.onprogress = onupload;
let bufferContent;
if (
content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)
) {
bufferContent = await new Response(content).arrayBuffer();
}
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText);
} else if (request.status === 409) {
reject(request.status);
} else {
reject(request.responseText);
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest();
request.open(
"POST",
`${baseURL}/api/resources${url}?override=${overwrite}`,
true
);
request.setRequestHeader("X-Auth", state.jwt);
if (typeof onupload === "function") {
request.upload.onprogress = onupload;
}
};
request.onerror = () => {
reject(new Error("001 Connection aborted"));
};
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText);
} else if (request.status === 409) {
reject(request.status);
} else {
reject(request.responseText);
}
};
request.send(bufferContent || content);
});
request.onerror = () => {
reject(new Error("001 Connection aborted"));
};
request.send(bufferContent || content);
});
} catch (err) {
notify.showError(err.message || "Error posting resource");
throw err;
}
}
function moveCopy(items, copy = false, overwrite = false, rename = false) {
@@ -131,7 +160,10 @@ function moveCopy(items, copy = false, overwrite = false, rename = false) {
promises.push(resourceAction(url, "PATCH"));
}
return Promise.all(promises);
return Promise.all(promises).catch((err) => {
notify.showError(err.message || "Error moving/copying resources");
throw err;
});
}
export function move(items, overwrite = false, rename = false) {
@@ -143,44 +175,68 @@ export function copy(items, overwrite = false, rename = false) {
}
export async function checksum(url, algo) {
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
return (await data.json()).checksums[algo];
try {
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
return (await data.json()).checksums[algo];
} catch (err) {
notify.showError(err.message || "Error fetching checksum");
throw err;
}
}
export function getDownloadURL(file, inline) {
const params = {
...(inline && { inline: "true" }),
};
try {
const params = {
...(inline && { inline: "true" }),
};
return createURL("api/raw" + file.path, params);
return createURL("api/raw" + file.path, params);
} catch (err) {
notify.showError(err.message || "Error getting download URL");
throw err;
}
}
export function getPreviewURL(file, size) {
const params = {
inline: "true",
key: Date.parse(file.modified),
};
try {
const params = {
inline: "true",
key: Date.parse(file.modified),
};
return createURL("api/preview/" + size + file.path, params);
return createURL("api/preview/" + size + file.path, params);
} catch (err) {
notify.showError(err.message || "Error getting preview URL");
throw err;
}
}
export function getSubtitlesURL(file) {
const params = {
inline: "true",
};
try {
const params = {
inline: "true",
};
const subtitles = [];
for (const sub of file.subtitles) {
subtitles.push(createURL("api/raw" + sub, params));
const subtitles = [];
for (const sub of file.subtitles) {
subtitles.push(createURL("api/raw" + sub, params));
}
return subtitles;
} catch (err) {
notify.showError(err.message || "Error fetching subtitles URL");
throw err;
}
return subtitles;
}
export async function usage(url) {
url = removePrefix(url);
try {
url = removePrefix(url);
const res = await fetchURL(`/api/usage${url}`, {});
return await res.json();
const res = await fetchURL(`/api/usage${url}`, {});
return await res.json();
} catch (err) {
notify.showError(err.message || "Error fetching usage data");
throw err;
}
}
+33 -14
View File
@@ -1,20 +1,24 @@
import { fetchURL, removePrefix, createURL } from "./utils";
import { removePrefix, createURL } from "./utils";
import { baseURL } from "@/utils/constants";
export async function fetch(url, password = "") {
export async function fetchPub(url, password = "") {
url = removePrefix(url);
const res = await fetchURL(
`/api/public/share${url}`,
{
headers: { "X-SHARE-PASSWORD": encodeURIComponent(password) },
},
false
const res = await fetch(
`/api/public/share${url}`,
{
headers: {
"X-SHARE-PASSWORD": encodeURIComponent(password),
},
}
);
if (res.status != 200) {
const error = new Error("000 No connection");
error.status = res.status;
throw error;
}
let data = await res.json();
data.url = `/share${url}`;
if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => {
@@ -34,12 +38,10 @@ export async function fetch(url, password = "") {
export function download(format, hash, token, ...files) {
let url = `${baseURL}/api/public/dl/${hash}`;
if (files.length === 1) {
url += encodeURIComponent(files[0]) + "?";
} else {
let arg = "";
for (let file of files) {
arg += encodeURIComponent(file) + ",";
}
@@ -60,11 +62,28 @@ export function download(format, hash, token, ...files) {
window.open(url);
}
export function getPublicUser() {
return fetch("/api/public/publicUser")
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.catch(error => {
console.error("Error fetching public user:", error);
throw error;
});
}
export function getDownloadURL(share, inline = false) {
const params = {
...(inline && { inline: "true" }),
...(share.token && { token: share.token }),
};
return createURL("api/public/dl/" + share.hash + share.path, params, false);
if (share.path == undefined) {
share.path = ""
}
const path = share.path.replace("/share/"+share.hash +"/","")
return createURL("api/public/dl/" + share.hash + "/"+path, params, false);
}
+19 -18
View File
@@ -1,27 +1,28 @@
import { fetchURL, removePrefix } from "./utils";
import url from "../utils/url";
import { notify } from "@/notify"; // Import notify for error handling
export default async function search(base, query) {
base = removePrefix(base);
query = encodeURIComponent(query);
try {
base = removePrefix(base);
query = encodeURIComponent(query);
if (!base.endsWith("/")) {
base += "/";
}
let res = await fetchURL(`/api/search${base}?query=${query}`, {});
let data = await res.json();
data = data.map((item) => {
item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) {
item.url += "/";
if (!base.endsWith("/")) {
base += "/";
}
return item;
});
const res = await fetchURL(`/api/search${base}?query=${query}`, {});
return data;
let data = await res.json();
data = data.map((item) => {
item.url = `/files${base}` + url.encodePath(item.path);
return item;
});
return data;
} catch (err) {
notify.showError(err.message || "Error occurred during search");
throw err;
}
}
+1
View File
@@ -18,6 +18,7 @@ export async function remove(hash) {
export async function create(url, password = "", expires = "", unit = "hours") {
url = removePrefix(url);
url = `/api/share${url}`;
expires = String(expires);
if (expires !== "") {
url += `?expires=${expires}&unit=${unit}`;
}
+60 -25
View File
@@ -1,41 +1,76 @@
import { fetchURL, fetchJSON } from "./utils";
import { fetchURL, fetchJSON } from "@/api/utils";
import { notify } from "@/notify"; // Import notify for error handling
export async function getAll() {
return fetchJSON(`/api/users`, {});
export async function getAllUsers() {
try {
return await fetchJSON(`/api/users`, {});
} catch (err) {
notify.showError(err.message || "Failed to fetch users");
throw err; // Re-throw to handle further if needed
}
}
export async function get(id) {
return fetchJSON(`/api/users/${id}`, {});
try {
return await fetchJSON(`/api/users/${id}`, {});
} catch (err) {
notify.showError(err.message || `Failed to fetch user with ID: ${id}`);
throw err;
}
}
export async function create(user) {
const res = await fetchURL(`/api/users`, {
method: "POST",
body: JSON.stringify({
what: "user",
which: [],
data: user,
}),
});
try {
const res = await fetchURL(`/api/users`, {
method: "POST",
body: JSON.stringify({
what: "user",
which: [],
data: user,
}),
});
if (res.status === 201) {
return res.headers.get("Location");
if (res.status === 201) {
return res.headers.get("Location");
} else {
throw new Error("Failed to create user");
}
} catch (err) {
notify.showError(err.message || "Error creating user");
throw err;
}
}
export async function update(user, which = ["all"]) {
await fetchURL(`/api/users/${user.id}`, {
method: "PUT",
body: JSON.stringify({
what: "user",
which: which,
data: user,
}),
});
try {
// List of keys to exclude from the "which" array
const excludeKeys = ["id", "name"];
// Filter out the keys from "which"
which = which.filter(item => !excludeKeys.includes(item));
if (user.username === "publicUser") {
return;
}
await fetchURL(`/api/users/${user.id}`, {
method: "PUT",
body: JSON.stringify({
what: "user",
which: which,
data: user,
}),
});
} catch (err) {
notify.showError(err.message || `Failed to update user with ID: ${user.id}`);
throw err;
}
}
export async function remove(id) {
await fetchURL(`/api/users/${id}`, {
method: "DELETE",
});
try {
await fetchURL(`/api/users/${id}`, {
method: "DELETE",
});
} catch (err) {
notify.showError(err.message || `Failed to delete user with ID: ${id}`);
throw err;
}
}
+11 -11
View File
@@ -1,7 +1,8 @@
import store from "@/store";
import { state } from "@/store";
import { renew, logout } from "@/utils/auth";
import { baseURL } from "@/utils/constants";
import { encodePath } from "@/utils/url";
import { notify } from "@/notify";
export async function fetchURL(url, opts, auth = true) {
opts = opts || {};
@@ -13,22 +14,22 @@ export async function fetchURL(url, opts, auth = true) {
try {
res = await fetch(`${baseURL}${url}`, {
headers: {
"X-Auth": store.state.jwt,
"sessionId": store.state.sessionId,
"userScope": store.state.user.scope,
"X-Auth": state.jwt,
"sessionId": state.sessionId,
"userScope": state.user.scope,
...headers,
},
...rest,
});
} catch {
} catch (e) {
console.error(e)
const error = new Error("000 No connection");
error.status = 0;
error.status = res.status;
throw error;
}
if (auth && res.headers.get("X-Renew-Token") === "true") {
await renew(store.state.jwt);
await renew(state.jwt);
}
if (res.status < 200 || res.status > 299) {
@@ -47,17 +48,16 @@ export async function fetchURL(url, opts, auth = true) {
export async function fetchJSON(url, opts) {
const res = await fetchURL(url, opts);
if (res.status === 200) {
return res.json();
} else {
notify.showError("unable to fetch : " + url + "status" + res.status);
throw new Error(res.status);
}
}
export function removePrefix(url) {
url = url.split("/").splice(2).join("/");
if (url === "") url = "/";
if (url[0] !== "/") url = "/" + url;
return url;
@@ -71,7 +71,7 @@ export function createURL(endpoint, params = {}, auth = true) {
const url = new URL(prefix + encodePath(endpoint), origin);
const searchParams = {
...(auth && { auth: store.state.jwt }),
...(auth && { auth: state.jwt }),
...params,
};
@@ -7,19 +7,22 @@
</template>
<script>
import { mutations } from "@/store"; // Import your custom store
export default {
name: "action",
props: ["icon", "label", "counter", "show"],
methods: {
action: function () {
if (this.show) {
this.$store.commit("showHover", this.show);
mutations.showHover(this.show);
}
this.$emit("action");
},
},
};
</script>
<style></style>
<style>
</style>
+43 -6
View File
@@ -10,21 +10,52 @@
</component>
<span v-for="(link, index) in items" :key="index">
<span class="chevron"
><i class="material-icons">keyboard_arrow_right</i></span
>
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
<component :is="element" :to="link.url">{{ link.name }}</component>
</span>
<action style="display: contents" v-if="showShare" icon="share" show="share" />
<div v-if="isCardView">
Size:
<input
v-model="gallerySize"
type="range"
id="gallary-size"
name="gallary-size"
:value="gallerySize"
min="0"
max="10"
/>
</div>
</div>
</template>
<script>
import { state, mutations, getters } from "@/store"; // Import mutations as well
import Action from "@/components/Action.vue";
export default {
name: "breadcrumbs",
components: {
Action,
},
data() {
return {
gallerySize: state.user.gallerySize,
};
},
watch: {
gallerySize(newValue) {
this.gallerySize = parseInt(newValue, 0); // Update the user object
mutations.setGallerySize(this.gallerySize);
},
},
props: ["base", "noLink"],
computed: {
isCardView() {
return getters.isCardView();
},
items() {
const relativePath = this.$route.path.replace(this.base, "");
const relativePath = state.route.path.replace(this.base, "");
let parts = relativePath.split("/");
if (parts[0] === "") {
@@ -68,8 +99,14 @@ export default {
return "router-link";
},
showShare() {
// Ensure user properties are accessed safely
if (state.route.path.startsWith("/share")) {
return false;
}
return state.user?.perm && state.user?.perm.share; // Access from state directly
},
},
methods: { },
};
</script>
<style></style>
+20 -10
View File
@@ -1,9 +1,18 @@
<template>
<div class="button-group">
<button v-for="(btn, index) in buttons" :key="index" :disabled="isDisabled" :class="{ active: activeButton === index && !isDisabled }"
@click="setActiveButton(index, btn.label)">
{{ btn.label }}
<button v-if="isDisabled" disabled>
No options for folders
</button>
<template v-else>
<button
v-for="(btn, index) in buttons"
:key="index"
:class="{ active: activeButton === index }"
@click="setActiveButton(index, btn.label)"
>
{{ btn.label }}
</button>
</template>
</div>
</template>
@@ -16,7 +25,7 @@ export default {
},
isDisabled: {
type: Boolean,
default: false
default: false,
},
initialActive: {
type: Number,
@@ -31,15 +40,12 @@ export default {
methods: {
setActiveButton(index, label) {
if (label == "Only Folders" && this.activeButton != index) {
console.log("Only Folders && this.activeButton != index")
this.$emit("disableAll");
}
if (label == "Only Folders" && this.activeButton == index) {
console.log("Only Folders && this.activeButton == index")
this.$emit("enableAll");
}
if (label == "Only Files" && this.activeButton != index) {
console.log("Only Files && this.activeButton != index")
this.$emit("enableAll");
}
// If the clicked button is already active, de-select it
@@ -72,7 +78,7 @@ export default {
<style scoped>
.button-group {
margin:1em;
margin: 1em;
display: flex;
border: 1px solid #ccc;
border-radius: 1em;
@@ -93,7 +99,7 @@ button {
}
/* Remove the border from the last button */
.button-group>button:last-child {
.button-group > button:last-child {
border-right: none;
}
@@ -101,8 +107,12 @@ button:hover {
background: #e0e0e0;
}
button:disabled {
cursor: not-allowed !important;
}
button.active {
background-color: var(--blue);
background-color: var(--blue) !important;
color: #ffffff;
}
</style>
+230
View File
@@ -0,0 +1,230 @@
<template>
<div
id="context-menu"
ref="contextMenu"
v-show="showContext"
:style="{
top: `${top}px`,
left: `${left}px`,
}"
class="button"
:class="{ 'dark-mode': isDarkMode, mobile: isMobile }"
>
<div v-if="selectedCount > 0" class="button selected-count-header">
<span>{{ selectedCount }} selected</span>
</div>
<action
v-if="!headerButtons.select"
icon="create_new_folder"
:label="$t('sidebar.newFolder')"
@action="showHover('newDir')"
/>
<action
v-if="!headerButtons.select"
icon="note_add"
:label="$t('sidebar.newFile')"
@action="showHover('newFile')"
/>
<action
v-if="!headerButtons.select"
icon="file_upload"
:label="$t('buttons.upload')"
@action="uploadFunc"
/>
<action
v-if="headerButtons.select"
icon="info"
:label="$t('buttons.info')"
show="info"
/>
<action
v-if="!isMultiple"
icon="check_circle"
:label="$t('buttons.selectMultiple')"
@action="toggleMultipleSelection"
/>
<action
v-if="headerButtons.download"
icon="file_download"
:label="$t('buttons.download')"
@action="startDownload"
:counter="selectedCount"
/>
<action
v-if="headerButtons.share"
icon="share"
:label="$t('buttons.share')"
show="share"
/>
<action
v-if="headerButtons.rename"
icon="mode_edit"
:label="$t('buttons.rename')"
show="rename"
/>
<action
v-if="headerButtons.copy"
icon="content_copy"
:label="$t('buttons.copyFile')"
show="copy"
/>
<action
v-if="headerButtons.move"
icon="forward"
:label="$t('buttons.moveFile')"
show="move"
/>
<action
v-if="headerButtons.delete"
icon="delete"
:label="$t('buttons.delete')"
show="delete"
/>
</div>
</template>
<script>
import downloadFiles from "@/utils/download";
import { state, getters, mutations } from "@/store"; // Import your custom store
import Action from "@/components/Action.vue";
export default {
name: "ContextMenu",
components: {
Action,
},
data() {
return {
posX: 0,
posY: 0,
};
},
computed: {
isMultiple() {
return state.multiple;
},
user() {
return state.user;
},
isMobile() {
return getters.isMobile();
},
showContext() {
if (getters.currentPromptName() == "ContextMenu" && state.prompts != []) {
this.setPositions();
return true;
}
return false;
},
top() {
// Ensure the context menu stays within the viewport
return Math.min(
this.posY,
window.innerHeight - (this.$refs.contextMenu?.clientHeight ?? 0)
);
},
left() {
return Math.min(
this.posX,
window.innerWidth - (this.$refs.contextMenu?.clientWidth ?? 0)
);
},
isDarkMode() {
return getters.isDarkMode();
},
headerButtons() {
return {
select: state.selected.length > 0,
upload: state.user.perm?.create && state.selected.length > 0,
download: state.user.perm.download && state.selected.length > 0,
delete: state.selected.length > 0 && state.user.perm.delete,
rename: state.selected.length === 1 && state.user.perm.rename,
share: state.selected.length === 1 && state.user.perm.share,
move: state.selected.length > 0 && state.user.perm.rename,
copy: state.selected.length > 0 && state.user.perm?.create,
};
},
selectedCount() {
return getters.selectedCount();
},
},
methods: {
uploadFunc() {
mutations.showHover("upload");
},
showHover(value) {
return mutations.showHover(value);
},
setPositions() {
const contextProps = getters.currentPrompt().props;
this.posX = contextProps.posX;
this.posY = contextProps.posY;
},
toggleMultipleSelection() {
mutations.setMultiple(!state.multiple);
mutations.closeHovers();
},
startDownload() {
downloadFiles();
},
},
};
</script>
<style scoped>
#context-menu {
position: absolute;
z-index: 1000;
background-color: white;
max-width: 20em;
min-width: 15em;
min-height: 4em;
height: auto;
display: flex;
flex-direction: column;
justify-content: center;
}
#context-menu.mobile {
top: 50% !important;
left: 50% !important;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.selected-count-header {
border-radius: 0.5em;
cursor: unset;
margin-bottom: 0.5em;
}
#context-menu .action {
width: auto;
display: flex;
align-items: center;
}
#context-menu > span {
display: inline-block;
margin-left: 1em;
color: #6f6f6f;
margin-right: auto;
}
#context-menu .action span {
display: none;
}
/* File selection */
#context-menu.dark-mode {
background: var(--surfaceSecondary) !important;
}
#context-menu.dark-mode span {
color: var(--textPrimary) !important;
}
</style>
+232
View File
@@ -0,0 +1,232 @@
<!-- This component taken directly from vue-simple-progress
since it didnt support Vue 3 but the component itself does
https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/components/Progress.vue -->
<template>
<div>
<div
class="vue-simple-progress-text"
:style="text_style"
v-if="text.length > 0 && textPosition == 'top'"
>
{{ text }}
</div>
<div class="vue-simple-progress" :style="progress_style">
<div
class="vue-simple-progress-text"
:style="text_style"
v-if="text.length > 0 && textPosition == 'middle'"
>
{{ text }}
</div>
<div
style="position: relative; left: -9999px"
:style="text_style"
v-if="text.length > 0 && textPosition == 'inside'"
>
{{ text }}
</div>
<div class="vue-simple-progress-bar" :style="bar_style">
<div :style="text_style" v-if="text.length > 0 && textPosition == 'inside'">
{{ text }}
</div>
</div>
</div>
<div
class="vue-simple-progress-text"
:style="text_style"
v-if="text.length > 0 && textPosition == 'bottom'"
>
{{ text }}
</div>
</div>
</template>
<script>
// We're leaving this untouched as you can read in the beginning
var isNumber = function (n) {
return !isNaN(parseFloat(n)) && isFinite(n);
};
export default {
name: "progress-bar",
props: {
val: {
default: 0,
},
max: {
default: 100,
},
size: {
// either a number (pixel width/height) or 'tiny', 'small',
// 'medium', 'large', 'huge', 'massive' for common sizes
default: 3,
},
"bg-color": {
type: String,
default: "#eee",
},
"bar-color": {
type: String,
default: "#2196f3", // match .blue color to Material Design's 'Blue 500' color
},
"bar-transition": {
type: String,
default: "all 0.5s ease",
},
"bar-border-radius": {
type: Number,
default: 0,
},
spacing: {
type: Number,
default: 4,
},
text: {
type: String,
default: "",
},
"text-align": {
type: String,
default: "center", // 'left', 'right'
},
"text-position": {
type: String,
default: "bottom", // 'bottom', 'top', 'middle', 'inside'
},
"font-size": {
type: Number,
default: 13,
},
"text-fg-color": {
type: String,
default: "#222",
},
},
computed: {
pct() {
var pct = (this.val / this.max) * 100;
pct = pct.toFixed(2);
return Math.min(pct, this.max);
},
size_px() {
switch (this.size) {
case "tiny":
return 2;
case "small":
return 4;
case "medium":
return 8;
case "large":
return 12;
case "big":
return 16;
case "huge":
return 32;
case "massive":
return 64;
}
return isNumber(this.size) ? this.size : 32;
},
text_padding() {
switch (this.size) {
case "tiny":
case "small":
case "medium":
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(this.size_px / 8), 3), 12);
}
return isNumber(this.spacing) ? this.spacing : 4;
},
text_font_size() {
switch (this.size) {
case "tiny":
case "small":
case "medium":
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32);
}
return isNumber(this.fontSize) ? this.fontSize : 13;
},
progress_style() {
var style = {
background: this.bgColor,
};
if (this.textPosition == "middle" || this.textPosition == "inside") {
style["position"] = "relative";
style["min-height"] = this.size_px + "px";
style["z-index"] = "-2";
}
if (this.barBorderRadius > 0) {
style["border-radius"] = this.barBorderRadius + "px";
}
return style;
},
bar_style() {
var style = {
background: this.barColor,
width: this.pct + "%",
height: this.size_px + "px",
transition: this.barTransition,
};
if (this.barBorderRadius > 0) {
style["border-radius"] = this.barBorderRadius + "px";
}
if (this.textPosition == "middle" || this.textPosition == "inside") {
style["position"] = "absolute";
style["top"] = "0";
style["height"] = "100%";
(style["min-height"] = this.size_px + "px"), (style["z-index"] = "-1");
}
return style;
},
text_style() {
var style = {
color: this.textFgColor,
"font-size": this.text_font_size + "px",
"text-align": this.textAlign,
};
if (
this.textPosition == "top" ||
this.textPosition == "middle" ||
this.textPosition == "inside"
)
style["padding-bottom"] = this.text_padding + "px";
if (
this.textPosition == "bottom" ||
this.textPosition == "middle" ||
this.textPosition == "inside"
)
style["padding-top"] = this.text_padding + "px";
return style;
},
},
};
</script>
<style>
.vue-simple-progress {
margin: 0.5em;
}
.vue-simple-progress,
.vue-simple-progress-bar {
border-radius: 0.5em;
}
</style>
+297 -303
View File
@@ -1,10 +1,8 @@
<template>
<div
id="search"
@click="open"
v-bind:class="{ active, ongoing, 'dark-mode': isDarkMode }"
>
<div id="input">
<div id="search" :class="{ active, ongoing, 'dark-mode': isDarkMode }">
<!-- Search input section -->
<div id="input" @click="open">
<!-- Close button visible when search is active -->
<button
v-if="active"
class="action"
@@ -14,8 +12,11 @@
>
<i class="material-icons">close</i>
</button>
<!-- Search icon when search is not active -->
<i v-else class="material-icons">search</i>
<!-- Input field for search -->
<input
id="main-input"
class="main-input"
type="text"
@keyup.exact="keyup"
@@ -27,19 +28,15 @@
:placeholder="$t('search.search')"
/>
</div>
<!-- Search results for mobile -->
<div v-if="isMobile && active" id="result" :class="{ hidden: !active }" ref="result">
<div id="result-list">
<div class="button" style="width: 100%">
Search Context: {{ getContext }}
</div>
<div class="button" style="width: 100%">Search Context: {{ getContext }}</div>
<!-- List of search results -->
<ul v-show="results.length > 0">
<li
v-for="(s, k) in results"
:key="k"
@click.stop.prevent="navigateTo(s.url)"
style="cursor: pointer"
>
<router-link to="#" event="">
<li v-for="(s, k) in results" :key="k" style="cursor: pointer">
<router-link :to="s.url">
<i v-if="s.dir" class="material-icons folder-icons"> folder </i>
<i v-else-if="s.audio" class="material-icons audio-icons"> volume_up </i>
<i v-else-if="s.image" class="material-icons image-icons"> photo </i>
@@ -47,20 +44,23 @@
<i v-else-if="s.archive" class="material-icons archive-icons"> archive </i>
<i v-else class="material-icons file-icons"> insert_drive_file </i>
<span class="text-container">
{{ basePath(s.path,s.dir) }}<b>{{ baseName(s.path) }}</b>
{{ basePath(s.path, s.dir) }}<b>{{ baseName(s.path) }}</b>
</span>
</router-link>
</li>
</ul>
<!-- Loading icon when search is ongoing -->
<p v-show="isEmpty && isRunning" id="renew">
<i class="material-icons spin">autorenew</i>
</p>
<!-- Message when no results are found -->
<div v-show="isEmpty && !isRunning">
<div class="searchPrompt" v-show="isEmpty && !isRunning">
<p>{{ noneMessage }}</p>
</div>
</div>
<template v-if="isEmpty">
<div v-if="isEmpty">
<!-- Reset filters button -->
<button
class="mobile-boxes"
v-if="value.length === 0 && !showBoxes"
@@ -68,7 +68,8 @@
>
Reset filters
</button>
<template v-if="value.length === 0 && showBoxes">
<!-- Box types when no search input is present -->
<div v-if="value.length === 0 && showBoxes">
<div class="boxes">
<h3>{{ $t("search.types") }}</h3>
<div>
@@ -86,45 +87,18 @@
</div>
</div>
</div>
</template>
</template>
</div>
</div>
</div>
</div>
<!-- Search results for desktop -->
<div v-show="!isMobile && active" id="result-desktop" ref="result">
<div class="searchContext">Search Context: {{ getContext }}</div>
<div id="result-list">
<template>
<p v-show="isEmpty && isRunning" id="renew">
<i class="material-icons spin">autorenew</i>
</p>
<div class="searchPrompt" v-show="isEmpty && !isRunning">
<p>{{ noneMessage }}</p>
<div class="helpButton" @click="toggleHelp()">Help</div>
</div>
<div class="helpText" v-if="showHelp">
<p>
Search occurs on each character you type (3 character minimum for search
terms).
</p>
<p>
<b>The index:</b> Search utilizes the index which automatically gets updated
on the configured interval (default: 5 minutes). Searching when the program
has just started may result in incomplete results.
</p>
<p>
<b>Filter by type:</b> You can have multiple type filters by adding
<code>type:condition</code> followed by search terms.
</p>
<p>
<b>Multiple Search terms:</b> Additional terms separated by <code>|</code>,
for example <code>"test|not"</code> searches for both terms independently.
</p>
<p>
<b>File size:</b> Searching files by size may have significantly longer
search times.
</p>
</div>
<template>
<div>
<div>
<!-- Button groups for filtering search results -->
<ButtonGroup
:buttons="folderSelect"
@button-clicked="addToTypes"
@@ -138,7 +112,8 @@
@remove-button-clicked="removeFromTypes"
:isDisabled="isTypeSelectDisabled"
/>
<div class="sizeConstraints">
<!-- Inputs for filtering by file size -->
<div v-if="!foldersOnly" class="sizeConstraints">
<div class="sizeInputWrapper">
<p>Smaller Than:</p>
<input
@@ -161,16 +136,45 @@
<p>MB</p>
</div>
</div>
</template>
</template>
</div>
</div>
<!-- Loading icon when search is ongoing -->
<p v-show="isEmpty && isRunning" id="renew">
<i class="material-icons spin">autorenew</i>
</p>
<!-- Message when no results are found -->
<div class="searchPrompt" v-show="isEmpty && !isRunning">
<p>{{ noneMessage }}</p>
<div class="helpButton" @click="toggleHelp()">Help</div>
</div>
<!-- Help text section -->
<div class="helpText" v-if="showHelp">
<p>
Search occurs on each character you type (3 character minimum for search
terms).
</p>
<p>
<b>The index:</b> Search utilizes the index which automatically gets updated
on the configured interval (default: 5 minutes). Searching when the program
has just started may result in incomplete results.
</p>
<p>
<b>Filter by type:</b> You can have multiple type filters by adding
<code>type:condition</code> followed by search terms.
</p>
<p>
<b>Multiple Search terms:</b> Additional terms separated by <code>|</code>,
for example <code>"test|not"</code> searches for both terms independently.
</p>
<p>
<b>File size:</b> Searching files by size may have significantly longer search
times.
</p>
</div>
<!-- List of search results -->
<ul v-show="results.length > 0">
<li
v-for="(s, k) in results"
:key="k"
@click.stop.prevent="navigateTo(s.url)"
style="cursor: pointer"
>
<router-link to="#" event="">
<li v-for="(s, k) in results" :key="k" style="cursor: pointer">
<router-link :to="s.url">
<i v-if="s.dir" class="material-icons folder-icons"> folder </i>
<i v-else-if="s.audio" class="material-icons audio-icons"> volume_up </i>
<i v-else-if="s.image" class="material-icons image-icons"> photo </i>
@@ -178,7 +182,7 @@
<i v-else-if="s.archive" class="material-icons archive-icons"> archive </i>
<i v-else class="material-icons file-icons"> insert_drive_file </i>
<span class="text-container">
{{ basePath(s.path,s.dir) }}<b>{{ baseName(s.path) }}</b>
{{ basePath(s.path, s.dir) }}<b>{{ baseName(s.path) }}</b>
</span>
</router-link>
</li>
@@ -187,6 +191,228 @@
</div>
</div>
</template>
<script>
import ButtonGroup from "./ButtonGroup.vue";
import { search } from "@/api";
import { getters, mutations, state } from "@/store";
var boxes = {
folder: { label: "folders", icon: "folder" },
file: { label: "files", icon: "insert_drive_file" },
archive: { label: "archives", icon: "archive" },
image: { label: "images", icon: "photo" },
audio: { label: "audio files", icon: "volume_up" },
video: { label: "videos", icon: "movie" },
doc: { label: "documents", icon: "picture_as_pdf" },
};
export default {
components: {
ButtonGroup,
},
name: "search",
data: function () {
return {
largerThan: "",
smallerThan: "",
noneMessage: "Start typing 3 or more characters to begin searching.",
searchTypes: "",
isTypeSelectDisabled: false,
showHelp: false,
folderSelect: [
{ label: "Only Folders", value: "type:folder" },
{ label: "Only Files", value: "type:file" },
],
typeSelect: [
{ label: "Photos", value: "type:image" },
{ label: "Audio", value: "type:audio" },
{ label: "Videos", value: "type:video" },
{ label: "Documents", value: "type:doc" },
{ label: "Archives", value: "type:archive" },
],
value: "",
ongoing: false,
results: [],
reload: false,
scrollable: null,
};
},
watch: {
largerThan() {
this.submit();
},
smallerThan() {
this.submit();
},
searchTypes() {
this.submit();
},
active(active) {
// this is hear to allow for animation
const resultList = document.getElementById("result-list");
if (!active) {
resultList.classList.remove("active");
this.value = "";
event.stopPropagation();
mutations.closeHovers();
return;
}
setTimeout(() => {
resultList.classList.add("active");
document.getElementById("main-input").focus();
}, 100);
},
value() {
if (this.results.length) {
this.ongoing = false;
this.results = [];
}
},
},
computed: {
isMobile() {
return state.isMobile;
},
foldersOnly() {
return this.isTypeSelectDisabled;
},
active() {
return getters.currentPromptName() == "search";
},
isDarkMode() {
return getters.isDarkMode();
},
showBoxes() {
return this.searchTypes == "";
},
boxes() {
return boxes;
},
isEmpty() {
return this.results.length === 0;
},
text() {
if (this.ongoing) {
return "";
}
return this.value === ""
? this.$t("search.typeToSearch")
: this.$t("search.pressToSearch");
},
isRunning() {
return this.ongoing;
},
searchHelp() {
return this.showHelp;
},
getContext() {
let path = state.route.path;
path = path.slice(1);
path = "./" + path.substring(path.indexOf("/") + 1);
path = path.replace(/\/+$/, "") + "/";
return path;
},
},
methods: {
basePath(str, isDir) {
let parts = str.replace(/(\/$|^\/)/, "").split("/");
if (parts.length <= 1) {
if (isDir) {
return "/";
}
return "";
}
parts.pop();
parts = parts.join("/") + "/";
if (isDir) {
parts = "/" + parts; // fix weird rtl thing
}
return parts;
},
baseName(str) {
let parts = str.replace(/(\/$|^\/)/, "").split("/");
return parts.pop();
},
open() {
if (!this.active) {
mutations.showHover("search");
}
},
close(event) {
this.value = "";
event.stopPropagation();
mutations.closeHovers();
},
keyup(event) {
if (event.keyCode === 27) {
this.close(event);
return;
}
this.results.length === 0;
},
addToTypes(string) {
if (this.searchTypes.includes(string)) {
return true;
}
if (string == null || string == "") {
return false;
}
this.searchTypes = this.searchTypes + string + " ";
},
resetSearchFilters() {
this.searchTypes = "";
},
removeFromTypes(string) {
if (string == null || string == "") {
return false;
}
this.searchTypes = this.searchTypes.replace(string + " ", "");
if (state.isMobile) {
this.$refs.input.focus();
}
},
folderSelectClicked() {
this.isTypeSelectDisabled = true; // Disable the other ButtonGroup
},
resetButtonGroups() {
this.isTypeSelectDisabled = false;
},
async submit(event) {
this.showHelp = false;
if (event != undefined) {
event.preventDefault();
}
if (this.value === "" || this.value.length < 3) {
this.ongoing = false;
this.results = [];
this.noneMessage = "Not enough characters to search (min 3)";
return;
}
let searchTypesFull = this.searchTypes;
if (this.largerThan != "") {
searchTypesFull = searchTypesFull + "type:largerThan=" + this.largerThan + " ";
}
if (this.smallerThan != "") {
searchTypesFull = searchTypesFull + "type:smallerThan=" + this.smallerThan + " ";
}
let path = state.route.path;
this.ongoing = true;
this.results = await search(path, searchTypesFull + this.value);
this.ongoing = false;
if (this.results.length == 0) {
this.noneMessage = "No results found in indexed search.";
}
},
toggleHelp() {
this.showHelp = !this.showHelp;
},
},
};
</script>
<style>
.main-input {
@@ -200,6 +426,7 @@
color: white;
border-left: 1px solid gray;
border-right: 1px solid gray;
word-wrap: break-word;
}
#result-desktop > #result-list {
@@ -264,8 +491,8 @@
/* Search */
#search {
background-color: unset;
z-index: 3;
background-color: unset !important;
z-index: 5;
position: fixed;
top: 0.5em;
min-width: 35em;
@@ -385,6 +612,10 @@ body.rtl #search #result ul > * {
border-bottom-left-radius: 0;
}
input.sizeInput:disabled {
cursor: not-allowed;
}
/* Search Input Placeholder */
#search::-webkit-input-placeholder {
color: rgba(255, 255, 255, 0.5);
@@ -511,240 +742,3 @@ body.rtl #search .boxes h3 {
align-items: center;
}
</style>
<script>
import ButtonGroup from "./ButtonGroup.vue";
import { mapState, mapGetters, mapMutations } from "vuex";
import { search } from "@/api";
import { darkMode } from "@/utils/constants";
var boxes = {
folder: { label: "folders", icon: "folder" },
file: { label: "files", icon: "insert_drive_file" },
archive: { label: "archives", icon: "archive" },
image: { label: "images", icon: "photo" },
audio: { label: "audio files", icon: "volume_up" },
video: { label: "videos", icon: "movie" },
doc: { label: "documents", icon: "picture_as_pdf" },
};
export default {
components: {
ButtonGroup,
},
name: "search",
data: function () {
return {
largerThan: "",
smallerThan: "",
noneMessage: "Start typing 3 or more characters to begin searching.",
searchTypes: "",
isTypeSelectDisabled: false,
showHelp: false,
folderSelect: [
{ label: "Only Folders", value: "type:folder" },
{ label: "Only Files", value: "type:file" },
],
typeSelect: [
{ label: "Photos", value: "type:image" },
{ label: "Audio", value: "type:audio" },
{ label: "Videos", value: "type:video" },
{ label: "Documents", value: "type:doc" },
{ label: "Archives", value: "type:archive" },
],
value: "",
width: window.innerWidth,
active: false,
ongoing: false,
results: [],
reload: false,
scrollable: null,
};
},
watch: {
active(active) {
const resultList = document.getElementById("result-list");
if (!active) {
resultList.classList.remove("active");
return;
}
setTimeout(() => {
resultList.classList.add("active");
}, 100);
},
show(val, old) {
this.active = val === "search";
if (old === "search" && !this.active) {
if (this.reload) {
this.setReload(true);
}
document.body.style.overflow = "auto";
this.ongoing = false;
this.results = [];
this.value = "";
this.active = false;
this.$refs.input.blur();
} else if (this.active) {
this.reload = false;
this.$refs.input.focus();
document.body.style.overflow = "hidden";
}
},
value() {
if (this.results.length) {
this.ongoing = false;
this.results = [];
}
},
},
computed: {
...mapState(["user", "show"]),
...mapGetters(["isListing"]),
isDarkMode() {
return this.user && Object.prototype.hasOwnProperty.call(this.user, "darkMode") ? this.user.darkMode : darkMode;
},
showBoxes() {
return this.searchTypes == "";
},
boxes() {
return boxes;
},
isEmpty() {
return this.results.length === 0;
},
text() {
if (this.ongoing) {
return "";
}
return this.value === ""
? this.$t("search.typeToSearch")
: this.$t("search.pressToSearch");
},
isMobile() {
return this.width <= 800;
},
isRunning() {
return this.ongoing;
},
searchHelp() {
return this.showHelp;
},
getContext() {
let path = this.$route.path
path = path.slice(1);
path = "./" + path.substring(path.indexOf("/") + 1);
path = path.replace(/\/+$/, "") + "/";
return path
},
},
mounted() {
window.addEventListener("resize", this.handleResize);
this.handleResize(); // Call this once to set the initial width
},
methods: {
handleResize() {
this.width = window.innerWidth;
},
async navigateTo(url) {
this.closeHovers();
await this.$nextTick();
setTimeout(() => this.$router.push(url), 0);
},
basePath(str,isDir) {
let parts = str.replace(/(\/$|^\/)/, "").split("/");
if (parts.length <= 1) {
if (isDir) {
return "/"
}
return "";
}
console.log("basePath",parts)
parts.pop();
parts = parts.join("/") + "/";
if (isDir) {
parts = "/" + parts; // fix weird rtl thing
}
return parts;
},
baseName(str) {
let parts = str.replace(/(\/$|^\/)/, "").split("/");
return parts.pop();
},
...mapMutations(["showHover", "closeHovers", "setReload"]),
open() {
this.showHover("search");
},
close(event) {
event.stopPropagation();
this.closeHovers();
},
keyup(event) {
if (event.keyCode === 27) {
this.close(event);
return;
}
this.results.length === 0;
},
addToTypes(string) {
if (this.searchTypes.includes(string)) {
return true;
}
if (string == null || string == "") {
return false;
}
this.searchTypes = this.searchTypes + string + " ";
},
resetSearchFilters() {
this.searchTypes = "";
},
removeFromTypes(string) {
if (string == null || string == "") {
return false;
}
this.searchTypes = this.searchTypes.replace(string + " ", "");
if (this.isMobile) {
this.$refs.input.focus();
}
},
folderSelectClicked() {
this.isTypeSelectDisabled = true; // Disable the other ButtonGroup
},
resetButtonGroups() {
this.isTypeSelectDisabled = false;
},
async submit(event) {
this.showHelp = false;
event.preventDefault();
if (this.value === "" || this.value.length < 3) {
this.ongoing = false;
this.results = [];
this.noneMessage = "Not enough characters to search (min 3)";
return;
}
let searchTypesFull = this.searchTypes;
if (this.largerThan != "") {
searchTypesFull = searchTypesFull + "type:largerThan=" + this.largerThan + " ";
}
if (this.smallerThan != "") {
searchTypesFull = searchTypesFull + "type:smallerThan=" + this.smallerThan + " ";
}
let path = this.$route.path;
this.ongoing = true;
try {
this.results = await search(path, searchTypesFull + this.value);
} catch (error) {
this.$showError(error);
}
this.ongoing = false;
if (this.results.length == 0) {
this.noneMessage = "No results found in indexed search.";
}
},
toggleHelp() {
this.showHelp = !this.showHelp;
},
},
};
</script>
-231
View File
@@ -1,231 +0,0 @@
<template>
<nav :class="{ active, 'dark-mode': isDarkMode }">
<template v-if="isLogged">
<button
class="action"
@click="toRoot"
:aria-label="$t('sidebar.myFiles')"
:title="$t('sidebar.myFiles')"
>
<i class="material-icons">folder</i>
<span>{{ $t("sidebar.myFiles") }}</span>
</button>
<div v-if="user.perm.create">
<button
@click="$store.commit('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>
<button
@click="$store.commit('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>
<button id="upload-button" @click="upload($event)" class="action">
<i class="material-icons">file_upload</i>
<span>Upload file</span>
</button>
</div>
<div>
<button
class="action"
@click="toSettings"
:aria-label="$t('sidebar.settings')"
:title="$t('sidebar.settings')"
>
<i class="material-icons">settings_applications</i>
<span>{{ $t("sidebar.settings") }}</span>
</button>
<button
v-if="canLogout"
@click="logout"
class="action"
id="logout"
:aria-label="$t('sidebar.logout')"
:title="$t('sidebar.logout')"
>
<i class="material-icons">exit_to_app</i>
<span>{{ $t("sidebar.logout") }}</span>
</button>
</div>
</template>
<template v-else>
<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>
<router-link
v-if="signup"
class="action"
to="/login"
:aria-label="$t('sidebar.signup')"
:title="$t('sidebar.signup')"
>
<i class="material-icons">person_add</i>
<span>{{ $t("sidebar.signup") }}</span>
</router-link>
</template>
<div
class="credits"
v-if="$router.currentRoute.path.includes('/files/') && !disableUsedPercentage"
>
<progress-bar :val="usage.usedPercentage" size="medium"></progress-bar>
<span style="text-align: center">{{ usage.usedPercentage }}%</span>
<span>{{ usage.used }} of {{ usage.total }} used</span>
<br />
<span v-if="disableExternal">File Browser</span>
<span v-else>
<a
rel="noopener noreferrer"
target="_blank"
href="https://github.com/gtsteffaniak/filebrowser"
>
File Browser
</a>
</span>
<span>{{ version }}</span>
<span>
<a @click="help">{{ $t("sidebar.help") }}</a>
</span>
</div>
</nav>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import * as upload from "@/utils/upload";
import * as auth from "@/utils/auth";
import {
version,
signup,
disableExternal,
disableUsedPercentage,
noAuth,
loginPage,
} from "@/utils/constants";
import { files as api } from "@/api";
import ProgressBar from "vue-simple-progress";
import prettyBytes from "pretty-bytes";
import { darkMode } from "@/utils/constants";
export default {
name: "sidebar",
components: {
ProgressBar,
},
computed: {
...mapState(["user"]),
isDarkMode() {
return this.user && Object.prototype.hasOwnProperty.call(this.user, "darkMode") ? this.user.darkMode : darkMode;
},
...mapGetters(["isLogged"]),
active() {
return this.$store.state.show === "sidebar";
},
signup: () => signup,
version: () => version,
disableExternal: () => disableExternal,
disableUsedPercentage: () => disableUsedPercentage,
canLogout: () => !noAuth && loginPage,
},
asyncComputed: {
usage: {
async get() {
let path = this.$route.path.endsWith("/")
? this.$route.path
: this.$route.path + "/";
let usageStats = { used: 0, total: 0, usedPercentage: 0 };
if (this.disableUsedPercentage) {
return usageStats;
}
try {
let usage = await api.usage(path);
usageStats = {
used: prettyBytes(usage.used / 1024, { binary: true }),
total: prettyBytes(usage.total / 1024, { binary: true }),
usedPercentage: Math.round((usage.used / usage.total) * 100),
};
} catch (error) {
this.$showError(error);
}
return usageStats;
},
default: { used: "0 B", total: "0 B", usedPercentage: 0 },
shouldUpdate() {
return this.$router.currentRoute.path.includes("/files/");
},
},
},
methods: {
toRoot() {
this.$router.push({ path: "/files/" }, () => {});
this.$store.commit("closeHovers");
},
toSettings() {
this.$router.push({ path: "/settings" }, () => {});
this.$store.commit("closeHovers");
},
help() {
this.$store.commit("showHover", "help");
},
upload: function () {
if (
typeof window.DataTransferItem !== "undefined" &&
typeof DataTransferItem.prototype.webkitGetAsEntry !== "undefined"
) {
this.$store.commit("showHover", "upload");
} else {
document.getElementById("upload-input").click();
}
},
uploadInput(event) {
this.$store.commit("closeHovers");
let files = event.currentTarget.files;
let folder_upload =
files[0].webkitRelativePath !== undefined && files[0].webkitRelativePath !== "";
if (folder_upload) {
for (let i = 0; i < files.length; i++) {
let file = files[i];
files[i].fullPath = file.webkitRelativePath;
}
}
let path = this.$route.path.endsWith("/")
? this.$route.path
: this.$route.path + "/";
let conflict = upload.checkConflict(files, this.req.items);
if (conflict) {
this.$store.commit("showHover", {
prompt: "replace",
confirm: (event) => {
event.preventDefault();
this.$store.commit("closeHovers");
upload.handleFiles(files, path, true);
},
});
return;
}
upload.handleFiles(files, path);
},
logout: auth.logout,
},
};
</script>
+65 -87
View File
@@ -10,18 +10,23 @@
@mouseup="mouseUp"
@wheel="wheelMove"
>
<div v-if="!isLoaded">Loading image...</div>
<img
src=""
class="image-ex-img image-ex-img-center"
v-if="!isTiff && isLoaded"
:src="src"
class="image-ex-img"
ref="imgex"
@load="onLoad"
/>
<canvas v-else-if="isLoaded" ref="imgex" class="image-ex-img"></canvas>
</div>
</template>
<script>
import throttle from "lodash.throttle";
import UTIF from "utif";
<script>
import { state, mutations } from "@/store";
import throttle from "@/utils/throttle";
import { notify } from "@/notify";
export default {
props: {
src: String,
@@ -55,15 +60,18 @@ export default {
},
maxScale: 4,
minScale: 0.25,
isTiff: false, // Determine if the image is a TIFF
};
},
mounted() {
if (!this.decodeUTIF()) {
this.isTiff = this.checkIfTiff(this.src);
if (this.isTiff) {
this.decodeTiff(this.src);
} else {
this.$refs.imgex.src = this.src;
}
let container = this.$refs.container;
this.classList.forEach((className) => container.classList.add(className));
// set width and height if they are zero
if (getComputedStyle(container).width === "0px") {
container.style.width = "100%";
}
@@ -77,61 +85,54 @@ export default {
window.removeEventListener("resize", this.onResize);
document.removeEventListener("mouseup", this.onMouseUp);
},
computed: {
isLoaded() {
return !("preview-img" in state.loading);
},
},
watch: {
src: function () {
if (!this.decodeUTIF()) {
if (this.src == undefined || this.$refs.imgex == undefined) {
mutations.setLoading("preview-img", false);
return;
}
this.isTiff = this.checkIfTiff(this.src);
if (this.isTiff) {
this.decodeTiff(this.src);
} else {
this.$refs.imgex.src = this.src;
}
this.scale = 1;
this.setZoom();
this.setCenter();
mutations.setLoading("preview-img", false);
this.showSpinner = false;
},
},
methods: {
// Modified from UTIF.replaceIMG
decodeUTIF() {
checkIfTiff(src) {
const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
let suff = document.location.pathname.split(".").pop().toLowerCase();
if (sufs.indexOf(suff) == -1) return false;
let xhr = new XMLHttpRequest();
UTIF._xhrs.push(xhr);
UTIF._imgs.push(this.$refs.imgex);
xhr.open("GET", this.src);
xhr.responseType = "arraybuffer";
xhr.onload = UTIF._imgLoaded;
xhr.send();
return true;
const suff = src.split(".").pop().toLowerCase();
return sufs.includes(suff);
},
onLoad() {
let img = this.$refs.imgex;
async decodeTiff(src) {
try {
const response = await fetch(src);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const blob = await response.blob(); // Convert response to a blob
const imgex = this.$refs.imgex;
this.imageLoaded = true;
if (img === undefined) {
return;
if (imgex) {
// Create a URL for the blob and set it as the image source
imgex.src = URL.createObjectURL(blob);
imgex.onload = () => URL.revokeObjectURL(imgex.src); // Clean up URL object after loading
}
} catch (error) {
notify.showError("Error decoding TIFF");
}
img.classList.remove("image-ex-img-center");
this.setCenter();
img.classList.add("image-ex-img-ready");
document.addEventListener("mouseup", this.onMouseUp);
let realSize = img.naturalWidth;
let displaySize = img.offsetWidth;
// Image is in portrait orientation
if (img.naturalHeight > img.naturalWidth) {
realSize = img.naturalHeight;
displaySize = img.offsetHeight;
}
// Scale needed to display the image on full size
const fullScale = realSize / displaySize;
// Full size plus additional zoom
this.maxScale = fullScale + 4;
},
onMouseUp() {
this.inDrag = false;
@@ -146,9 +147,7 @@ export default {
let container = this.$refs.container;
let img = this.$refs.imgex;
this.position.center.x = Math.floor(
(container.clientWidth - img.clientWidth) / 2
);
this.position.center.x = Math.floor((container.clientWidth - img.clientWidth) / 2);
this.position.center.y = Math.floor(
(container.clientHeight - img.clientHeight) / 2
);
@@ -242,32 +241,19 @@ export default {
}
},
doMove(x, y) {
let style = this.$refs.imgex.style;
let posX = this.pxStringToNumber(style.left) + x;
let posY = this.pxStringToNumber(style.top) + y;
style.left = posX + "px";
style.top = posY + "px";
this.position.relative.x = Math.abs(this.position.center.x - posX);
this.position.relative.y = Math.abs(this.position.center.y - posY);
if (posX < this.position.center.x) {
this.position.relative.x = this.position.relative.x * -1;
}
if (posY < this.position.center.y) {
this.position.relative.y = this.position.relative.y * -1;
}
this.position.relative.x += x;
this.position.relative.y += y;
// Update the transform with separate translate and scale values
this.$refs.imgex.style.transform = `translate(${this.position.relative.x}px, ${this.position.relative.y}px) scale(${this.scale})`;
},
wheelMove(event) {
this.scale += -Math.sign(event.deltaY) * this.zoomStep;
this.setZoom();
},
setZoom() {
this.scale = this.scale < this.minScale ? this.minScale : this.scale;
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale;
this.$refs.imgex.style.transform = `scale(${this.scale})`;
this.scale = Math.max(this.minScale, Math.min(this.maxScale, this.scale));
// Update the transform with both translate and scale values
this.$refs.imgex.style.transform = `translate(${this.position.relative.x}px, ${this.position.relative.y}px) scale(${this.scale})`;
},
pxStringToNumber(style) {
return +style.replace("px", "");
@@ -275,28 +261,20 @@ export default {
},
};
</script>
<style>
.image-ex-container {
margin: auto;
overflow: hidden;
position: relative;
max-width: 100%; /* Image container max width */
max-height: 100%; /* Image container max height */
overflow: hidden; /* Hide overflow if image exceeds container */
position: relative; /* Required for absolute positioning of child */
display: flex;
justify-content: center;
}
.image-ex-img {
max-width: 100%; /* Image max width */
max-height: 100%; /* Image max height */
position: absolute;
}
.image-ex-img-center {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
position: absolute;
transition: none;
}
.image-ex-img-ready {
left: 0;
top: 0;
transition: transform 0.1s ease;
}
</style>
Loaded 100 of 190 files, more files were not shown because too many files have changed in this diff. Show more