mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-08 10:25:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
415158d730 | ||
|
|
cd192744c2 | ||
|
|
906ac19be1 | ||
|
|
c272eb1e61 | ||
|
|
bae39c8586 | ||
|
|
62d1cd88a1 | ||
|
|
8767a43ee8 | ||
|
|
c1e846067e | ||
|
|
9bdc332ffd | ||
|
|
bfa3b6a7d8 | ||
|
|
66d09a4b75 | ||
|
|
3cb9308254 | ||
|
|
b01d663bbe | ||
|
|
5998f82e54 | ||
|
|
df7624fcfc | ||
|
|
8a72da8b30 | ||
|
|
125c0d6108 | ||
|
|
f7f08bba2a | ||
|
|
b49e5dbc81 | ||
|
|
abd5464b80 | ||
|
|
97dc393c9b | ||
|
|
a3f684ff82 | ||
|
|
876aabfcec | ||
|
|
e22abd3a4b | ||
|
|
01e82e282c | ||
|
|
58a4e07bf4 | ||
|
|
7c2ebb7801 | ||
|
|
ab0fdc0e17 | ||
|
|
c2a627d5c1 | ||
|
|
0bad14b51e | ||
|
|
74ccfab070 | ||
|
|
5f185597c9 | ||
|
|
9e9109984d | ||
|
|
5ebaf2a45b | ||
|
|
c7cce0694b | ||
|
|
43b45203d0 | ||
|
|
af407068b6 | ||
|
|
865fb4e243 | ||
|
|
c616b1473a | ||
|
|
0479670c52 | ||
|
|
fa3ed6b948 | ||
|
|
d8085c1f1b | ||
|
|
dc5da5662f | ||
|
|
b4bba3391d | ||
|
|
62f3953aea | ||
|
|
bc87dcd32c | ||
|
|
cdb49680e3 | ||
|
|
d9e583fea7 | ||
|
|
0281aee56f | ||
|
|
7aa3230649 | ||
|
|
398ac49aa9 | ||
|
|
ac544738a2 | ||
|
|
d574be9e89 | ||
|
|
b747ab19b7 | ||
|
|
e8091dcb24 | ||
|
|
8e4629a0c4 |
No files matched your search
@@ -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
|
||||
+12
-45
@@ -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
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
name: pr-merge
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
- '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:
|
||||
needs: [lint-frontend, lint-backend, test-backend, format-backend]
|
||||
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 }}
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -1,46 +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:
|
||||
@@ -50,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
|
||||
@@ -75,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 }}
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
|
||||
+146
-51
@@ -1,69 +1,164 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
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.1
|
||||
- issue #29 - Rules can now be configured and read from configuration yaml
|
||||
- issue #28 - Allow disable settings per user.
|
||||
- issue #27 - shorten download link for password protected files
|
||||
- issue #26 - enable dark mode per user and improve switching performance.
|
||||
- More rounded corners and improved listing styling
|
||||
- improve search performance
|
||||
- fixes authentication issues
|
||||
- adds compact view mode
|
||||
- improves view mode configuration and behavior
|
||||
- updates configuration file to accept new settings
|
||||
## v0.2.9
|
||||
|
||||
# v0.2.0
|
||||
- improved UI
|
||||
- more unified coehisive look
|
||||
- Adjusted header bar look and icon behavior
|
||||
- The shell is dead.
|
||||
- If you need to use custom commands, exec into the docker container.
|
||||
- The json config file is dead.
|
||||
- All configuration is done via advanced `filebrowser.yaml`
|
||||
- The only flag that is allowed is flag to specify config file.
|
||||
- Removed old code to migrate database versions
|
||||
- Removed all unused cmd code
|
||||
This release focused on UI navigation experience. Improving keyboard navigation and adds right click context menu.
|
||||
|
||||
# v0.1.4
|
||||
- various UI fixes
|
||||
- Added download button back to toolbar
|
||||
- Added upload button to side menu
|
||||
- breadcrumb spacing fix
|
||||
- Added "compact" view option
|
||||
- fixed slash issue with css rtl logic
|
||||
- various backend fixes
|
||||
- search has a sessionId attached so searches don't collide
|
||||
- search no longer searches by word with spaces, includes space in searches
|
||||
- prepared for full json configuration
|
||||
- made size search work for smaller and larger
|
||||
- made search types not show up in search bar when used
|
||||
**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
|
||||
- FIX: Hidden files are still directly accessible. (https://github.com/filebrowser/filebrowser/issues/2698)
|
||||
- FIX: search/user context bug
|
||||
|
||||
## v0.2.2
|
||||
|
||||
- CHG: **Speed:** (0m57s) - Decreased by 78% compared to the previous release.
|
||||
- CHG: **Memory Usage:** (41MB) - Reduced by 45% compared to the previous release.
|
||||
- Feature: Now utilizes the index for file browser listings!
|
||||
- FIX: Editor issues fixed on save and themes.
|
||||
|
||||
## v0.2.1
|
||||
|
||||
- Addressed issue #29 - Rules can now be configured and read from the configuration YAML.
|
||||
- Addressed issue #28 - Allows disabling settings per user.
|
||||
- Addressed issue #27 - Shortened download link for password-protected files.
|
||||
- Addressed issue #26 - Enables dark mode per user and improves switching performance.
|
||||
- Improved styling with more rounded corners and enhanced listing design.
|
||||
- Enhanced search performance.
|
||||
- Fixed authentication issues.
|
||||
- Added compact view mode.
|
||||
- Improved view mode configuration and behavior.
|
||||
- Updated the configuration file to accept new settings.
|
||||
|
||||
## v0.2.0
|
||||
|
||||
- **Improved UI:**
|
||||
- Enhanced the cohesive and unified look.
|
||||
- Adjusted the header bar appearance and icon behavior.
|
||||
- The shell feature has been deprecated.
|
||||
- Custom commands can be executed within the Docker container if needed.
|
||||
- The JSON config file is no longer used.
|
||||
- All configurations are now performed via the advanced `filebrowser.yaml`.
|
||||
- The only allowed flag is specifying the config file.
|
||||
- Removed old code for migrating database versions.
|
||||
- Eliminated all unused `cmd` code.
|
||||
|
||||
## v0.1.4
|
||||
|
||||
- **Various UI fixes:**
|
||||
- Reintroduced the download button to the toolbar.
|
||||
- Added the upload button to the side menu.
|
||||
- Adjusted breadcrumb spacing.
|
||||
- Introduced a "compact" view option.
|
||||
- Fixed a slash issue with CSS right-to-left (RTL) logic.
|
||||
- **Various backend improvements:**
|
||||
- Added session IDs to searches to prevent collisions.
|
||||
- Modified search behavior to include spaces in searches.
|
||||
- Prepared for full JSON configuration support.
|
||||
- Made size-based searches work for both smaller and larger files.
|
||||
- Modified search types not to appear in the search bar when used.
|
||||
|
||||
## v0.1.3
|
||||
|
||||
- improved styling, colors, transparency, blur
|
||||
- Made sidebar hidden on desktop as well
|
||||
- simplified navbar to be three buttons
|
||||
- open menu
|
||||
- search
|
||||
- toggle view
|
||||
- Changed desktop search style and included additional search options.
|
||||
- Enhanced styling with improved colors, transparency, and blur effects.
|
||||
- Hid the sidebar on desktop views.
|
||||
- Simplified the navbar to include three buttons:
|
||||
- Open menu
|
||||
- Search
|
||||
- Toggle view
|
||||
- Revised desktop search style and included additional search options.
|
||||
|
||||
## v0.1.2
|
||||
|
||||
- Updated UI to use search features better
|
||||
- More filter options
|
||||
- Better icons with colors
|
||||
- GUI styling
|
||||
- Improved search performance
|
||||
- Updated the UI to better utilize search features:
|
||||
- Added more filter options.
|
||||
- Enhanced icons with colors.
|
||||
- Improved GUI styling.
|
||||
- Improved search performance.
|
||||
- **Index Changes:**
|
||||
- **Speed:** (0m32s) - Increased by 6% compared to the previous release.
|
||||
- **Memory Usage:** (93MB) - Increased by 3% compared to the previous release.
|
||||
|
||||
## v0.1.1
|
||||
|
||||
- Improved search with indexing
|
||||
- Improved search functionality with indexing.
|
||||
- **Index Changes (Baseline Results):**
|
||||
- **Speed:** (0m30s)
|
||||
- **Memory Usage:** (90MB)
|
||||
|
||||
## v0.1.0
|
||||
|
||||
- nothing changed from origin.
|
||||
- No changes from the original.
|
||||
|
||||
Forked from https://github.com/filebrowser/filebrowser
|
||||
Forked from [filebrowser/filebrowser](https://github.com/filebrowser/filebrowser).
|
||||
+15
-14
@@ -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" ]
|
||||
@@ -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
|
||||
@@ -2,72 +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="500" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/b45683b0-bd55-4430-9831-650fe0d21eb8" title="Main Screenshot">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/8ba93582-aba2-4996-8ac3-25f763a2e596" title="Main Screenshot">
|
||||
</p>
|
||||
|
||||
> **NOTE**
|
||||
Intended for docker use only
|
||||
> [!WARNING]
|
||||
> Starting with v0.2.0, *ALL* configuration is done via `filebrowser.yaml`
|
||||
> Configuration file.
|
||||
> Starting with v0.2.4 *ALL* share links need to be re-created (due to
|
||||
> security fix).
|
||||
|
||||
> **Warning**
|
||||
Starting with v0.2.0, *ALL* configuration is done via `filebrowser.yaml` configuration file. `.filebrowser.json` and any flags other than `-c` and `-config` during execution WILL NO LONGER WORK. This is by design, in order to use the v0.2.0 You can mount your directory and initialize a new DB with a new default `filebrowser.yaml` which you can tweak and use in the future. Or you can copy and paste the default startup `filebrowser.yaml` below.
|
||||
FileBrowser Quantum is a fork of the filebrowser opensource project with the
|
||||
following changes:
|
||||
|
||||
This fork makes the following significant changes to filebrowser for origin:
|
||||
|
||||
1. [x] Improves search to use index instead of filesystem.
|
||||
- Lightning fast, realtime results as you type
|
||||
- Works with more type filters
|
||||
1. [x] Improved and simplified GUI navbar and sidebar menu.
|
||||
1. [x] Updated version and dependencies.
|
||||
1. [x] **IMPORTANT** Moved all configurations to `filebrowser.yaml`.
|
||||
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
|
||||
|
||||
## 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>
|
||||
|
||||
## Search Performance
|
||||
|
||||
100x faster search. However, this will be at expense of RAM. if you have < 1 million
|
||||
files and folders in the given scope, the RAM usage should be less than 200MB total. RAM requirements
|
||||
should scale based on the number of directories.
|
||||
|
||||
Also , the approx. time to fully index will vary widely based on performance. A sufficiently performant
|
||||
system should fully index within the first 5 minutes, potentially within the first few seconds.
|
||||
|
||||
For example, a low end 11th gen i5 with SSD indexes 128K files within 1 second:
|
||||
|
||||
```
|
||||
2023/09/09 21:38:50 Initializing with config file: filebrowser.yaml
|
||||
2023/09/09 21:38:50 Indexing files...
|
||||
2023/09/09 21:38:50 Listening on [::]:8080
|
||||
2023/09/09 21:38:51 Successfully indexed files.
|
||||
2023/09/09 21:38:51 Files found : 123452
|
||||
2023/09/09 21:38:51 Directories found : 1768
|
||||
2023/09/09 21:38:51 Indexing scheduler will run every 5 minutes
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
Using docker:
|
||||
@@ -75,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
|
||||
|
||||
@@ -91,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
|
||||
```
|
||||
@@ -107,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:
|
||||
@@ -119,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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
@@ -44,8 +43,8 @@ func (a *HookAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
|
||||
}
|
||||
|
||||
a.Users = usr
|
||||
a.Settings = &settings.GlobalConfiguration
|
||||
a.Server = &settings.GlobalConfiguration.Server
|
||||
a.Settings = &settings.Config
|
||||
a.Server = &settings.Config.Server
|
||||
a.Cred = cred
|
||||
|
||||
action, err := a.RunCommand()
|
||||
@@ -146,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,
|
||||
@@ -189,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,
|
||||
@@ -207,7 +205,7 @@ func (a *HookAuth) GetUser(d *users.User) *users.User {
|
||||
Locale: d.Locale,
|
||||
ViewMode: d.ViewMode,
|
||||
SingleClick: d.SingleClick,
|
||||
Sorting: files.Sorting{
|
||||
Sorting: users.Sorting{
|
||||
Asc: d.Sorting.Asc,
|
||||
By: d.Sorting.By,
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@ type JSONAuth struct {
|
||||
|
||||
// Auth authenticates the user via a json in content body.
|
||||
func (a JSONAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
|
||||
config := &settings.GlobalConfiguration
|
||||
config := &settings.Config
|
||||
var cred jsonCred
|
||||
|
||||
if r.Body == nil {
|
||||
|
||||
@@ -15,7 +15,7 @@ type NoAuth struct{}
|
||||
|
||||
// Auth uses authenticates user 1.
|
||||
func (a NoAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
|
||||
return usr.Get(settings.GlobalConfiguration.Server.Root, uint(1))
|
||||
return usr.Get(settings.Config.Server.Root, uint(1))
|
||||
}
|
||||
|
||||
// LoginPage tells that no auth doesn't require a login page.
|
||||
|
||||
@@ -21,7 +21,7 @@ type ProxyAuth struct {
|
||||
// Auth authenticates the user via an HTTP header.
|
||||
func (a ProxyAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
|
||||
username := r.Header.Get(a.Header)
|
||||
user, err := usr.Get(settings.GlobalConfiguration.Server.Root, username)
|
||||
user, err := usr.Get(settings.Config.Server.Root, username)
|
||||
if err == errors.ErrNotExist {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
@@ -7,48 +7,35 @@
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/diskcache 0.004s
|
||||
? github.com/gtsteffaniak/filebrowser/errors [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/files [no test files]
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/fileutils 0.003s
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 h: 401 <nil>
|
||||
2023/10/18 10:19:52 h: 401 <nil>
|
||||
2023/10/18 10:19:52 h: 401 <nil>
|
||||
2023/10/18 10:19:52 h: 401 <nil>
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:53 h: 401 <nil>
|
||||
2023/10/18 10:19:53 h: 401 <nil>
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/http 0.208s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/img 0.124s
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
pkg: github.com/gtsteffaniak/filebrowser/index
|
||||
pkg: github.com/gtsteffaniak/filebrowser/files
|
||||
cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz
|
||||
BenchmarkFillIndex-8 10 3239196 ns/op 11289 B/op 448 allocs/op
|
||||
BenchmarkSearchAllIndexes-8 10 6645964 ns/op 3176834 B/op 59104 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/index 0.126s
|
||||
ok github.com/gtsteffaniak/filebrowser/files 0.417s
|
||||
PASS
|
||||
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.100s
|
||||
PASS
|
||||
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]
|
||||
@@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
||||
+72
-41
@@ -3,6 +3,7 @@ package cmd
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
@@ -12,20 +13,27 @@ import (
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"embed"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/diskcache"
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
fbhttp "github.com/gtsteffaniak/filebrowser/http"
|
||||
"github.com/gtsteffaniak/filebrowser/img"
|
||||
"github.com/gtsteffaniak/filebrowser/index"
|
||||
"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,14 +48,15 @@ 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)
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "filebrowser",
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
serverConfig := settings.GlobalConfiguration.Server
|
||||
serverConfig := settings.Config.Server
|
||||
if !d.hadDB {
|
||||
quickSetup(d)
|
||||
}
|
||||
@@ -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 index.Initialize(serverConfig.IndexingInterval)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,29 +150,28 @@ func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfac
|
||||
}
|
||||
|
||||
func quickSetup(d pythonData) {
|
||||
settings.GlobalConfiguration.Auth.Key = generateKey()
|
||||
if settings.GlobalConfiguration.Auth.Method == "noauth" {
|
||||
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.GlobalConfiguration.Auth.Method = "password"
|
||||
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.GlobalConfiguration)
|
||||
checkErr(err)
|
||||
err = d.store.Settings.SaveServer(&settings.GlobalConfiguration.Server)
|
||||
checkErr(err)
|
||||
user := &users.User{}
|
||||
settings.GlobalConfiguration.UserDefaults.Apply(user)
|
||||
user.Username = settings.GlobalConfiguration.Auth.AdminUsername
|
||||
user.Password = settings.GlobalConfiguration.Auth.AdminPassword
|
||||
err := d.store.Settings.Save(&settings.Config)
|
||||
checkErr("d.store.Settings.Save", err)
|
||||
err = d.store.Settings.SaveServer(&settings.Config.Server)
|
||||
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
|
||||
user.Scope = "./"
|
||||
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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{}),
|
||||
}
|
||||
@@ -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{}),
|
||||
}
|
||||
@@ -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
@@ -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{}),
|
||||
}
|
||||
|
||||
@@ -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{}),
|
||||
}
|
||||
@@ -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{}),
|
||||
}
|
||||
+13
-11
@@ -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
|
||||
}
|
||||
|
||||
@@ -83,7 +84,7 @@ func dbExists(path string) (bool, error) {
|
||||
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
|
||||
return func(cmd *cobra.Command, args []string) {
|
||||
data := pythonData{hadDB: true}
|
||||
path := settings.GlobalConfiguration.Server.Database
|
||||
path := settings.Config.Server.Database
|
||||
exists, err := dbExists(path)
|
||||
|
||||
if err != nil {
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
server:
|
||||
port: 8080
|
||||
baseURL: "/"
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
root: "./srv"
|
||||
auth:
|
||||
method: password
|
||||
signup: true
|
||||
signup: false
|
||||
userDefaults:
|
||||
darkMode: true
|
||||
disableSettings: false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package index
|
||||
package files
|
||||
|
||||
import (
|
||||
"mime"
|
||||
@@ -15,22 +15,30 @@ var AllFiletypeOptions = []string{
|
||||
"video",
|
||||
"doc",
|
||||
"dir",
|
||||
"text",
|
||||
}
|
||||
var documentTypes = []string{
|
||||
".word",
|
||||
".pdf",
|
||||
".txt",
|
||||
".doc",
|
||||
".docx",
|
||||
}
|
||||
var textTypes = []string{
|
||||
".text",
|
||||
".sh",
|
||||
".yaml",
|
||||
".yml",
|
||||
".json",
|
||||
".env",
|
||||
}
|
||||
|
||||
var compressedFile = []string{
|
||||
".7z",
|
||||
".rar",
|
||||
".zip",
|
||||
".tar",
|
||||
".tar.gz",
|
||||
".tar.xz",
|
||||
".gz",
|
||||
".xz",
|
||||
}
|
||||
|
||||
type SearchOptions struct {
|
||||
@@ -137,12 +145,25 @@ func IsMatchingType(extension string, matchType string) bool {
|
||||
switch matchType {
|
||||
case "doc":
|
||||
return isDoc(extension)
|
||||
case "pdf":
|
||||
return extension == ".pdf"
|
||||
case "text":
|
||||
return isText(extension)
|
||||
case "archive":
|
||||
return isArchive(extension)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isText(extension string) bool {
|
||||
for _, typefile := range textTypes {
|
||||
if extension == typefile {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isDoc(extension string) bool {
|
||||
for _, typefile := range documentTypes {
|
||||
if extension == typefile {
|
||||
+375
-161
@@ -1,40 +1,47 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"crypto/md5" //nolint:gosec
|
||||
"crypto/sha1" //nolint:gosec
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
bytesInMegabyte int64 = 1000000
|
||||
pathMutexes = make(map[string]*sync.Mutex)
|
||||
pathMutexesMu sync.Mutex // Mutex to protect the pathMutexes map
|
||||
)
|
||||
|
||||
// FileInfo describes a file.
|
||||
type FileInfo struct {
|
||||
*Listing
|
||||
Fs afero.Fs `json:"-"`
|
||||
Path string `json:"path"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Extension string `json:"extension"`
|
||||
Extension string `json:"-"`
|
||||
ModTime time.Time `json:"modified"`
|
||||
Mode os.FileMode `json:"mode"`
|
||||
IsDir bool `json:"isDir"`
|
||||
IsSymlink bool `json:"isSymlink"`
|
||||
CacheTime time.Time `json:"-"`
|
||||
Mode os.FileMode `json:"-"`
|
||||
IsDir bool `json:"isDir,omitempty"`
|
||||
IsSymlink bool `json:"isSymlink,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Subtitles []string `json:"subtitles,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
@@ -44,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
|
||||
@@ -54,6 +61,22 @@ type FileOptions struct {
|
||||
Content bool
|
||||
}
|
||||
|
||||
// Sorting constants
|
||||
const (
|
||||
SortingByName = "name"
|
||||
SortingBySize = "size"
|
||||
SortingByModified = "modified"
|
||||
)
|
||||
|
||||
// Listing is a collection of files.
|
||||
type Listing struct {
|
||||
Items []*FileInfo `json:"items"`
|
||||
Path string `json:"path"`
|
||||
NumDirs int `json:"numDirs"`
|
||||
NumFiles int `json:"numFiles"`
|
||||
Sorting users.Sorting `json:"sorting"`
|
||||
}
|
||||
|
||||
// NewFileInfo creates a File object from a path and a given user. This File
|
||||
// object will be automatically filled depending on if it is a directory
|
||||
// or a file. If it's a video file, it will also detect any subtitles.
|
||||
@@ -61,84 +84,112 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
|
||||
if !opts.Checker.Check(opts.Path) {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
file, err := stat(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.Expand {
|
||||
if file.IsDir {
|
||||
if err := file.readListing(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
|
||||
}
|
||||
|
||||
err = file.detectType(opts.Modify, opts.Content, true)
|
||||
err = file.detectType(opts.Path, opts.Modify, opts.Content, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return file, err
|
||||
}
|
||||
func FileInfoFaster(opts FileOptions) (*FileInfo, error) {
|
||||
// Lock access for the specific path
|
||||
pathMutex := getMutex(opts.Path)
|
||||
pathMutex.Lock()
|
||||
defer pathMutex.Unlock()
|
||||
if !opts.Checker.Check(opts.Path) {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
index := GetIndex(rootPath)
|
||||
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
|
||||
}
|
||||
info, exists := index.GetMetadataInfo(adjustedPath)
|
||||
if !exists || info.Name == "" {
|
||||
return &FileInfo{}, errors.ErrEmptyKey
|
||||
}
|
||||
return &info, nil
|
||||
|
||||
}
|
||||
|
||||
func RefreshFileInfo(opts FileOptions) error {
|
||||
if !opts.Checker.Check(opts.Path) {
|
||||
return fmt.Errorf("permission denied: %s", opts.Path)
|
||||
}
|
||||
index := GetIndex(rootPath)
|
||||
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)
|
||||
}
|
||||
_ = 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 fmt.Errorf("Dir info could not be read: %s", opts.Path)
|
||||
}
|
||||
}
|
||||
result := index.UpdateFileMetadata(adjustedPath, *file)
|
||||
if !result {
|
||||
return fmt.Errorf("File/folder does not exist in metadata: %s", adjustedPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stat(opts FileOptions) (*FileInfo, error) {
|
||||
var file *FileInfo
|
||||
|
||||
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
|
||||
info, _, err := lstaterFs.LstatIfPossible(opts.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file = &FileInfo{
|
||||
Fs: opts.Fs,
|
||||
Path: opts.Path,
|
||||
Name: info.Name(),
|
||||
ModTime: info.ModTime(),
|
||||
Mode: info.Mode(),
|
||||
IsDir: info.IsDir(),
|
||||
IsSymlink: IsSymlink(info.Mode()),
|
||||
Size: info.Size(),
|
||||
Extension: filepath.Ext(info.Name()),
|
||||
Token: opts.Token,
|
||||
}
|
||||
}
|
||||
|
||||
// regular file
|
||||
if file != nil && !file.IsSymlink {
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// fs doesn't support afero.Lstater interface or the file is a symlink
|
||||
info, err := opts.Fs.Stat(opts.Path)
|
||||
info, err := os.Lstat(opts.Path)
|
||||
if err != nil {
|
||||
// can't follow symlink
|
||||
if file != nil && file.IsSymlink {
|
||||
return file, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set correct file size in case of symlink
|
||||
if file != nil && file.IsSymlink {
|
||||
file.Size = info.Size()
|
||||
file.IsDir = info.IsDir()
|
||||
return file, nil
|
||||
}
|
||||
|
||||
file = &FileInfo{
|
||||
Fs: opts.Fs,
|
||||
file := &FileInfo{
|
||||
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,
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
||||
@@ -154,25 +205,21 @@ 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
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
var h hash.Hash
|
||||
hashFuncs := map[string]hash.Hash{
|
||||
"md5": md5.New(),
|
||||
"sha1": sha1.New(),
|
||||
"sha256": sha256.New(),
|
||||
"sha512": sha512.New(),
|
||||
}
|
||||
|
||||
//nolint:gosec
|
||||
switch algo {
|
||||
case "md5":
|
||||
h = md5.New()
|
||||
case "sha1":
|
||||
h = sha1.New()
|
||||
case "sha256":
|
||||
h = sha256.New()
|
||||
case "sha512":
|
||||
h = sha512.New()
|
||||
default:
|
||||
h, ok := hashFuncs[algo]
|
||||
if !ok {
|
||||
return errors.ErrInvalidOption
|
||||
}
|
||||
|
||||
@@ -185,94 +232,209 @@ func (i *FileInfo) Checksum(algo string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// TODO: use constants
|
||||
//
|
||||
//nolint:goconst
|
||||
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
|
||||
if IsNamedPipe(i.Mode) {
|
||||
i.Type = "blob"
|
||||
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
|
||||
}
|
||||
|
||||
// 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 i.IsDir {
|
||||
return nil
|
||||
}
|
||||
if IsNamedPipe(i.Mode) {
|
||||
i.Type = "blob"
|
||||
if saveContent {
|
||||
return i.addContent(path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// failing to detect the type should not return error.
|
||||
// imagine the situation where a file in a dir with thousands
|
||||
// of files couldn't be opened: we'd have immediately
|
||||
// a 500 even though it doesn't matter. So we just log it.
|
||||
|
||||
mimetype := mime.TypeByExtension(i.Extension)
|
||||
|
||||
var buffer []byte
|
||||
if readHeader {
|
||||
buffer = i.readFirstBytes()
|
||||
|
||||
mimetype := mime.TypeByExtension(i.Extension)
|
||||
if mimetype == "" {
|
||||
mimetype = http.DetectContentType(buffer)
|
||||
http.DetectContentType(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(mimetype, "video"):
|
||||
i.Type = "video"
|
||||
i.detectSubtitles()
|
||||
return nil
|
||||
case strings.HasPrefix(mimetype, "audio"):
|
||||
i.Type = "audio"
|
||||
return nil
|
||||
case strings.HasPrefix(mimetype, "image"):
|
||||
i.Type = "image"
|
||||
return nil
|
||||
case strings.HasSuffix(mimetype, "pdf"):
|
||||
i.Type = "pdf"
|
||||
return nil
|
||||
case (strings.HasPrefix(mimetype, "text") || !isBinary(buffer)) && i.Size <= 10*1024*1024: // 10 MB
|
||||
i.Type = "text"
|
||||
|
||||
if !modify {
|
||||
i.Type = "textImmutable"
|
||||
ext := filepath.Ext(i.Name)
|
||||
for _, fileType := range AllFiletypeOptions {
|
||||
if IsMatchingType(ext, fileType) {
|
||||
i.Type = fileType
|
||||
}
|
||||
|
||||
if saveContent {
|
||||
afs := &afero.Afero{Fs: i.Fs}
|
||||
content, err := afs.ReadFile(i.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
switch i.Type {
|
||||
case "text":
|
||||
if !modify {
|
||||
i.Type = "textImmutable"
|
||||
}
|
||||
if saveContent {
|
||||
return i.addContent(path)
|
||||
}
|
||||
case "video":
|
||||
parentDir := strings.TrimRight(path, i.Name)
|
||||
i.detectSubtitles(parentDir)
|
||||
case "doc":
|
||||
if ext == ".pdf" {
|
||||
i.Type = "pdf"
|
||||
return nil
|
||||
}
|
||||
if saveContent {
|
||||
return i.addContent(path)
|
||||
}
|
||||
|
||||
i.Content = string(content)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
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 {
|
||||
log.Print(err)
|
||||
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 {
|
||||
log.Print(err)
|
||||
i.Type = "blob"
|
||||
return nil
|
||||
}
|
||||
@@ -280,44 +442,65 @@ func (i *FileInfo) readFirstBytes() []byte {
|
||||
return buffer[:n]
|
||||
}
|
||||
|
||||
func (i *FileInfo) detectSubtitles() {
|
||||
// detectSubtitles detects subtitles for video files.
|
||||
func (i *FileInfo) detectSubtitles(parentDir string) {
|
||||
if i.Type != "video" {
|
||||
return
|
||||
}
|
||||
|
||||
i.Subtitles = []string{}
|
||||
ext := filepath.Ext(i.Path)
|
||||
ext := filepath.Ext(i.Name)
|
||||
dir, err := os.Open(parentDir)
|
||||
if err != nil {
|
||||
// Directory must have been deleted, remove it from the index
|
||||
return
|
||||
}
|
||||
defer dir.Close() // Ensure directory handle is closed
|
||||
|
||||
// detect multiple languages. Base*.vtt
|
||||
// TODO: give subtitles descriptive names (lang) and track attributes
|
||||
parentDir := strings.TrimRight(i.Path, i.Name)
|
||||
dir, err := afero.ReadDir(i.Fs, parentDir)
|
||||
if err == nil {
|
||||
base := strings.TrimSuffix(i.Name, ext)
|
||||
for _, f := range dir {
|
||||
if !f.IsDir() && strings.HasPrefix(f.Name(), base) && strings.HasSuffix(f.Name(), ".vtt") {
|
||||
i.Subtitles = append(i.Subtitles, path.Join(parentDir, f.Name()))
|
||||
files, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
base := strings.TrimSuffix(i.Name, ext)
|
||||
subtitleExts := []string{".vtt", ".txt", ".srt", ".lrc"}
|
||||
|
||||
for _, f := range files {
|
||||
if f.IsDir() || !strings.HasPrefix(f.Name(), base) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, subtitleExt := range subtitleExts {
|
||||
if strings.HasSuffix(f.Name(), subtitleExt) {
|
||||
i.Subtitles = append(i.Subtitles, filepath.Join(parentDir, f.Name()))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
||||
afs := &afero.Afero{Fs: i.Fs}
|
||||
dir, err := afs.ReadDir(i.Path)
|
||||
// readListing reads the contents of a directory and fills the listing.
|
||||
func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bool) error {
|
||||
dir, err := os.Open(i.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
files, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listing := &Listing{
|
||||
Items: []*FileInfo{},
|
||||
Path: i.Path,
|
||||
NumDirs: 0,
|
||||
NumFiles: 0,
|
||||
}
|
||||
|
||||
for _, f := range dir {
|
||||
for _, f := range files {
|
||||
name := f.Name()
|
||||
fPath := path.Join(i.Path, name)
|
||||
fPath := filepath.Join(i.Path, name)
|
||||
|
||||
if !checker.Check(fPath) {
|
||||
continue
|
||||
@@ -326,9 +509,7 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
||||
isSymlink, isInvalidLink := false, false
|
||||
if IsSymlink(f.Mode()) {
|
||||
isSymlink = true
|
||||
// It's a symbolic link. We try to follow it. If it doesn't work,
|
||||
// we stay with the link information instead of the target's.
|
||||
info, err := i.Fs.Stat(fPath)
|
||||
info, err := os.Stat(fPath)
|
||||
if err == nil {
|
||||
f = info
|
||||
} else {
|
||||
@@ -337,15 +518,16 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
||||
}
|
||||
|
||||
file := &FileInfo{
|
||||
Fs: i.Fs,
|
||||
Name: name,
|
||||
Size: f.Size(),
|
||||
ModTime: f.ModTime(),
|
||||
Mode: f.Mode(),
|
||||
IsDir: f.IsDir(),
|
||||
IsSymlink: isSymlink,
|
||||
Extension: filepath.Ext(name),
|
||||
Path: fPath,
|
||||
Name: name,
|
||||
Size: f.Size(),
|
||||
ModTime: f.ModTime(),
|
||||
Mode: f.Mode(),
|
||||
}
|
||||
if f.IsDir() {
|
||||
file.IsDir = true
|
||||
}
|
||||
if isSymlink {
|
||||
file.IsSymlink = true
|
||||
}
|
||||
|
||||
if file.IsDir {
|
||||
@@ -356,7 +538,7 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
||||
if isInvalidLink {
|
||||
file.Type = "invalid_link"
|
||||
} else {
|
||||
err := file.detectType(true, false, readHeader)
|
||||
err := file.detectType(path, true, false, readHeader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -369,3 +551,35 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
||||
i.Listing = listing
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsNamedPipe(mode os.FileMode) bool {
|
||||
return mode&os.ModeNamedPipe != 0
|
||||
}
|
||||
|
||||
func IsSymlink(mode os.FileMode) bool {
|
||||
return mode&os.ModeSymlink != 0
|
||||
}
|
||||
|
||||
func getMutex(path string) *sync.Mutex {
|
||||
// Lock access to pathMutexes map
|
||||
pathMutexesMu.Lock()
|
||||
defer pathMutexesMu.Unlock()
|
||||
|
||||
// Create a mutex for the path if it doesn't exist
|
||||
if pathMutexes[path] == nil {
|
||||
pathMutexes[path] = &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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
)
|
||||
|
||||
type Directory struct {
|
||||
Metadata map[string]FileInfo
|
||||
Files string
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Name string
|
||||
IsDir bool
|
||||
}
|
||||
|
||||
type Index struct {
|
||||
Root string
|
||||
Directories map[string]Directory
|
||||
NumDirs int
|
||||
NumFiles int
|
||||
inProgress bool
|
||||
quickList []File
|
||||
LastIndexed time.Time
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
rootPath string = "/srv"
|
||||
indexes []*Index
|
||||
indexesMutex sync.RWMutex
|
||||
)
|
||||
|
||||
func InitializeIndex(intervalMinutes uint32, schedule bool) {
|
||||
if schedule {
|
||||
go indexingScheduler(intervalMinutes)
|
||||
}
|
||||
}
|
||||
|
||||
func indexingScheduler(intervalMinutes uint32) {
|
||||
if settings.Config.Server.Root != "" {
|
||||
rootPath = settings.Config.Server.Root
|
||||
}
|
||||
si := GetIndex(rootPath)
|
||||
log.Printf("Indexing Files...")
|
||||
log.Printf("Configured to run every %v minutes", intervalMinutes)
|
||||
log.Printf("Indexing from root: %s", si.Root)
|
||||
for {
|
||||
startTime := time.Now()
|
||||
// Set the indexing flag to indicate that indexing is in progress
|
||||
si.resetCount()
|
||||
// Perform the indexing operation
|
||||
err := si.indexFiles(si.Root)
|
||||
si.quickList = []File{}
|
||||
// Reset the indexing flag to indicate that indexing has finished
|
||||
si.inProgress = false
|
||||
// Update the LastIndexed time
|
||||
si.LastIndexed = time.Now()
|
||||
if err != nil {
|
||||
log.Printf("Error during indexing: %v", err)
|
||||
}
|
||||
if si.NumFiles+si.NumDirs > 0 {
|
||||
timeIndexedInSeconds := int(time.Since(startTime).Seconds())
|
||||
log.Println("Successfully indexed files.")
|
||||
log.Printf("Time spent indexing: %v seconds\n", timeIndexedInSeconds)
|
||||
log.Printf("Files found: %v\n", si.NumFiles)
|
||||
log.Printf("Directories found: %v\n", si.NumDirs)
|
||||
}
|
||||
// Sleep for the specified interval
|
||||
time.Sleep(time.Duration(intervalMinutes) * time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
adjustedPath := si.makeIndexPath(path, true)
|
||||
dir, err := os.Open(path)
|
||||
if err != nil {
|
||||
// Directory must have been deleted, remove it from the index
|
||||
si.RemoveDirectory(adjustedPath)
|
||||
}
|
||||
dirInfo, err := dir.Stat()
|
||||
if err != nil {
|
||||
dir.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
// Compare the last modified time of the directory with the last indexed time
|
||||
lastIndexed := si.LastIndexed
|
||||
if dirInfo.ModTime().Before(lastIndexed) {
|
||||
dir.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read the directory contents
|
||||
files, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir.Close()
|
||||
si.UpdateQuickList(files)
|
||||
si.InsertFiles(path)
|
||||
// done separately for memory efficiency on recursion
|
||||
si.InsertDirs(path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (si *Index) InsertFiles(path string) {
|
||||
adjustedPath := si.makeIndexPath(path, true)
|
||||
subDirectory := Directory{}
|
||||
buffer := bytes.Buffer{}
|
||||
|
||||
for _, f := range si.GetQuickList() {
|
||||
if !f.IsDir {
|
||||
buffer.WriteString(f.Name + ";")
|
||||
si.UpdateCount("files")
|
||||
}
|
||||
}
|
||||
// Use GetMetadataInfo and SetFileMetadata for safer read and write operations
|
||||
subDirectory.Files = buffer.String()
|
||||
si.SetDirectoryInfo(adjustedPath, subDirectory)
|
||||
}
|
||||
|
||||
func (si *Index) InsertDirs(path string) {
|
||||
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
|
||||
if adjustedPath == "/" {
|
||||
si.SetDirectoryInfo("/"+f.Name, Directory{})
|
||||
} else {
|
||||
si.SetDirectoryInfo(adjustedPath+"/"+f.Name, Directory{})
|
||||
}
|
||||
}
|
||||
err := si.indexFiles(path + "/" + f.Name)
|
||||
if err != nil {
|
||||
if err.Error() == "invalid argument" {
|
||||
log.Printf("Could not index \"%v\": %v \n", path, "Permission Denied")
|
||||
} else {
|
||||
log.Printf("Could not index \"%v\": %v \n", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (si *Index) makeIndexPath(subPath string, isDir bool) string {
|
||||
if si.Root == subPath {
|
||||
return "/"
|
||||
}
|
||||
// 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,4 +1,4 @@
|
||||
package index
|
||||
package files
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -6,28 +6,35 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
)
|
||||
|
||||
func BenchmarkFillIndex(b *testing.B) {
|
||||
indexes = Index{
|
||||
Dirs: make([]string, 0, 1000),
|
||||
Files: make([]string, 0, 1000),
|
||||
}
|
||||
InitializeIndex(5, false)
|
||||
si := GetIndex(settings.Config.Server.Root)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
createMockData(50, 3) // 1000 dirs, 3 files per dir
|
||||
si.createMockData(50, 3) // 1000 dirs, 3 files per dir
|
||||
}
|
||||
}
|
||||
|
||||
func createMockData(numDirs, numFilesPerDir int) {
|
||||
func (si *Index) createMockData(numDirs, numFilesPerDir int) {
|
||||
for i := 0; i < numDirs; i++ {
|
||||
dirName := generateRandomPath(rand.Intn(3) + 1)
|
||||
addToIndex("/", dirName, true)
|
||||
files := []File{}
|
||||
// Append a new Directory to the slice
|
||||
for j := 0; j < numFilesPerDir; j++ {
|
||||
fileName := "file-" + getRandomTerm() + getRandomExtension()
|
||||
addToIndex("/"+dirName, fileName, false)
|
||||
newFile := File{
|
||||
Name: "file-" + getRandomTerm() + getRandomExtension(),
|
||||
IsDir: false,
|
||||
}
|
||||
files = append(files, newFile)
|
||||
}
|
||||
si.UpdateQuickListForTests(files)
|
||||
si.InsertFiles(dirName)
|
||||
si.InsertDirs(dirName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +98,7 @@ func TestGetIndex(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := GetIndex(); !reflect.DeepEqual(got, tt.want) {
|
||||
if got := GetIndex("root"); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GetIndex() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
@@ -110,7 +117,7 @@ func TestInitializeIndex(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
Initialize(tt.args.intervalMinutes)
|
||||
InitializeIndex(tt.args.intervalMinutes, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -131,54 +138,3 @@ func Test_indexingScheduler(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_indexFiles(t *testing.T) {
|
||||
type args struct {
|
||||
path string
|
||||
numFiles *int
|
||||
numDirs *int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int
|
||||
want1 int
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1, err := indexFiles(tt.args.path, tt.args.numFiles, tt.args.numDirs)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("indexFiles() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("indexFiles() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("indexFiles() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_addToIndex(t *testing.T) {
|
||||
type args struct {
|
||||
path string
|
||||
fileName string
|
||||
isDir bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
addToIndex(tt.args.path, tt.args.fileName, tt.args.isDir)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/maruel/natural"
|
||||
)
|
||||
|
||||
// Listing is a collection of files.
|
||||
type Listing struct {
|
||||
Items []*FileInfo `json:"items"`
|
||||
NumDirs int `json:"numDirs"`
|
||||
NumFiles int `json:"numFiles"`
|
||||
Sorting Sorting `json:"sorting"`
|
||||
}
|
||||
|
||||
// ApplySort applies the sort order using .Order and .Sort
|
||||
//
|
||||
//nolint:goconst
|
||||
func (l Listing) ApplySort() {
|
||||
// Check '.Order' to know how to sort
|
||||
// TODO: use enum
|
||||
if !l.Sorting.Asc {
|
||||
switch l.Sorting.By {
|
||||
case "name":
|
||||
sort.Sort(sort.Reverse(byName(l)))
|
||||
case "size":
|
||||
sort.Sort(sort.Reverse(bySize(l)))
|
||||
case "modified":
|
||||
sort.Sort(sort.Reverse(byModified(l)))
|
||||
default:
|
||||
// If not one of the above, do nothing
|
||||
return
|
||||
}
|
||||
} else { // If we had more Orderings we could add them here
|
||||
switch l.Sorting.By {
|
||||
case "name":
|
||||
sort.Sort(byName(l))
|
||||
case "size":
|
||||
sort.Sort(bySize(l))
|
||||
case "modified":
|
||||
sort.Sort(byModified(l))
|
||||
default:
|
||||
sort.Sort(byName(l))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implement sorting for Listing
|
||||
type byName Listing
|
||||
type bySize Listing
|
||||
type byModified Listing
|
||||
|
||||
// By Name
|
||||
func (l byName) Len() int {
|
||||
return len(l.Items)
|
||||
}
|
||||
|
||||
func (l byName) Swap(i, j int) {
|
||||
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
|
||||
}
|
||||
|
||||
// Treat upper and lower case equally
|
||||
func (l byName) Less(i, j int) bool {
|
||||
if l.Items[i].IsDir && !l.Items[j].IsDir {
|
||||
return l.Sorting.Asc
|
||||
}
|
||||
|
||||
if !l.Items[i].IsDir && l.Items[j].IsDir {
|
||||
return !l.Sorting.Asc
|
||||
}
|
||||
|
||||
return natural.Less(strings.ToLower(l.Items[j].Name), strings.ToLower(l.Items[i].Name))
|
||||
}
|
||||
|
||||
// By Size
|
||||
func (l bySize) Len() int {
|
||||
return len(l.Items)
|
||||
}
|
||||
|
||||
func (l bySize) Swap(i, j int) {
|
||||
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
|
||||
}
|
||||
|
||||
const directoryOffset = -1 << 31 // = math.MinInt32
|
||||
func (l bySize) Less(i, j int) bool {
|
||||
iSize, jSize := l.Items[i].Size, l.Items[j].Size
|
||||
if l.Items[i].IsDir {
|
||||
iSize = directoryOffset + iSize
|
||||
}
|
||||
if l.Items[j].IsDir {
|
||||
jSize = directoryOffset + jSize
|
||||
}
|
||||
return iSize < jSize
|
||||
}
|
||||
|
||||
// By Modified
|
||||
func (l byModified) Len() int {
|
||||
return len(l.Items)
|
||||
}
|
||||
|
||||
func (l byModified) Swap(i, j int) {
|
||||
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
|
||||
}
|
||||
|
||||
func (l byModified) Less(i, j int) bool {
|
||||
iModified, jModified := l.Items[i].ModTime, l.Items[j].ModTime
|
||||
return iModified.Sub(jModified) < 0
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package index
|
||||
package files
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
@@ -12,64 +12,68 @@ import (
|
||||
|
||||
var (
|
||||
sessionInProgress sync.Map
|
||||
mutex sync.RWMutex
|
||||
maxSearchResults = 100
|
||||
bytesInMegabyte int64 = 1000000
|
||||
maxSearchResults = 100
|
||||
)
|
||||
|
||||
func (si *Index) Search(search string, scope string, sourceSession string) ([]string, map[string]map[string]bool) {
|
||||
if scope == "" {
|
||||
scope = "/"
|
||||
}
|
||||
fileTypes := map[string]bool{}
|
||||
|
||||
// Remove slashes
|
||||
scope = strings.TrimLeft(scope, "/")
|
||||
scope = strings.TrimRight(scope, "/")
|
||||
runningHash := generateRandomHash(4)
|
||||
sessionInProgress.Store(sourceSession, runningHash) // Store the value in the sync.Map
|
||||
searchOptions := ParseSearch(search)
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
fileListTypes := make(map[string]map[string]bool)
|
||||
matching := []string{}
|
||||
count := 0
|
||||
for _, searchTerm := range searchOptions.Terms {
|
||||
if searchTerm == "" {
|
||||
continue
|
||||
}
|
||||
// Iterate over the embedded index.Index fields Dirs and Files
|
||||
for _, i := range []string{"Dirs", "Files"} {
|
||||
isDir := false
|
||||
count := 0
|
||||
var paths []string
|
||||
|
||||
switch i {
|
||||
case "Dirs":
|
||||
isDir = true
|
||||
paths = si.Dirs
|
||||
case "Files":
|
||||
paths = si.Files
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
for dirName, dir := range si.Directories {
|
||||
isDir := true
|
||||
files := strings.Split(dir.Files, ";")
|
||||
value, found := sessionInProgress.Load(sourceSession)
|
||||
if !found || value != runningHash {
|
||||
return []string{}, map[string]map[string]bool{}
|
||||
}
|
||||
for _, path := range paths {
|
||||
if count > maxSearchResults {
|
||||
break
|
||||
}
|
||||
pathName := scopedPathNameFilter(dirName, scope, isDir)
|
||||
if pathName == "" {
|
||||
continue // path not matched
|
||||
}
|
||||
fileTypes := map[string]bool{}
|
||||
matches, fileType := containsSearchTerm(dirName, searchTerm, *searchOptions, isDir, fileTypes)
|
||||
if matches {
|
||||
fileListTypes[pathName] = fileType
|
||||
matching = append(matching, pathName)
|
||||
count++
|
||||
}
|
||||
isDir = false
|
||||
for _, file := range files {
|
||||
if file == "" {
|
||||
continue
|
||||
}
|
||||
value, found := sessionInProgress.Load(sourceSession)
|
||||
if !found || value != runningHash {
|
||||
return []string{}, map[string]map[string]bool{}
|
||||
}
|
||||
|
||||
if count > maxSearchResults {
|
||||
break
|
||||
}
|
||||
pathName := scopedPathNameFilter(path, scope)
|
||||
if pathName == "" {
|
||||
continue
|
||||
}
|
||||
fullName := strings.TrimLeft(pathName+file, "/")
|
||||
fileTypes := map[string]bool{}
|
||||
|
||||
matches, fileType := containsSearchTerm(path, searchTerm, *searchOptions, isDir, fileTypes)
|
||||
matches, fileType := containsSearchTerm(fullName, searchTerm, *searchOptions, isDir, fileTypes)
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
if isDir {
|
||||
fileListTypes[pathName+"/"] = fileType
|
||||
} else {
|
||||
fileListTypes[pathName] = fileType
|
||||
}
|
||||
matching = append(matching, pathName)
|
||||
fileListTypes[fullName] = fileType
|
||||
matching = append(matching, fullName)
|
||||
count++
|
||||
}
|
||||
}
|
||||
@@ -83,13 +87,17 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
|
||||
return matching, fileListTypes
|
||||
}
|
||||
|
||||
func scopedPathNameFilter(pathName string, scope string) string {
|
||||
scope = strings.TrimPrefix(scope, "/")
|
||||
pathName = strings.TrimPrefix(pathName, "/")
|
||||
if strings.HasPrefix(pathName, scope) {
|
||||
pathName = "/" + strings.TrimPrefix(pathName, scope)
|
||||
func scopedPathNameFilter(pathName string, scope string, isDir bool) string {
|
||||
pathName = strings.TrimLeft(pathName, "/")
|
||||
pathName = strings.TrimRight(pathName, "/")
|
||||
if strings.HasPrefix(pathName, scope) || scope == "" {
|
||||
pathName = strings.TrimPrefix(pathName, scope)
|
||||
pathName = strings.TrimLeft(pathName, "/")
|
||||
if isDir {
|
||||
pathName = pathName + "/"
|
||||
}
|
||||
} else {
|
||||
pathName = ""
|
||||
pathName = "" // return not matched
|
||||
}
|
||||
return pathName
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package index
|
||||
package files
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
@@ -8,12 +8,10 @@ import (
|
||||
)
|
||||
|
||||
func BenchmarkSearchAllIndexes(b *testing.B) {
|
||||
indexes = Index{
|
||||
Dirs: make([]string, 0, 1000),
|
||||
Files: make([]string, 0, 1000),
|
||||
}
|
||||
// Create mock data
|
||||
createMockData(50, 3) // 1000 dirs, 3 files per dir
|
||||
InitializeIndex(5, false)
|
||||
si := GetIndex(rootPath)
|
||||
|
||||
si.createMockData(50, 3) // 1000 dirs, 3 files per dir
|
||||
|
||||
// Generate 100 random search terms
|
||||
searchTerms := generateRandomSearchTerms(100)
|
||||
@@ -23,7 +21,7 @@ func BenchmarkSearchAllIndexes(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Execute the SearchAllIndexes function
|
||||
for _, term := range searchTerms {
|
||||
indexes.Search(term, "/", "test")
|
||||
si.Search(term, "/", "test")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,20 +74,37 @@ func TestParseSearch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchWhileIndexing(t *testing.T) {
|
||||
InitializeIndex(5, false)
|
||||
si := GetIndex(rootPath)
|
||||
// Generate 100 random search terms
|
||||
// Generate 100 random search terms
|
||||
searchTerms := generateRandomSearchTerms(10)
|
||||
for i := 0; i < 5; i++ {
|
||||
// Execute the SearchAllIndexes function
|
||||
go si.createMockData(100, 100) // 1000 dirs, 3 files per dir
|
||||
for _, term := range searchTerms {
|
||||
go si.Search(term, "/", "test")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchIndexes(t *testing.T) {
|
||||
indexes = Index{
|
||||
Dirs: []string{
|
||||
"/test",
|
||||
"/test/path",
|
||||
"/new/test/path",
|
||||
},
|
||||
Files: []string{
|
||||
"/test/path/file.txt",
|
||||
"/test/audio1.wav",
|
||||
"/new/test/audio.wav",
|
||||
"/new/test/video.mp4",
|
||||
"/new/test/video.MP4",
|
||||
"/new/test/path/archive.zip",
|
||||
index := Index{
|
||||
Directories: map[string]Directory{
|
||||
"test": {
|
||||
Files: "audio1.wav;",
|
||||
},
|
||||
"test/path": {
|
||||
Files: "file.txt;",
|
||||
},
|
||||
"new": {},
|
||||
"new/test": {
|
||||
Files: "audio.wav;video.mp4;video.MP4;",
|
||||
},
|
||||
"new/test/path": {
|
||||
Files: "archive.zip;",
|
||||
},
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
@@ -101,70 +116,79 @@ func TestSearchIndexes(t *testing.T) {
|
||||
{
|
||||
search: "audio",
|
||||
scope: "/new/",
|
||||
expectedResult: []string{"/test/audio.wav"},
|
||||
expectedResult: []string{"test/audio.wav"},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"/test/audio.wav": map[string]bool{"audio": true, "dir": false},
|
||||
"test/audio.wav": map[string]bool{"audio": true, "dir": false},
|
||||
},
|
||||
},
|
||||
{
|
||||
search: "test",
|
||||
scope: "/",
|
||||
expectedResult: []string{"/test"},
|
||||
expectedResult: []string{"test/", "new/test/"},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"/test/": map[string]bool{"dir": true},
|
||||
"test/": map[string]bool{"dir": true},
|
||||
"new/test/": map[string]bool{"dir": true},
|
||||
},
|
||||
},
|
||||
{
|
||||
search: "archive",
|
||||
scope: "/",
|
||||
expectedResult: []string{"/new/test/path/archive.zip"},
|
||||
expectedResult: []string{"new/test/path/archive.zip"},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"/new/test/path/archive.zip": map[string]bool{"archive": true, "dir": false},
|
||||
"new/test/path/archive.zip": map[string]bool{"archive": true, "dir": false},
|
||||
},
|
||||
},
|
||||
{
|
||||
search: "video",
|
||||
scope: "/",
|
||||
expectedResult: []string{
|
||||
"/new/test/video.mp4",
|
||||
"/new/test/video.MP4",
|
||||
"new/test/video.mp4",
|
||||
"new/test/video.MP4",
|
||||
},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"/new/test/video.MP4": map[string]bool{"video": true, "dir": false},
|
||||
"/new/test/video.mp4": map[string]bool{"video": true, "dir": false},
|
||||
"new/test/video.MP4": map[string]bool{"video": true, "dir": false},
|
||||
"new/test/video.mp4": map[string]bool{"video": true, "dir": false},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.search, func(t *testing.T) {
|
||||
actualResult, actualTypes := indexes.Search(tt.search, tt.scope, "")
|
||||
actualResult, actualTypes := index.Search(tt.search, tt.scope, "")
|
||||
assert.Equal(t, tt.expectedResult, actualResult)
|
||||
if len(tt.expectedTypes) > 0 {
|
||||
for key, value := range tt.expectedTypes {
|
||||
actualValue, exists := actualTypes[key]
|
||||
assert.True(t, exists, "Expected type key '%s' not found in actual types", key)
|
||||
assert.Equal(t, value, actualValue, "Type value mismatch for key '%s'", key)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.expectedTypes, actualTypes) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", actualTypes, tt.expectedTypes)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_scopedPathNameFilter(t *testing.T) {
|
||||
type args struct {
|
||||
pathName string
|
||||
scope string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
args struct {
|
||||
pathName string
|
||||
scope string
|
||||
isDir bool // Assuming isDir should be included in args
|
||||
}
|
||||
want string
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
{
|
||||
name: "scope test",
|
||||
args: struct {
|
||||
pathName string
|
||||
scope string
|
||||
isDir bool
|
||||
}{
|
||||
pathName: "/",
|
||||
scope: "/",
|
||||
isDir: false,
|
||||
},
|
||||
want: "", // Update this with the expected result
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := scopedPathNameFilter(tt.args.pathName, tt.args.scope); got != tt.want {
|
||||
if got := scopedPathNameFilter(tt.args.pathName, tt.args.scope, tt.args.isDir); got != tt.want {
|
||||
t.Errorf("scopedPathNameFilter() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
package files
|
||||
|
||||
// Sorting contains a sorting order.
|
||||
type Sorting struct {
|
||||
By string `json:"by"`
|
||||
Asc bool `json:"asc"`
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
)
|
||||
|
||||
// UpdateFileMetadata updates the FileInfo for the specified directory in the index.
|
||||
func (si *Index) UpdateFileMetadata(adjustedPath string, info FileInfo) bool {
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
dir, exists := si.Directories[adjustedPath]
|
||||
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 {
|
||||
_, exists := si.Directories[adjustedPath]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
info.CacheTime = time.Now()
|
||||
si.Directories[adjustedPath].Metadata[adjustedPath] = info
|
||||
return true
|
||||
}
|
||||
|
||||
// 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()
|
||||
if exists {
|
||||
// Initialize the Metadata map if it is nil
|
||||
if dir.Metadata == nil {
|
||||
dir.Metadata = make(map[string]FileInfo)
|
||||
si.SetDirectoryInfo(adjustedPath, dir)
|
||||
} else {
|
||||
fi = dir.Metadata[adjustedPath]
|
||||
}
|
||||
}
|
||||
return fi, exists
|
||||
}
|
||||
|
||||
// SetDirectoryInfo sets the directory information in the index.
|
||||
func (si *Index) SetDirectoryInfo(adjustedPath string, dir Directory) {
|
||||
si.mu.Lock()
|
||||
si.Directories[adjustedPath] = dir
|
||||
si.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetDirectoryInfo sets the directory information in the index.
|
||||
func (si *Index) GetDirectoryInfo(adjustedPath string) (Directory, bool) {
|
||||
si.mu.RLock()
|
||||
dir, exists := si.Directories[adjustedPath]
|
||||
si.mu.RUnlock()
|
||||
return dir, exists
|
||||
}
|
||||
|
||||
func (si *Index) RemoveDirectory(path string) {
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
delete(si.Directories, path)
|
||||
}
|
||||
|
||||
func (si *Index) UpdateCount(given string) {
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
if given == "files" {
|
||||
si.NumFiles++
|
||||
} else if given == "dirs" {
|
||||
si.NumDirs++
|
||||
} else {
|
||||
log.Println("could not update unknown type: ", given)
|
||||
}
|
||||
}
|
||||
|
||||
func (si *Index) resetCount() {
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
si.NumDirs = 0
|
||||
si.NumFiles = 0
|
||||
si.inProgress = true
|
||||
}
|
||||
|
||||
func GetIndex(root string) *Index {
|
||||
for _, index := range indexes {
|
||||
if index.Root == root {
|
||||
return index
|
||||
}
|
||||
}
|
||||
if settings.Config.Server.Root != "" {
|
||||
rootPath = settings.Config.Server.Root
|
||||
}
|
||||
newIndex := &Index{
|
||||
Root: rootPath,
|
||||
Directories: make(map[string]Directory), // Initialize the map
|
||||
NumDirs: 0,
|
||||
NumFiles: 0,
|
||||
inProgress: false,
|
||||
}
|
||||
indexesMutex.Lock()
|
||||
indexes = append(indexes, newIndex)
|
||||
indexesMutex.Unlock()
|
||||
return newIndex
|
||||
}
|
||||
|
||||
func (si *Index) UpdateQuickList(files []fs.FileInfo) {
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
si.quickList = []File{}
|
||||
for _, file := range files {
|
||||
newFile := File{
|
||||
Name: file.Name(),
|
||||
IsDir: file.IsDir(),
|
||||
}
|
||||
si.quickList = append(si.quickList, newFile)
|
||||
}
|
||||
}
|
||||
|
||||
func (si *Index) UpdateQuickListForTests(files []File) {
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
si.quickList = []File{}
|
||||
for _, file := range files {
|
||||
newFile := File{
|
||||
Name: file.Name,
|
||||
IsDir: file.IsDir,
|
||||
}
|
||||
si.quickList = append(si.quickList, newFile)
|
||||
}
|
||||
}
|
||||
|
||||
func (si *Index) GetQuickList() []File {
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
newQuickList := si.quickList
|
||||
return newQuickList
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"os"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func isBinary(content []byte) bool {
|
||||
maybeStr := string(content)
|
||||
runeCnt := utf8.RuneCount(content)
|
||||
runeIndex := 0
|
||||
gotRuneErrCnt := 0
|
||||
firstRuneErrIndex := -1
|
||||
|
||||
const (
|
||||
// 8 and below are control chars (e.g. backspace, null, eof, etc)
|
||||
maxControlCharsCode = 8
|
||||
// 0xFFFD(65533) is the "error" Rune or "Unicode replacement character"
|
||||
// see https://golang.org/pkg/unicode/utf8/#pkg-constants
|
||||
unicodeReplacementChar = 0xFFFD
|
||||
)
|
||||
|
||||
for _, b := range maybeStr {
|
||||
if b <= maxControlCharsCode {
|
||||
return true
|
||||
}
|
||||
|
||||
if b == unicodeReplacementChar {
|
||||
// if it is not the last (utf8.UTFMax - x) rune
|
||||
if runeCnt > utf8.UTFMax && runeIndex < runeCnt-utf8.UTFMax {
|
||||
return true
|
||||
}
|
||||
// else it is the last (utf8.UTFMax - x) rune
|
||||
// there maybe Vxxx, VVxx, VVVx, thus, we may got max 3 0xFFFD rune (assume V is the byte we got)
|
||||
// for Chinese, it can only be Vxx, VVx, we may got max 2 0xFFFD rune
|
||||
gotRuneErrCnt++
|
||||
|
||||
// mark the first time
|
||||
if firstRuneErrIndex == -1 {
|
||||
firstRuneErrIndex = runeIndex
|
||||
}
|
||||
}
|
||||
runeIndex++
|
||||
}
|
||||
|
||||
// if last (utf8.UTFMax - x ) rune has the "error" Rune, but not all
|
||||
if firstRuneErrIndex != -1 && gotRuneErrCnt != runeCnt-firstRuneErrIndex {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsNamedPipe(mode os.FileMode) bool {
|
||||
return mode&os.ModeNamedPipe != 0
|
||||
}
|
||||
|
||||
func IsSymlink(mode os.FileMode) bool {
|
||||
return mode&os.ModeSymlink != 0
|
||||
}
|
||||
+10
-10
@@ -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
@@ -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
@@ -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]
|
||||
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
+30
-32
@@ -1,58 +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/maruel/natural v1.1.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.12.0
|
||||
golang.org/x/image v0.12.0
|
||||
golang.org/x/text v0.13.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
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.10.0 // indirect
|
||||
golang.org/x/sys v0.11.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
-491
@@ -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,415 +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/maruel/natural v1.1.0 h1:2z1NgP/Vae+gYrtC0VuvrTJ6U35OuyUqDdfluLqMWuQ=
|
||||
github.com/maruel/natural v1.1.0/go.mod h1:eFVhYCcUOfZFxXoDZam8Ktya72wa79fNC3lc/leA0DQ=
|
||||
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.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
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.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
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 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
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/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
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=
|
||||
+11
-13
@@ -16,10 +16,6 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
const (
|
||||
TokenExpirationTime = time.Hour * 2
|
||||
)
|
||||
|
||||
type authToken struct {
|
||||
User users.User `json:"user"`
|
||||
jwt.RegisteredClaims
|
||||
@@ -112,7 +108,7 @@ type signupBody struct {
|
||||
}
|
||||
|
||||
var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
if !settings.GlobalConfiguration.Auth.Signup {
|
||||
if !settings.Config.Auth.Signup {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
@@ -130,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.GlobalConfiguration.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)
|
||||
@@ -142,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.GlobalConfiguration.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 {
|
||||
@@ -158,11 +152,15 @@ 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 {
|
||||
duration = time.Hour * 2
|
||||
}
|
||||
claims := &authToken{
|
||||
User: *user,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenExpirationTime)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(duration)),
|
||||
Issuer: "File Browser",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/tomasen/realip"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/rules"
|
||||
"github.com/gtsteffaniak/filebrowser/runner"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
@@ -27,9 +26,6 @@ type data struct {
|
||||
|
||||
// Check implements rules.Checker.
|
||||
func (d *data) Check(path string) bool {
|
||||
if d.user.HideDotfiles && rules.MatchHidden(path) {
|
||||
return false
|
||||
}
|
||||
|
||||
allow := true
|
||||
for _, rule := range d.settings.Rules {
|
||||
@@ -37,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
@@ -45,18 +46,17 @@ func previewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, re
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
file, err := files.NewFileInfo(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: "/" + vars["path"],
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: true,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
Checker: d,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
setContentDisposition(w, r, file)
|
||||
|
||||
switch file.Type {
|
||||
@@ -81,7 +81,6 @@ func handleImagePreview(
|
||||
(previewSize == PreviewSizeThumb && !enableThumbnails) {
|
||||
return rawFileHandler(w, r, file)
|
||||
}
|
||||
|
||||
format, err := imgSvc.FormatFromExtension(file.Extension)
|
||||
// Unsupported extensions directly return the raw data
|
||||
if err == img.ErrUnsupportedFormat || format == img.FormatGif {
|
||||
@@ -111,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
|
||||
}
|
||||
|
||||
+36
-60
@@ -2,44 +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.NewFileInfo(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
Path: link.Path,
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
Expand: true,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
Checker: d,
|
||||
Token: link.Token,
|
||||
@@ -47,67 +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.NewFileInfo(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 = files.Sorting{By: "name", Asc: false}
|
||||
file.Listing.ApplySort()
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+11
-9
@@ -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
|
||||
}
|
||||
|
||||
file, err := files.NewFileInfo(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.StatusInternalServerError, err
|
||||
}
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
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
|
||||
}
|
||||
|
||||
+78
-141
@@ -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,37 +18,32 @@ import (
|
||||
)
|
||||
|
||||
var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
file, err := files.NewFileInfo(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
|
||||
}
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
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
|
||||
file.Listing.ApplySort()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// do not waste bandwidth if we just want the checksum
|
||||
file.Content = ""
|
||||
}
|
||||
|
||||
return renderJSON(w, r, file)
|
||||
})
|
||||
|
||||
@@ -59,15 +52,19 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
|
||||
if r.URL.Path == "/" || !d.user.Perm.Delete {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
file, err := files.NewFileInfo(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
|
||||
}
|
||||
@@ -78,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
|
||||
})
|
||||
}
|
||||
@@ -95,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.NewFileInfo(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
|
||||
@@ -125,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
|
||||
})
|
||||
}
|
||||
@@ -155,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
|
||||
@@ -192,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)
|
||||
@@ -222,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)
|
||||
@@ -300,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)
|
||||
|
||||
file, err := files.NewFileInfo(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
Path: src,
|
||||
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{
|
||||
Path: realSrc,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: false,
|
||||
@@ -326,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)
|
||||
}
|
||||
@@ -338,14 +273,17 @@ type DiskUsageResponse struct {
|
||||
}
|
||||
|
||||
var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
file, err := files.NewFileInfo(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
|
||||
}
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: false,
|
||||
Checker: d,
|
||||
Content: false,
|
||||
})
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
@@ -357,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,7 +4,8 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/index"
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
)
|
||||
|
||||
var searchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
@@ -13,9 +14,8 @@ var searchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *dat
|
||||
// Retrieve the User-Agent and X-Auth headers from the request
|
||||
sessionId := r.Header.Get("SessionId")
|
||||
userScope := r.Header.Get("UserScope")
|
||||
index := *index.GetIndex()
|
||||
index := files.GetIndex(settings.Config.Server.Root)
|
||||
combinedScope := strings.TrimPrefix(userScope+r.URL.Path, ".")
|
||||
combinedScope = strings.TrimPrefix(combinedScope, "/")
|
||||
results, fileTypes := index.Search(query, combinedScope, sessionId)
|
||||
for _, path := range results {
|
||||
responseObj := map[string]interface{}{
|
||||
|
||||
@@ -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.GlobalConfiguration.Auth.Signup,
|
||||
CreateUserDir: settings.GlobalConfiguration.Server.CreateUserDir,
|
||||
UserHomeBasePath: settings.GlobalConfiguration.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
@@ -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
|
||||
}
|
||||
|
||||
@@ -30,12 +30,13 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
|
||||
"Name": d.settings.Frontend.Name,
|
||||
"DisableExternal": d.settings.Frontend.DisableExternal,
|
||||
"DisableUsedPercentage": d.settings.Frontend.DisableUsedPercentage,
|
||||
"darkMode": settings.GlobalConfiguration.UserDefaults.DarkMode,
|
||||
"darkMode": settings.Config.UserDefaults.DarkMode,
|
||||
"Color": d.settings.Frontend.Color,
|
||||
"BaseURL": d.server.BaseURL,
|
||||
"Version": version.Version,
|
||||
"CommitSHA": version.CommitSHA,
|
||||
"StaticURL": path.Join(d.server.BaseURL, "/static"),
|
||||
"Signup": settings.GlobalConfiguration.Auth.Signup,
|
||||
"Signup": settings.Config.Auth.Signup,
|
||||
"NoAuth": d.settings.Auth.Method == "noauth",
|
||||
"AuthMethod": d.settings.Auth.Method,
|
||||
"LoginPage": auther.LoginPage(),
|
||||
@@ -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) {
|
||||
|
||||
+34
-23
@@ -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"
|
||||
)
|
||||
|
||||
@@ -19,6 +21,11 @@ var (
|
||||
NonModifiableFieldsForNonAdmin = []string{"Username", "Scope", "LockPassword", "Perm", "Commands", "Rules"}
|
||||
)
|
||||
|
||||
// SortingSettings represents the sorting settings.
|
||||
type Sorting struct {
|
||||
By string `json:"by"`
|
||||
Asc bool `json:"asc"`
|
||||
}
|
||||
type modifyUserRequest struct {
|
||||
modifyRequest
|
||||
Data *users.User `json:"data"`
|
||||
@@ -57,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
|
||||
}
|
||||
@@ -124,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
|
||||
}
|
||||
@@ -150,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
|
||||
@@ -191,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
|
||||
|
||||
@@ -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,142 +0,0 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
)
|
||||
|
||||
const (
|
||||
maxIndexSize = 1000
|
||||
)
|
||||
|
||||
type Index struct {
|
||||
Dirs []string
|
||||
Files []string
|
||||
}
|
||||
|
||||
var (
|
||||
rootPath string = "/srv"
|
||||
indexes Index
|
||||
indexMutex sync.RWMutex
|
||||
lastIndexed time.Time
|
||||
)
|
||||
|
||||
func GetIndex() *Index {
|
||||
return &indexes
|
||||
}
|
||||
|
||||
func Initialize(intervalMinutes uint32) {
|
||||
// Initialize the index
|
||||
indexes = Index{
|
||||
Dirs: make([]string, 0, maxIndexSize),
|
||||
Files: make([]string, 0, maxIndexSize),
|
||||
}
|
||||
rootPath = settings.GlobalConfiguration.Server.Root
|
||||
var numFiles, numDirs int
|
||||
log.Println("Indexing files...")
|
||||
lastIndexedStart := time.Now()
|
||||
// Call the function to index files and directories
|
||||
totalNumFiles, totalNumDirs, err := indexFiles(rootPath, &numFiles, &numDirs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
lastIndexed = lastIndexedStart
|
||||
go indexingScheduler(intervalMinutes)
|
||||
log.Println("Successfully indexed files.")
|
||||
log.Println("Files found :", totalNumFiles)
|
||||
log.Println("Directories found :", totalNumDirs)
|
||||
}
|
||||
|
||||
func indexingScheduler(intervalMinutes uint32) {
|
||||
log.Printf("Indexing scheduler will run every %v minutes", intervalMinutes)
|
||||
for {
|
||||
indexes.Dirs = slices.Compact(indexes.Dirs)
|
||||
indexes.Files = slices.Compact(indexes.Files)
|
||||
time.Sleep(time.Duration(intervalMinutes) * time.Minute)
|
||||
var numFiles, numDirs int
|
||||
lastIndexedStart := time.Now()
|
||||
totalNumFiles, totalNumDirs, err := indexFiles(rootPath, &numFiles, &numDirs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
lastIndexed = lastIndexedStart
|
||||
if totalNumFiles+totalNumDirs > 0 {
|
||||
log.Println("re-indexing found changes and updated the index.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeFromSlice(slice []string, target string) []string {
|
||||
for i, s := range slice {
|
||||
if s == target {
|
||||
// Swap the target element with the last element
|
||||
slice[i], slice[len(slice)-1] = slice[len(slice)-1], slice[i]
|
||||
// Resize the slice to exclude the last element
|
||||
slice = slice[:len(slice)-1]
|
||||
break // Exit the loop, assuming there's only one target element
|
||||
}
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
// Define a function to recursively index files and directories
|
||||
func indexFiles(path string, numFiles *int, numDirs *int) (int, int, error) {
|
||||
// Check if the current directory has been modified since last indexing
|
||||
dir, err := os.Open(path)
|
||||
if err != nil {
|
||||
// directory must have been deleted, remove from index
|
||||
indexes.Dirs = removeFromSlice(indexes.Dirs, path)
|
||||
indexes.Files = removeFromSlice(indexes.Files, path)
|
||||
}
|
||||
defer dir.Close()
|
||||
dirInfo, err := dir.Stat()
|
||||
if err != nil {
|
||||
return *numFiles, *numDirs, err
|
||||
}
|
||||
// Compare the last modified time of the directory with the last indexed time
|
||||
if dirInfo.ModTime().Before(lastIndexed) {
|
||||
return *numFiles, *numDirs, nil
|
||||
}
|
||||
// Read the directory contents
|
||||
files, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
return *numFiles, *numDirs, err
|
||||
}
|
||||
// Iterate over the files and directories
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
*numDirs++
|
||||
addToIndex(path, file.Name(), true)
|
||||
_, _, err := indexFiles(path+"/"+file.Name(), numFiles, numDirs) // recursive
|
||||
if err != nil {
|
||||
log.Println("Could not index :", err)
|
||||
}
|
||||
} else {
|
||||
*numFiles++
|
||||
addToIndex(path, file.Name(), false)
|
||||
}
|
||||
}
|
||||
return *numFiles, *numDirs, nil
|
||||
}
|
||||
|
||||
func addToIndex(path string, fileName string, isDir bool) {
|
||||
indexMutex.Lock()
|
||||
defer indexMutex.Unlock()
|
||||
path = strings.TrimPrefix(path, rootPath+"/")
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
adjustedPath := path + "/" + fileName
|
||||
if path == rootPath {
|
||||
adjustedPath = fileName
|
||||
}
|
||||
if isDir {
|
||||
indexes.Dirs = append(indexes.Dirs, adjustedPath)
|
||||
} else {
|
||||
indexes.Files = append(indexes.Files, adjustedPath)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
+26
-27
@@ -3,21 +3,31 @@ package settings
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
var GlobalConfiguration Settings
|
||||
var Config Settings
|
||||
|
||||
func Initialize(configFile string) {
|
||||
yamlData := loadConfigFile(configFile)
|
||||
GlobalConfiguration = setDefaults()
|
||||
err := yaml.Unmarshal(yamlData, &GlobalConfiguration)
|
||||
Config = setDefaults()
|
||||
err := yaml.Unmarshal(yamlData, &Config)
|
||||
if err != nil {
|
||||
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
||||
}
|
||||
GlobalConfiguration.UserDefaults.Perm = GlobalConfiguration.UserDefaults.Permissions
|
||||
Config.UserDefaults.Perm = Config.UserDefaults.Permissions
|
||||
// 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 {
|
||||
@@ -25,7 +35,7 @@ func loadConfigFile(configFile string) []byte {
|
||||
yamlFile, err := os.Open(configFile)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: opening config file\n %v\n WARNING: Using default config only\n If this was a mistake, please make sure the file exists and is accessible by the filebrowser binary.\n\n", err)
|
||||
GlobalConfiguration = setDefaults()
|
||||
Config = setDefaults()
|
||||
return []byte{}
|
||||
}
|
||||
defer yamlFile.Close()
|
||||
@@ -50,30 +60,34 @@ 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{
|
||||
AdminUsername: "admin",
|
||||
AdminPassword: "admin",
|
||||
Method: "password",
|
||||
Signup: false,
|
||||
TokenExpirationTime: "2h",
|
||||
AdminUsername: "admin",
|
||||
AdminPassword: "admin",
|
||||
Method: "password",
|
||||
Signup: false,
|
||||
Recaptcha: Recaptcha{
|
||||
Host: "",
|
||||
},
|
||||
},
|
||||
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,
|
||||
@@ -85,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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -12,14 +12,14 @@ func TestConfigLoadChanged(t *testing.T) {
|
||||
yamlData := loadConfigFile("./testingConfig.yaml")
|
||||
// Marshal the YAML data to a more human-readable format
|
||||
newConfig := setDefaults()
|
||||
GlobalConfiguration := setDefaults()
|
||||
Config := setDefaults()
|
||||
|
||||
err := yaml.Unmarshal(yamlData, &newConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
||||
}
|
||||
// Use go-cmp to compare the two structs
|
||||
if diff := cmp.Diff(newConfig, GlobalConfiguration); diff == "" {
|
||||
if diff := cmp.Diff(newConfig, Config); diff == "" {
|
||||
t.Errorf("No change when there should have been (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ func TestConfigLoadSpecificValues(t *testing.T) {
|
||||
yamlData := loadConfigFile("./testingConfig.yaml")
|
||||
// Marshal the YAML data to a more human-readable format
|
||||
newConfig := setDefaults()
|
||||
GlobalConfiguration := setDefaults()
|
||||
Config := setDefaults()
|
||||
|
||||
err := yaml.Unmarshal(yamlData, &newConfig)
|
||||
if err != nil {
|
||||
@@ -39,16 +39,16 @@ func TestConfigLoadSpecificValues(t *testing.T) {
|
||||
globalVal interface{}
|
||||
newVal interface{}
|
||||
}{
|
||||
{"Auth.Method", GlobalConfiguration.Auth.Method, newConfig.Auth.Method},
|
||||
{"Auth.Method", GlobalConfiguration.Auth.Method, newConfig.Auth.Method},
|
||||
{"Frontend.disableExternal", GlobalConfiguration.Frontend.DisableExternal, newConfig.Frontend.DisableExternal},
|
||||
{"UserDefaults.HideDotfiles", GlobalConfiguration.UserDefaults.HideDotfiles, newConfig.UserDefaults.HideDotfiles},
|
||||
{"Server.Database", GlobalConfiguration.Server.Database, newConfig.Server.Database},
|
||||
{"Auth.Method", Config.Auth.Method, newConfig.Auth.Method},
|
||||
{"Auth.Method", Config.Auth.Method, newConfig.Auth.Method},
|
||||
{"Frontend.disableExternal", Config.Frontend.DisableExternal, newConfig.Frontend.DisableExternal},
|
||||
{"UserDefaults.HideDotfiles", Config.UserDefaults.HideDotfiles, newConfig.UserDefaults.HideDotfiles},
|
||||
{"Server.Database", Config.Server.Database, newConfig.Server.Database},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
if tc.globalVal == tc.newVal {
|
||||
t.Errorf("Differences should have been found:\n\tGlobalConfig.%s: %v \n\tSetConfig: %v \n", tc.fieldName, tc.globalVal, tc.newVal)
|
||||
t.Errorf("Differences should have been found:\n\tConfig.%s: %v \n\tSetConfig: %v \n", tc.fieldName, tc.globalVal, tc.newVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
+29
-15
@@ -2,7 +2,6 @@ package settings
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/rules"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
@@ -17,15 +16,16 @@ type Settings struct {
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
Recaptcha Recaptcha `json:"recaptcha"`
|
||||
Header string `json:"header"`
|
||||
Method string `json:"method"`
|
||||
Command string `json:"command"`
|
||||
Signup bool `json:"signup"`
|
||||
Shell string `json:"shell"`
|
||||
AdminUsername string `json:"adminUsername"`
|
||||
AdminPassword string `json:"adminPassword"`
|
||||
Key []byte `json:"key"`
|
||||
TokenExpirationTime string `json:"tokenExpirationTime"`
|
||||
Recaptcha Recaptcha `json:"recaptcha"`
|
||||
Header string `json:"header"`
|
||||
Method string `json:"method"`
|
||||
Command string `json:"command"`
|
||||
Signup bool `json:"signup"`
|
||||
Shell string `json:"shell"`
|
||||
AdminUsername string `json:"adminUsername"`
|
||||
AdminPassword string `json:"adminPassword"`
|
||||
Key []byte `json:"key"`
|
||||
}
|
||||
|
||||
type Recaptcha struct {
|
||||
@@ -53,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 {
|
||||
@@ -66,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"`
|
||||
}
|
||||
@@ -7,7 +7,7 @@ server:
|
||||
enableThumbnails: false
|
||||
resizePreview: true
|
||||
typeDetectionByHeader: true
|
||||
port: 8080
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
address: ""
|
||||
log: "stdout"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+57
-50
@@ -1,45 +1,55 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"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"`
|
||||
Asc bool `json:"asc"`
|
||||
}
|
||||
|
||||
// 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 files.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.
|
||||
@@ -47,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 {
|
||||
@@ -81,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
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package version
|
||||
|
||||
var (
|
||||
// Version is the current File Browser version.
|
||||
Version = "(0.2.1)"
|
||||
// CommitSHA is the commmit sha.
|
||||
CommitSHA = "(unknown)"
|
||||
// Dynamically updated during build via build args
|
||||
Version = "untracked"
|
||||
CommitSHA = "untracked"
|
||||
)
|
||||
+11
-15
@@ -7,12 +7,11 @@ This document covers the available configuration options, their defaults, and ho
|
||||
Here is an expanded config file which includes all possible configurations:
|
||||
|
||||
```
|
||||
adminUsername: admin
|
||||
adminPassword: admin
|
||||
server:
|
||||
CreateUserDir: false
|
||||
UserHomeBasePath: ""
|
||||
indexingInterval: 5
|
||||
indexing: true
|
||||
numImageProcessors: 4
|
||||
socket: ""
|
||||
tlsKey: ""
|
||||
@@ -20,22 +19,23 @@ server:
|
||||
enableThumbnails: false
|
||||
resizePreview: true
|
||||
typeDetectionByHeader: true
|
||||
port: 8080
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
address: ""
|
||||
log: "stdout"
|
||||
database: "/database/database.db"
|
||||
root: "/srv"
|
||||
auth:
|
||||
adminUsername: admin
|
||||
adminPassword: admin
|
||||
recaptcha:
|
||||
host: ""
|
||||
key: ""
|
||||
secret: ""
|
||||
tokenExpirationTime: 2h
|
||||
header: ""
|
||||
method: json
|
||||
command: ""
|
||||
signup: false
|
||||
shell: ""
|
||||
frontend:
|
||||
name: ""
|
||||
disableExternal: false
|
||||
@@ -61,7 +61,6 @@ userDefaults:
|
||||
delete: true
|
||||
share: true
|
||||
download: true
|
||||
commands: []
|
||||
hideDotfiles: false
|
||||
dateFormat: false
|
||||
```
|
||||
@@ -69,19 +68,19 @@ userDefaults:
|
||||
Here are the defaults if nothing is set:
|
||||
|
||||
```
|
||||
adminUsername: admin
|
||||
adminPassword: admin
|
||||
server:
|
||||
enableThumbnails: true
|
||||
enableExec: false
|
||||
indexingInterval: 5
|
||||
port: 8080
|
||||
port: 80
|
||||
numImageProcessors: 4
|
||||
baseURL: ""
|
||||
database: database.db
|
||||
log: stdout
|
||||
root: /srv
|
||||
auth:
|
||||
adminUsername: admin
|
||||
adminPassword: admin
|
||||
method: password
|
||||
recaptcha:
|
||||
host: ""
|
||||
@@ -112,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.
|
||||
@@ -126,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: `""`
|
||||
|
||||
@@ -161,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`
|
||||
@@ -223,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`)
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
presets: ["@vue/app"],
|
||||
};
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
frontend
|
||||
Generated
-8652
File diff suppressed because it is too large.
Load diff
+31
-41
@@ -1,56 +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"
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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"] },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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 ]}]
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
Loaded 100 of 205 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user