mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-08 10:25:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ac9802e5c | ||
|
|
742776c492 | ||
|
|
3ceaf43b86 | ||
|
|
bb420e9939 | ||
|
|
710d9c523b | ||
|
|
ac75b5344d | ||
|
|
64c5a24d1f | ||
|
|
f3567c01e8 | ||
|
|
69de3d712b | ||
|
|
4c8e35ebe0 | ||
|
|
9983cde9b6 | ||
|
|
b4b92bf852 | ||
|
|
2ec0c457cf | ||
|
|
c272eb1e61 | ||
|
|
bae39c8586 | ||
|
|
62d1cd88a1 | ||
|
|
0bad14b51e | ||
|
|
74ccfab070 | ||
|
|
5f185597c9 | ||
|
|
9e9109984d | ||
|
|
5ebaf2a45b | ||
|
|
c7cce0694b | ||
|
|
43b45203d0 | ||
|
|
af407068b6 | ||
|
|
865fb4e243 | ||
|
|
c616b1473a | ||
|
|
0479670c52 | ||
|
|
fa3ed6b948 | ||
|
|
d8085c1f1b | ||
|
|
dc5da5662f | ||
|
|
b4bba3391d |
No files matched your search
@@ -1,76 +0,0 @@
|
||||
name: dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'dev_v*'
|
||||
|
||||
jobs:
|
||||
test-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.22.0
|
||||
- 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.22.0
|
||||
- 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.22.0
|
||||
- run: cd backend && go fmt ./...
|
||||
lint-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '20'
|
||||
- run: cd frontend && npm i eslint
|
||||
- run: cd frontend && npm run lint
|
||||
push_dev_to_registry:
|
||||
name: Push dev image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
# Workaround to fix error:
|
||||
# failed to push: failed to copy: io: read/write on closed pipe
|
||||
# See https://github.com/docker/build-push-action/issues/761
|
||||
with:
|
||||
driver-opts: |
|
||||
image=moby/buildkit:v0.10.6
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: gtstef/filebrowser
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
+12
-45
@@ -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.22.0
|
||||
- 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.22.0
|
||||
- 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.22.0
|
||||
- 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'
|
||||
- 'dev_v*'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
|
||||
jobs:
|
||||
test-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.22.0
|
||||
- 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.22.0
|
||||
- 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.22.0
|
||||
- run: cd backend && go fmt ./...
|
||||
lint-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '20'
|
||||
- run: cd frontend && npm i eslint
|
||||
- run: cd frontend && npm run lint
|
||||
|
||||
push_pr_to_registry:
|
||||
name: Push PR
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
# Workaround to fix error:
|
||||
# failed to push: failed to copy: io: read/write on closed pipe
|
||||
# See https://github.com/docker/build-push-action/issues/761
|
||||
with:
|
||||
driver-opts: |
|
||||
image=moby/buildkit:v0.10.6
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: gtstef/filebrowser
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -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,9 +1,9 @@
|
||||
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
|
||||
@@ -14,60 +14,28 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
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.22.2
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
target_commitish: ${{ steps.extract_branch.outputs.branch }}
|
||||
token: ${{ secrets.PAT }}
|
||||
tag_name: ${{ steps.extract_branch.outputs.branch }}
|
||||
release_name: ${{ steps.extract_branch.outputs.branch }}
|
||||
body: |
|
||||
Please see usage on [readme](https://github.com/gtsteffaniak/filebrowser).
|
||||
Docker repo: https://hub.docker.com/r/gtstef/filebrowser/tags
|
||||
draft: false
|
||||
prerelease: false
|
||||
test-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.22.0
|
||||
- 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.22.0
|
||||
- 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.22.0
|
||||
- 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
|
||||
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:
|
||||
@@ -77,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
|
||||
@@ -102,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
|
||||
|
||||
+74
-2
@@ -2,16 +2,88 @@
|
||||
|
||||
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.10
|
||||
|
||||
**New Features**:
|
||||
- Allows user creation command line arguments https://github.com/gtsteffaniak/filebrowser/issues/196
|
||||
- Folder sizes are always shown, leveraging the index. https://github.com/gtsteffaniak/filebrowser/issues/138
|
||||
- Searching files based on filesize is no longer slower.
|
||||
|
||||
**Bugfixes**:
|
||||
- fixes file selection usage when in single-click mode https://github.com/gtsteffaniak/filebrowser/issues/214
|
||||
- Fixed displayed search context on root directory
|
||||
- Fixed issue searching "smaller than" actually returned files "larger than"
|
||||
|
||||
**Notes**:
|
||||
- Memory usage from index is reduced by ~40%
|
||||
- Indexing time has increased 2x due to the extra processing time required to calculate directory sizes.
|
||||
- File size calcuations use 1024 base vs previous 1000 base (matching windows explorer)
|
||||
|
||||
## v0.2.9
|
||||
|
||||
This release focused on UI navigation experience. Improving keyboard navigation and adds right click context menu.
|
||||
|
||||
**New Features**:
|
||||
- listing view items are middle-clickable on selected listing or when in single-click mode.
|
||||
- listing view items can be navigated via arrow keys.
|
||||
- listing view can jump to items using letters and number keys to cycle through files that start with that character.
|
||||
- You can use the enter key and backspace key to navigate backwards and forwards on selected items.
|
||||
- ctr-space will open/close the search (leaving ctr-f to browser default find prompt)
|
||||
- Added right-click context menu to replace the file selection prompt.
|
||||
|
||||
**Bugfixes**:
|
||||
- Fixed drag to upload not working.
|
||||
- Fixed shared video link issues.
|
||||
- Fixed user edit bug related to other user.
|
||||
- Fixed password reset bug.
|
||||
- Fixed loading state getting stuck.
|
||||
|
||||
## v0.2.8
|
||||
|
||||
- **Feature**: New gallary view scaling options (closes [#141](https://github.com/gtsteffaniak/filebrowser/issues/141))
|
||||
- **Change**: Refactored backend files functions
|
||||
- **Change**: Improved UI response to filesystem changes
|
||||
- **Change**: Added frontend tests for deployment integrity
|
||||
- **Fix**: move/replace file prompt issue
|
||||
- **Fix**: opening files from search
|
||||
- **Fix**: Display count issue when hideDotFile is enabled.
|
||||
|
||||
## v0.2.7
|
||||
|
||||
- **Change**: New sidebar style and behavior
|
||||
- **Change**: make search view and button behavior more consistent.
|
||||
- **Fix**: [upload file bug](https://github.com/gtsteffaniak/filebrowser/issues/153)
|
||||
- **Fix**: user lock out bug introduced in 0.2.6
|
||||
- **Fix**: many minor state related issues.
|
||||
|
||||
## v0.2.6
|
||||
|
||||
This change focuses on minimizing and simplifying build process.
|
||||
|
||||
- **Change**: Migrated to Vite / Vue 3
|
||||
- **Change**: removed npm modules
|
||||
- replaced vuex with custom state management via src/store
|
||||
- replaced noty with simple card popup notifications
|
||||
- replaced moment with simple date formatter where needed
|
||||
- replaced vue-simple-progress with vue component
|
||||
- **Feature**: improved error logging
|
||||
- backend errors show the root function that called them during the error
|
||||
- frontend errors print errors to console that fail try/catch
|
||||
- all frontend errors via popup notification & print to console as well
|
||||
- **Fix**: Allow editing blank text based files in editor
|
||||
- tweaked listing styles
|
||||
- Feature: Allow disabling the index via configuration yaml
|
||||
|
||||
## v0.2.5
|
||||
|
||||
- Fix: delete user prompt works using native hovers.
|
||||
|
||||
## v0.2.4
|
||||
|
||||
- Faature: [create-folder-feature](https://github.com/gtsteffaniak/filebrowser/pull/105)
|
||||
- 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
|
||||
- Fix: sharable link bug, now uses special publicUser
|
||||
- Bump go version to 1.22
|
||||
- In prep for vue3 migration, npm modules removed:
|
||||
- js-base64
|
||||
|
||||
+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.22-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,54 +2,78 @@
|
||||
<a href="https://opensource.org/license/apache-2-0/"><img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg" alt="License: Apache-2.0"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="frontend/public/img/icons/favicon-256x256.png" width="100" title="Login With Custom URL">
|
||||
<img src="frontend/public/img/icons/favicon-256x256.png" width="100" title="Login With Custom URL">
|
||||
</p>
|
||||
<h3 align="center">Filebrowser - A modern file manager for the web</h3>
|
||||
<h3 align="center">FileBrowser Quantum - A modern web-based file manager</h3>
|
||||
<p align="center">
|
||||
<img width="800" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/899152cf-3e69-4179-aa82-752af2df3fc6" title="Main Screenshot">
|
||||
<img width="800" src="https://private-user-images.githubusercontent.com/42989099/367975355-3d6f4619-4985-4ce3-952f-286510dff4f1.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjgxNTA2MjEsIm5iZiI6MTcyODE1MDMyMSwicGF0aCI6Ii80Mjk4OTA5OS8zNjc5NzUzNTUtM2Q2ZjQ2MTktNDk4NS00Y2UzLTk1MmYtMjg2NTEwZGZmNGYxLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEwMDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMDA1VDE3NDUyMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTg1OGNlMWM3M2I1ZmY3MDcxMGU1ODc3N2ZkMjI5YWQ3YzEyODRmNDU0ZDkxMjJhNTU0ZGY1MDQ2YmIwOWRmMTgmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.mOl0Hm70XmQEk-DPzx1FbwrpxNMDAqb-WDprs1HK-mc" title="Main Screenshot">
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
> Starting with v0.2.0, *ALL* configuration is done via `filebrowser.yaml` configuration file.
|
||||
> Starting with v0.2.4 *ALL* share links need to be re-created (due to security fix).
|
||||
> Starting with v0.2.0, *ALL* configuration is done via `filebrowser.yaml`
|
||||
> Configuration file.
|
||||
> Starting with v0.2.4 *ALL* share links need to be re-created (due to
|
||||
> security fix).
|
||||
|
||||
This fork makes the following significant changes to filebrowser for origin:
|
||||
FileBrowser Quantum is a fork of the filebrowser opensource project with the
|
||||
following changes:
|
||||
|
||||
1. [x] Better search.
|
||||
- Lightning fast
|
||||
- realtime results as you type
|
||||
- Works with more type filters
|
||||
- interactive results page.
|
||||
2. [x] Revamped and simplified GUI navbar and sidebar menu.
|
||||
3. [x] **IMPORTANT** Revamped configuration via `filebrowser.yml` config file.
|
||||
4. [x] More configurations possible at a per-user level
|
||||
- <img width="450" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/625bd7c4-5ee9-4011-aaae-2a388ab0813b">
|
||||
5. [x] Additional compact view mode as well as refreshed view mode styles.
|
||||
1. [x] Efficiently indexed files
|
||||
- Real-time search results as you type
|
||||
- Search 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>
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
Using docker:
|
||||
@@ -60,7 +84,7 @@ Using docker:
|
||||
docker run -it -v /path/to/folder:/srv -p 80:80 gtstef/filebrowser
|
||||
```
|
||||
|
||||
1. docker-compose:
|
||||
1. docker compose:
|
||||
|
||||
- with local storage
|
||||
|
||||
@@ -101,43 +125,87 @@ volumes:
|
||||
|
||||
```
|
||||
|
||||
Not using docker (not recommended)
|
||||
Not using docker (not recommended), download your binary from releases and run with your custom config file:
|
||||
|
||||
```
|
||||
./filebrowser -f <filebrowser.yml or other /path/to/config.yaml>
|
||||
./filebrowser -c <filebrowser.yml or other /path/to/config.yaml>
|
||||
```
|
||||
|
||||
## Command Line Usage
|
||||
|
||||
There are very few commands available. There are 3 actions done via command line:
|
||||
|
||||
1. Running the program, as shown on install step. Only argument used is the config file, if you choose to override default "filebrowser.yaml"
|
||||
2. Checking the version info via `./filebrowser version`
|
||||
3. Updating the DB, which currently only supports adding users via `./filebrowser set -u username,password [-a] [-s "example/scope"]`
|
||||
|
||||
## 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](./docs/configuration.md) for available
|
||||
configuration options and other help.
|
||||
|
||||
|
||||
## Migration from filebrowser/filebrowser
|
||||
|
||||
If you are currently using filebrowser from the filebrowser/filebrowser repo, but want to try using this. I recommend you start fresh without reusing the database, but there are a few things you'll need to do if you must migrate:
|
||||
If you currently use the original filebrowser but want to try using this.
|
||||
I recommend you start fresh without reusing the database. If you want to
|
||||
migrate your existing database to FileBrowser Quantum, visit the [migration
|
||||
readme](./docs/migration.md)
|
||||
|
||||
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 -f filebrowser.yml and have valid filebrowser config.
|
||||
## Comparison Chart
|
||||
|
||||
|
||||
The filebrowser application should run with the same user and rules that you have from the original. But keep in mind the differences that are mentioned at the top of this readme.
|
||||
|
||||
### background & help
|
||||
|
||||
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.
|
||||
|
||||
I understand many coming from the original fork may notice differences which make using this improved version more difficult. If you notice issues that you believe should be fixed, please open an issue here and it will very likely be addressed with a PR within a few weeks.
|
||||
|
||||
This version of filebrowser is going through a configuration overhaul as mentioned above. Certain features related to rules and commands may not work as they do on the original filebrowser. The purpose of this is to create a more consistent experience where configuration is done via files rather than running commands, so that it's very clear what the current state of the configuration is. When running commands its not clear what the configuration is.
|
||||
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)
|
||||
|
||||
see [Roadmap Page](./docs/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
|
||||
|
||||
@@ -187,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,
|
||||
|
||||
@@ -5,49 +5,39 @@
|
||||
? github.com/gtsteffaniak/filebrowser/auth [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/cmd [no test files]
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/diskcache 0.003s
|
||||
ok github.com/gtsteffaniak/filebrowser/diskcache 0.004s
|
||||
? github.com/gtsteffaniak/filebrowser/errors [no test files]
|
||||
2024/10/07 12:46:34 could not update unknown type: unknown
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
pkg: github.com/gtsteffaniak/filebrowser/files
|
||||
cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz
|
||||
BenchmarkFillIndex-8 10 3587120 ns/op 273640 B/op 2013 allocs/op
|
||||
BenchmarkSearchAllIndexes-8 10 31291180 ns/op 19500700 B/op 298636 allocs/op
|
||||
BenchmarkFillIndex-8 10 3847878 ns/op 758424 B/op 5567 allocs/op
|
||||
BenchmarkSearchAllIndexes-8 10 780431 ns/op 173444 B/op 2014 allocs/op
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/files 0.408s
|
||||
ok github.com/gtsteffaniak/filebrowser/files 0.073s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/fileutils 0.003s
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 h: 401 <nil>
|
||||
2024/02/07 07:16:43 h: 401 <nil>
|
||||
2024/02/07 07:16:43 h: 401 <nil>
|
||||
2024/02/07 07:16:43 h: 401 <nil>
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 Saving new user: publicUser
|
||||
2024/02/07 07:16:43 h: 401 <nil>
|
||||
2024/02/07 07:16:43 h: 401 <nil>
|
||||
2024/10/07 12:46:34 h: 401 <nil>
|
||||
2024/10/07 12:46:34 h: 401 <nil>
|
||||
2024/10/07 12:46:34 h: 401 <nil>
|
||||
2024/10/07 12:46:34 h: 401 <nil>
|
||||
2024/10/07 12:46:34 h: 401 <nil>
|
||||
2024/10/07 12:46:34 h: 401 <nil>
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/http 0.202s
|
||||
ok github.com/gtsteffaniak/filebrowser/http 0.080s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/img 0.125s
|
||||
ok github.com/gtsteffaniak/filebrowser/img 0.137s
|
||||
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/utils [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/version [no test files]
|
||||
@@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
||||
+192
-104
@@ -3,6 +3,7 @@ package cmd
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
@@ -10,20 +11,27 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"embed"
|
||||
|
||||
"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/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/version"
|
||||
)
|
||||
|
||||
//go:embed dist/*
|
||||
var assets embed.FS
|
||||
|
||||
var (
|
||||
nonEmbededFS = os.Getenv("FILEBROWSER_NO_EMBEDED") == "true"
|
||||
)
|
||||
|
||||
type dirFS struct {
|
||||
@@ -34,79 +42,120 @@ func (d dirFS) Open(name string) (fs.File, error) {
|
||||
return d.Dir.Open(name)
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Define a flag for the config option (-c or --config)
|
||||
configFlag := pflag.StringP("config", "c", "filebrowser.yaml", "Path to the config file")
|
||||
// Bind the flags to the pflag command line parser
|
||||
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
|
||||
pflag.Parse()
|
||||
log.Println("Initializing with config file:", *configFlag)
|
||||
settings.Initialize(*configFlag)
|
||||
func getStore(config string) (*storage.Storage, bool) {
|
||||
// Use the config file (global flag)
|
||||
log.Printf("Using Config file : %v", config)
|
||||
settings.Initialize(config)
|
||||
store, hasDB, err := storage.InitializeDb(settings.Config.Server.Database)
|
||||
if err != nil {
|
||||
log.Fatal("could not load db info: ", err)
|
||||
}
|
||||
return store, hasDB
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "filebrowser",
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
serverConfig := settings.Config.Server
|
||||
if !d.hadDB {
|
||||
quickSetup(d)
|
||||
}
|
||||
if serverConfig.NumImageProcessors < 1 {
|
||||
log.Fatal("Image resize workers count could not be < 1")
|
||||
}
|
||||
imgSvc := img.New(serverConfig.NumImageProcessors)
|
||||
var fileCache diskcache.Interface = diskcache.NewNoOp()
|
||||
cacheDir := "/tmp"
|
||||
if cacheDir != "" {
|
||||
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet,gomnd
|
||||
log.Fatalf("can't make directory %s: %s", cacheDir, err)
|
||||
}
|
||||
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
|
||||
}
|
||||
// initialize indexing and schedule indexing ever n minutes (default 5)
|
||||
go files.InitializeIndex(serverConfig.IndexingInterval, true)
|
||||
_, err := os.Stat(serverConfig.Root)
|
||||
checkErr(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)
|
||||
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
|
||||
checkErr(err)
|
||||
err = os.Chmod(serverConfig.Socket, os.FileMode(socketPerm))
|
||||
checkErr(err)
|
||||
case serverConfig.TLSKey != "" && serverConfig.TLSCert != "":
|
||||
cer, err := tls.LoadX509KeyPair(serverConfig.TLSCert, serverConfig.TLSKey) //nolint:govet
|
||||
checkErr(err)
|
||||
listener, err = tls.Listen("tcp", address, &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
Certificates: []tls.Certificate{cer}},
|
||||
)
|
||||
checkErr(err)
|
||||
default:
|
||||
listener, err = net.Listen("tcp", address)
|
||||
checkErr(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)
|
||||
}
|
||||
}, pythonConfig{allowNoDB: true}),
|
||||
func generalUsage() {
|
||||
fmt.Printf(`usage: ./html-web-crawler <command> [options] --urls <urls>
|
||||
commands:
|
||||
collect Collect data from URLs
|
||||
crawl Crawl URLs and collect data
|
||||
install Install chrome browser for javascript enabled scraping.
|
||||
Note: Consider instead to install via native package manager,
|
||||
then set "CHROME_EXECUTABLE" in the environment
|
||||
` + "\n")
|
||||
}
|
||||
|
||||
func StartFilebrowser() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
// Global flags
|
||||
var configPath string
|
||||
var help bool
|
||||
// Override the default usage output to use generalUsage()
|
||||
flag.Usage = generalUsage
|
||||
flag.StringVar(&configPath, "c", "filebrowser.yaml", "Path to the config file.")
|
||||
flag.BoolVar(&help, "h", false, "Get help about commands")
|
||||
|
||||
// Parse global flags (before subcommands)
|
||||
flag.Parse() // print generalUsage on error
|
||||
|
||||
// Show help if requested
|
||||
if help {
|
||||
generalUsage()
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new FlagSet for the 'set' subcommand
|
||||
setCmd := flag.NewFlagSet("set", flag.ExitOnError)
|
||||
var user, scope, dbConfig string
|
||||
var asAdmin bool
|
||||
|
||||
setCmd.StringVar(&user, "u", "", "Comma-separated username and password: \"set -u <username>,<password>\"")
|
||||
setCmd.BoolVar(&asAdmin, "a", false, "Create user as admin user, used in combination with -u")
|
||||
setCmd.StringVar(&scope, "s", "", "Specify a user scope, otherwise default user config scope is used")
|
||||
setCmd.StringVar(&dbConfig, "c", "filebrowser.yaml", "Path to the config file.")
|
||||
|
||||
// Parse subcommand flags only if a subcommand is specified
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "set":
|
||||
err := setCmd.Parse(os.Args)
|
||||
if err != nil {
|
||||
setCmd.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
userInfo := strings.Split(user, ",")
|
||||
if len(userInfo) < 2 {
|
||||
fmt.Println("not enough info to create user: \"set -u username,password\"")
|
||||
setCmd.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
username := userInfo[0]
|
||||
password := userInfo[1]
|
||||
getStore(dbConfig)
|
||||
// Create the user logic
|
||||
if asAdmin {
|
||||
log.Printf("Creating user as admin: %s\n", username)
|
||||
} else {
|
||||
log.Printf("Creating user: %s\n", username)
|
||||
}
|
||||
newUser := users.User{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
if scope != "" {
|
||||
newUser.Scope = scope
|
||||
}
|
||||
err = storage.CreateUser(newUser, asAdmin)
|
||||
if err != nil {
|
||||
log.Fatal("Could not create user: ", err)
|
||||
}
|
||||
return
|
||||
case "version":
|
||||
fmt.Println("FileBrowser Quantum - A modern web-based file manager")
|
||||
fmt.Printf("Version : %v\n", version.Version)
|
||||
fmt.Printf("Commit : %v\n", version.CommitSHA)
|
||||
fmt.Printf("Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v\n", version.Version)
|
||||
return
|
||||
}
|
||||
}
|
||||
store, dbExists := getStore(configPath)
|
||||
indexingInterval := fmt.Sprint(settings.Config.Server.IndexingInterval, " minutes")
|
||||
if !settings.Config.Server.Indexing {
|
||||
indexingInterval = "disabled"
|
||||
}
|
||||
database := fmt.Sprintf("Using existing database : %v", settings.Config.Server.Database)
|
||||
if !dbExists {
|
||||
database = fmt.Sprintf("Creating new database : %v", settings.Config.Server.Database)
|
||||
}
|
||||
log.Printf("Initializing FileBrowser Quantum (%v)\n", version.Version)
|
||||
log.Println("Embeded frontend :", !nonEmbededFS)
|
||||
log.Println(database)
|
||||
log.Println("Sources :", settings.Config.Server.Root)
|
||||
log.Print("Indexing interval : ", indexingInterval)
|
||||
|
||||
serverConfig := settings.Config.Server
|
||||
// initialize indexing and schedule indexing ever n minutes (default 5)
|
||||
go files.InitializeIndex(serverConfig.IndexingInterval, serverConfig.Indexing)
|
||||
if err := rootCMD(store, &serverConfig); err != nil {
|
||||
log.Fatal("Error starting filebrowser:", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,38 +166,77 @@ func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfac
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func quickSetup(d pythonData) {
|
||||
settings.Config.Auth.Key = generateKey()
|
||||
if settings.Config.Auth.Method == "noauth" {
|
||||
err := d.store.Auth.Save(&auth.NoAuth{})
|
||||
checkErr(err)
|
||||
func rootCMD(store *storage.Storage, serverConfig *settings.Server) error {
|
||||
if serverConfig.NumImageProcessors < 1 {
|
||||
log.Fatal("Image resize workers count could not be < 1")
|
||||
}
|
||||
imgSvc := img.New(serverConfig.NumImageProcessors)
|
||||
|
||||
cacheDir := "/tmp"
|
||||
var fileCache diskcache.Interface
|
||||
|
||||
// Use file cache if cacheDir is specified
|
||||
if cacheDir != "" {
|
||||
var err error
|
||||
fileCache, err = diskcache.NewFileCache(cacheDir)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create file cache: %v", err)
|
||||
}
|
||||
} else {
|
||||
settings.Config.Auth.Method = "password"
|
||||
err := d.store.Auth.Save(&auth.JSONAuth{})
|
||||
checkErr(err)
|
||||
// No-op cache if no cacheDir is specified
|
||||
fileCache = diskcache.NewNoOp()
|
||||
}
|
||||
err := d.store.Settings.Save(&settings.Config)
|
||||
checkErr(err)
|
||||
err = d.store.Settings.SaveServer(&settings.Config.Server)
|
||||
checkErr(err)
|
||||
user := &users.User{}
|
||||
settings.Config.UserDefaults.Apply(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{
|
||||
Create: true,
|
||||
Rename: true,
|
||||
Modify: true,
|
||||
Delete: true,
|
||||
Share: true,
|
||||
Download: true,
|
||||
Admin: true,
|
||||
|
||||
fbhttp.SetupEnv(store, serverConfig, fileCache)
|
||||
|
||||
_, err := os.Stat(serverConfig.Root)
|
||||
utils.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)
|
||||
utils.CheckErr("net.Listen", err)
|
||||
err = os.Chmod(serverConfig.Socket, os.FileMode(0666)) // socket-perm
|
||||
utils.CheckErr("os.Chmod", err)
|
||||
case serverConfig.TLSKey != "" && serverConfig.TLSCert != "":
|
||||
cer, err := tls.LoadX509KeyPair(serverConfig.TLSCert, serverConfig.TLSKey) //nolint:govet
|
||||
utils.CheckErr("tls.LoadX509KeyPair", err)
|
||||
listener, err = tls.Listen("tcp", address, &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
Certificates: []tls.Certificate{cer}},
|
||||
)
|
||||
utils.CheckErr("tls.Listen", err)
|
||||
default:
|
||||
listener, err = net.Listen("tcp", address)
|
||||
utils.CheckErr("net.Listen", err)
|
||||
}
|
||||
err = d.store.Users.Save(user)
|
||||
checkErr(err)
|
||||
sigc := make(chan os.Signal, 1)
|
||||
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
||||
go cleanupHandler(listener, sigc)
|
||||
if !nonEmbededFS {
|
||||
assetsFs, err := fs.Sub(assets, "dist")
|
||||
if err != nil {
|
||||
log.Fatal("Could not embed frontend. Does backend/cmd/dist exist? Must be built and exist first")
|
||||
}
|
||||
handler, err := fbhttp.NewHandler(imgSvc, assetsFs)
|
||||
utils.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, assetsFs)
|
||||
utils.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)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+11
-9
@@ -6,7 +6,9 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -40,27 +42,27 @@ including 'index_end'.`,
|
||||
|
||||
return nil
|
||||
},
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
i, err := strconv.Atoi(args[0])
|
||||
checkErr(err)
|
||||
utils.CheckErr("strconv.Atoi", err)
|
||||
f := i
|
||||
if len(args) == 2 { //nolint:gomnd
|
||||
f, err = strconv.Atoi(args[1])
|
||||
checkErr(err)
|
||||
utils.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)
|
||||
err := store.Users.Save(u)
|
||||
utils.CheckErr("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)
|
||||
err := store.Settings.Save(s)
|
||||
utils.CheckErr("store.Settings.Save", err)
|
||||
}
|
||||
|
||||
runRules(d.store, cmd, user, global)
|
||||
}, pythonConfig{}),
|
||||
runRules(store, cmd, user, global)
|
||||
}),
|
||||
}
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(rulesCmd)
|
||||
rulesCmd.PersistentFlags().StringP("username", "u", "", "username of user to which the rules apply")
|
||||
rulesCmd.PersistentFlags().UintP("id", "i", 0, "id of user to which the rules apply")
|
||||
}
|
||||
@@ -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)
|
||||
utils.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)
|
||||
utils.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)
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/rules"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -21,7 +23,7 @@ var rulesAddCmd = &cobra.Command{
|
||||
Short: "Add a global rule or user rule",
|
||||
Long: `Add a global rule or user rule.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
allow := mustGetBool(cmd.Flags(), "allow")
|
||||
regex := mustGetBool(cmd.Flags(), "regex")
|
||||
exp := args[0]
|
||||
@@ -43,16 +45,16 @@ var rulesAddCmd = &cobra.Command{
|
||||
|
||||
user := func(u *users.User) {
|
||||
u.Rules = append(u.Rules, rule)
|
||||
err := d.store.Users.Save(u)
|
||||
checkErr(err)
|
||||
err := store.Users.Save(u)
|
||||
utils.CheckErr("store.Users.Save", err)
|
||||
}
|
||||
|
||||
global := func(s *settings.Settings) {
|
||||
s.Rules = append(s.Rules, rule)
|
||||
err := d.store.Settings.Save(s)
|
||||
checkErr(err)
|
||||
err := store.Settings.Save(s)
|
||||
utils.CheckErr("store.Settings.Save", err)
|
||||
}
|
||||
|
||||
runRules(d.store, cmd, user, global)
|
||||
}, pythonConfig{}),
|
||||
runRules(store, cmd, user, global)
|
||||
}),
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -13,7 +14,7 @@ var rulesLsCommand = &cobra.Command{
|
||||
Short: "List global rules or user specific rules",
|
||||
Long: `List global rules or user specific rules.`,
|
||||
Args: cobra.NoArgs,
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
runRules(d.store, cmd, nil, nil)
|
||||
}, pythonConfig{}),
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
runRules(store, cmd, nil, nil)
|
||||
}),
|
||||
}
|
||||
@@ -11,10 +11,6 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(usersCmd)
|
||||
}
|
||||
|
||||
var usersCmd = &cobra.Command{
|
||||
Use: "users",
|
||||
Short: "Users management utility",
|
||||
|
||||
@@ -3,7 +3,9 @@ package cmd
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -15,26 +17,26 @@ var usersAddCmd = &cobra.Command{
|
||||
Short: "Create a new user",
|
||||
Long: `Create a new user and add it to the database.`,
|
||||
Args: cobra.ExactArgs(2), //nolint:gomnd
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
user := &users.User{
|
||||
Username: args[0],
|
||||
Password: args[1],
|
||||
LockPassword: mustGetBool(cmd.Flags(), "lockPassword"),
|
||||
}
|
||||
servSettings, err := d.store.Settings.GetServer()
|
||||
checkErr(err)
|
||||
servSettings, err := store.Settings.GetServer()
|
||||
utils.CheckErr("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)
|
||||
s2, err := store.Settings.Get()
|
||||
utils.CheckErr("store.Settings.Get()", err)
|
||||
|
||||
userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root)
|
||||
checkErr(err)
|
||||
utils.CheckErr("s2.MakeUserDir", err)
|
||||
user.Scope = userHome
|
||||
|
||||
err = d.store.Users.Save(user)
|
||||
checkErr(err)
|
||||
err = store.Users.Save(user)
|
||||
utils.CheckErr("store.Users.Save", err)
|
||||
printUsers([]*users.User{user})
|
||||
}, pythonConfig{}),
|
||||
}),
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -14,11 +16,11 @@ var usersExportCmd = &cobra.Command{
|
||||
Long: `Export all users to a json or yaml file. Please indicate the
|
||||
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)
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
list, err := store.Users.Gets("")
|
||||
utils.CheckErr("store.Users.Gets", err)
|
||||
|
||||
err = marshal(args[0], list)
|
||||
checkErr(err)
|
||||
}, pythonConfig{}),
|
||||
utils.CheckErr("marshal", err)
|
||||
}),
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package cmd
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -26,7 +28,7 @@ var usersLsCmd = &cobra.Command{
|
||||
Run: findUsers,
|
||||
}
|
||||
|
||||
var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
var findUsers = cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
var (
|
||||
list []*users.User
|
||||
user *users.User
|
||||
@@ -36,16 +38,16 @@ var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
if len(args) == 1 {
|
||||
username, id := parseUsernameOrID(args[0])
|
||||
if username != "" {
|
||||
user, err = d.store.Users.Get("", username)
|
||||
user, err = store.Users.Get("", username)
|
||||
} else {
|
||||
user, err = d.store.Users.Get("", id)
|
||||
user, err = store.Users.Get("", id)
|
||||
}
|
||||
|
||||
list = []*users.User{user}
|
||||
} else {
|
||||
list, err = d.store.Users.Gets("")
|
||||
list, err = store.Users.Gets("")
|
||||
}
|
||||
|
||||
checkErr(err)
|
||||
utils.CheckErr("findUsers", err)
|
||||
printUsers(list)
|
||||
}, pythonConfig{})
|
||||
})
|
||||
+19
-20
@@ -8,7 +8,9 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -25,50 +27,47 @@ file. You can use this command to import new users to your
|
||||
installation. For that, just don't place their ID on the files
|
||||
list or set it to 0.`,
|
||||
Args: jsonYamlArg,
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
fd, err := os.Open(args[0])
|
||||
checkErr(err)
|
||||
utils.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)
|
||||
}
|
||||
utils.CheckErr("unmarshal", err)
|
||||
|
||||
if mustGetBool(cmd.Flags(), "replace") {
|
||||
oldUsers, err := d.store.Users.Gets("")
|
||||
checkErr(err)
|
||||
oldUsers, err := store.Users.Gets("")
|
||||
utils.CheckErr("store.Users.Gets", err)
|
||||
|
||||
err = marshal("users.backup.json", list)
|
||||
checkErr(err)
|
||||
utils.CheckErr("marshal users.backup.json", err)
|
||||
|
||||
for _, user := range oldUsers {
|
||||
err = d.store.Users.Delete(user.ID)
|
||||
checkErr(err)
|
||||
err = store.Users.Delete(user.ID)
|
||||
utils.CheckErr("store.Users.Delete", err)
|
||||
}
|
||||
}
|
||||
|
||||
overwrite := mustGetBool(cmd.Flags(), "overwrite")
|
||||
|
||||
for _, user := range list {
|
||||
onDB, err := d.store.Users.Get("", user.ID)
|
||||
onDB, err := store.Users.Get("", user.ID)
|
||||
|
||||
// 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")
|
||||
utils.CheckErr("", newErr)
|
||||
}
|
||||
|
||||
// If the usernames mismatch, check if there is another one in the DB
|
||||
// with the new username. If there is, print an error and cancel the
|
||||
// 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))
|
||||
if conflictuous, err := store.Users.Get("", user.Username); err == nil { //nolint:govet
|
||||
newErr := usernameConflictError(user.Username, conflictuous.ID, user.ID)
|
||||
utils.CheckErr("usernameConflictError", newErr)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -77,10 +76,10 @@ list or set it to 0.`,
|
||||
user.ID = 0
|
||||
}
|
||||
|
||||
err = d.store.Users.Save(user)
|
||||
checkErr(err)
|
||||
err = store.Users.Save(user)
|
||||
utils.CheckErr("store.Users.Save", err)
|
||||
}
|
||||
}, pythonConfig{}),
|
||||
}),
|
||||
}
|
||||
|
||||
func usernameConflictError(username string, originalID, newID uint) error {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -15,17 +17,17 @@ var usersRmCmd = &cobra.Command{
|
||||
Short: "Delete a user by username or id",
|
||||
Long: `Delete a user by username or id`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
username, id := parseUsernameOrID(args[0])
|
||||
var err error
|
||||
|
||||
if username != "" {
|
||||
err = d.store.Users.Delete(username)
|
||||
err = store.Users.Delete(username)
|
||||
} else {
|
||||
err = d.store.Users.Delete(id)
|
||||
err = store.Users.Delete(id)
|
||||
}
|
||||
|
||||
checkErr(err)
|
||||
fmt.Println("user deleted successfully")
|
||||
}, pythonConfig{}),
|
||||
utils.CheckErr("usersRmCmd", err)
|
||||
log.Println("user deleted successfully")
|
||||
}),
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package cmd
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -16,7 +18,7 @@ var usersUpdateCmd = &cobra.Command{
|
||||
Long: `Updates an existing user. Set the flags for the
|
||||
options you want to change.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
username, id := parseUsernameOrID(args[0])
|
||||
|
||||
var (
|
||||
@@ -25,14 +27,14 @@ options you want to change.`,
|
||||
)
|
||||
|
||||
if id != 0 {
|
||||
user, err = d.store.Users.Get("", id)
|
||||
user, err = store.Users.Get("", id)
|
||||
} else {
|
||||
user, err = d.store.Users.Get("", username)
|
||||
user, err = store.Users.Get("", username)
|
||||
}
|
||||
checkErr(err)
|
||||
utils.CheckErr("store.Users.Get", err)
|
||||
|
||||
err = d.store.Users.Update(user)
|
||||
checkErr(err)
|
||||
err = store.Users.Update(user)
|
||||
utils.CheckErr("store.Users.Update", err)
|
||||
printUsers([]*users.User{user})
|
||||
}, pythonConfig{}),
|
||||
}),
|
||||
}
|
||||
+13
-77
@@ -3,111 +3,42 @@ package cmd
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/storage/bolt"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
func checkErr(err error) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustGetString(flags *pflag.FlagSet, flag string) string {
|
||||
s, err := flags.GetString(flag)
|
||||
checkErr(err)
|
||||
utils.CheckErr("mustGetString", err)
|
||||
return s
|
||||
}
|
||||
|
||||
func mustGetBool(flags *pflag.FlagSet, flag string) bool {
|
||||
b, err := flags.GetBool(flag)
|
||||
checkErr(err)
|
||||
utils.CheckErr("mustGetBool", err)
|
||||
return b
|
||||
}
|
||||
|
||||
func mustGetUint(flags *pflag.FlagSet, flag string) uint {
|
||||
b, err := flags.GetUint(flag)
|
||||
checkErr(err)
|
||||
utils.CheckErr("mustGetUint", err)
|
||||
return b
|
||||
}
|
||||
|
||||
func generateKey() []byte {
|
||||
k, err := settings.GenerateKey()
|
||||
checkErr(err)
|
||||
return k
|
||||
}
|
||||
|
||||
type cobraFunc func(cmd *cobra.Command, args []string)
|
||||
type pythonFunc func(cmd *cobra.Command, args []string, data pythonData)
|
||||
|
||||
type pythonConfig struct {
|
||||
noDB bool
|
||||
allowNoDB bool
|
||||
}
|
||||
|
||||
type pythonData struct {
|
||||
hadDB bool
|
||||
store *storage.Storage
|
||||
}
|
||||
|
||||
func dbExists(path string) (bool, error) {
|
||||
stat, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return stat.Size() != 0, nil
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
d := filepath.Dir(path)
|
||||
_, err = os.Stat(d)
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet,gomnd
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
|
||||
return func(cmd *cobra.Command, args []string) {
|
||||
data := pythonData{hadDB: true}
|
||||
path := settings.Config.Server.Database
|
||||
exists, err := dbExists(path)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
} else if exists && cfg.noDB {
|
||||
log.Fatal(path + " already exists")
|
||||
} else if !exists && !cfg.noDB && !cfg.allowNoDB {
|
||||
log.Fatal(path + " does not exist. Please run 'filebrowser config init' first.")
|
||||
}
|
||||
|
||||
data.hadDB = exists
|
||||
db, err := storm.Open(path)
|
||||
checkErr(err)
|
||||
|
||||
defer db.Close()
|
||||
data.store, err = bolt.NewStorage(db)
|
||||
checkErr(err)
|
||||
fn(cmd, args, data)
|
||||
}
|
||||
}
|
||||
type pythonFunc func(cmd *cobra.Command, args []string, store *storage.Storage)
|
||||
|
||||
func marshal(filename string, data interface{}) error {
|
||||
fd, err := os.Create(filename)
|
||||
checkErr(err)
|
||||
|
||||
utils.CheckErr("os.Create", err)
|
||||
defer fd.Close()
|
||||
|
||||
switch ext := filepath.Ext(filename); ext {
|
||||
@@ -125,7 +56,7 @@ func marshal(filename string, data interface{}) error {
|
||||
|
||||
func unmarshal(filename string, data interface{}) error {
|
||||
fd, err := os.Open(filename)
|
||||
checkErr(err)
|
||||
utils.CheckErr("os.Open", err)
|
||||
defer fd.Close()
|
||||
|
||||
switch ext := filepath.Ext(filename); ext {
|
||||
@@ -150,3 +81,8 @@ func jsonYamlArg(cmd *cobra.Command, args []string) error {
|
||||
return errors.New("invalid format: " + ext)
|
||||
}
|
||||
}
|
||||
|
||||
func cobraCmd(fn pythonFunc) cobraFunc {
|
||||
return func(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/version"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
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)
|
||||
},
|
||||
}
|
||||
@@ -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,7 +1,7 @@
|
||||
server:
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
root: "/srv"
|
||||
root: "./srv"
|
||||
auth:
|
||||
method: password
|
||||
signup: false
|
||||
|
||||
@@ -91,7 +91,7 @@ func ParseSearch(value string) *SearchOptions {
|
||||
opts.LargerThan = updateSize(size)
|
||||
}
|
||||
if strings.HasPrefix(filter, "smallerThan=") {
|
||||
opts.Conditions["larger"] = true
|
||||
opts.Conditions["smaller"] = true
|
||||
size := strings.TrimPrefix(filter, "smallerThan=")
|
||||
opts.SmallerThan = updateSize(size)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Helper function to create error messages dynamically
|
||||
func errorMsg(extension, expectedType string, expectedMatch bool) string {
|
||||
matchStatus := "to match"
|
||||
if !expectedMatch {
|
||||
matchStatus = "to not match"
|
||||
}
|
||||
return fmt.Sprintf("Expected %s %s type '%s'", extension, matchStatus, expectedType)
|
||||
}
|
||||
|
||||
func TestIsMatchingType(t *testing.T) {
|
||||
// Test cases where IsMatchingType should return true
|
||||
trueTestCases := []struct {
|
||||
extension string
|
||||
expectedType string
|
||||
}{
|
||||
{".pdf", "pdf"},
|
||||
{".doc", "doc"},
|
||||
{".docx", "doc"},
|
||||
{".json", "text"},
|
||||
{".sh", "text"},
|
||||
{".zip", "archive"},
|
||||
{".rar", "archive"},
|
||||
}
|
||||
|
||||
for _, tc := range trueTestCases {
|
||||
assert.True(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, true))
|
||||
}
|
||||
|
||||
// Test cases where IsMatchingType should return false
|
||||
falseTestCases := []struct {
|
||||
extension string
|
||||
expectedType string
|
||||
}{
|
||||
{".mp4", "doc"},
|
||||
{".mp4", "text"},
|
||||
{".mp4", "archive"},
|
||||
}
|
||||
|
||||
for _, tc := range falseTestCases {
|
||||
assert.False(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, false))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSize(t *testing.T) {
|
||||
// Helper function for size error messages
|
||||
sizeErrorMsg := func(input string, expected, actual int) string {
|
||||
return fmt.Sprintf("Expected size for input '%s' to be %d, got %d", input, expected, actual)
|
||||
}
|
||||
|
||||
// Test cases for updateSize
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected int
|
||||
}{
|
||||
{"150", 150},
|
||||
{"invalid", 100},
|
||||
{"", 100},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
actual := updateSize(tc.input)
|
||||
assert.Equal(t, tc.expected, actual, sizeErrorMsg(tc.input, tc.expected, actual))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDoc(t *testing.T) {
|
||||
// Test cases where IsMatchingType should return true for document types
|
||||
docTrueTestCases := []struct {
|
||||
extension string
|
||||
expectedType string
|
||||
}{
|
||||
{".doc", "doc"},
|
||||
{".pdf", "doc"},
|
||||
}
|
||||
|
||||
for _, tc := range docTrueTestCases {
|
||||
assert.True(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, true))
|
||||
}
|
||||
|
||||
// Test case where IsMatchingType should return false for document types
|
||||
docFalseTestCases := []struct {
|
||||
extension string
|
||||
expectedType string
|
||||
}{
|
||||
{".mp4", "doc"},
|
||||
}
|
||||
|
||||
for _, tc := range docFalseTestCases {
|
||||
assert.False(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, false))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsText(t *testing.T) {
|
||||
// Test cases where IsMatchingType should return true for text types
|
||||
textTrueTestCases := []struct {
|
||||
extension string
|
||||
expectedType string
|
||||
}{
|
||||
{".json", "text"},
|
||||
{".sh", "text"},
|
||||
}
|
||||
|
||||
for _, tc := range textTrueTestCases {
|
||||
assert.True(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, true))
|
||||
}
|
||||
|
||||
// Test case where IsMatchingType should return false for text types
|
||||
textFalseTestCases := []struct {
|
||||
extension string
|
||||
expectedType string
|
||||
}{
|
||||
{".mp4", "text"},
|
||||
}
|
||||
|
||||
for _, tc := range textFalseTestCases {
|
||||
assert.False(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, false))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsArchive(t *testing.T) {
|
||||
// Test cases where IsMatchingType should return true for archive types
|
||||
archiveTrueTestCases := []struct {
|
||||
extension string
|
||||
expectedType string
|
||||
}{
|
||||
{".zip", "archive"},
|
||||
{".rar", "archive"},
|
||||
}
|
||||
|
||||
for _, tc := range archiveTrueTestCases {
|
||||
assert.True(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, true))
|
||||
}
|
||||
|
||||
// Test case where IsMatchingType should return false for archive types
|
||||
archiveFalseTestCases := []struct {
|
||||
extension string
|
||||
expectedType string
|
||||
}{
|
||||
{".mp4", "archive"},
|
||||
}
|
||||
|
||||
for _, tc := range archiveFalseTestCases {
|
||||
assert.False(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, false))
|
||||
}
|
||||
}
|
||||
+267
-164
@@ -1,59 +1,68 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"crypto/md5" //nolint:gosec
|
||||
"crypto/sha1" //nolint:gosec
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
filepath "path/filepath"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/rules"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
)
|
||||
|
||||
var (
|
||||
bytesInMegabyte int64 = 1000000
|
||||
pathMutexes = make(map[string]*sync.Mutex)
|
||||
pathMutexesMu sync.Mutex // Mutex to protect the pathMutexes map
|
||||
pathMutexes = make(map[string]*sync.Mutex)
|
||||
pathMutexesMu sync.Mutex // Mutex to protect the pathMutexes map
|
||||
)
|
||||
|
||||
type ReducedItem struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
ModTime time.Time `json:"modified"`
|
||||
IsDir bool `json:"isDir,omitempty"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// FileInfo describes a file.
|
||||
// reduced item is non-recursive reduced "Items", used to pass flat items array
|
||||
type FileInfo struct {
|
||||
*Listing
|
||||
Fs afero.Fs `json:"-"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Extension string `json:"-"`
|
||||
ModTime time.Time `json:"modified"`
|
||||
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"`
|
||||
Checksums map[string]string `json:"checksums,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Items []*FileInfo `json:"-"`
|
||||
ReducedItems []ReducedItem `json:"items,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Extension string `json:"-"`
|
||||
ModTime time.Time `json:"modified"`
|
||||
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"`
|
||||
Checksums map[string]string `json:"checksums,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
NumDirs int `json:"numDirs"`
|
||||
NumFiles int `json:"numFiles"`
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -62,38 +71,43 @@ 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.
|
||||
// Legacy file info method, only called on non-indexed directories.
|
||||
// Once indexing completes for the first time, NewFileInfo is never called.
|
||||
func NewFileInfo(opts FileOptions) (*FileInfo, error) {
|
||||
|
||||
index := GetIndex(rootPath)
|
||||
if !opts.Checker.Check(opts.Path) {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
file, err := stat(opts.Path, opts) // Pass opts.Path here
|
||||
file, err := stat(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if opts.Expand {
|
||||
if file.IsDir {
|
||||
if err := file.readListing(opts.Path, opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
|
||||
if err = file.readListing(opts.Path, opts.Checker, opts.ReadHeader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cleanedItems := []ReducedItem{}
|
||||
for _, item := range file.Items {
|
||||
// This is particularly useful for root of index, while indexing hasn't finished.
|
||||
// adds the directory sizes for directories that have been indexed already.
|
||||
if item.IsDir {
|
||||
adjustedPath := index.makeIndexPath(opts.Path+"/"+item.Name, true)
|
||||
info, _ := index.GetMetadataInfo(adjustedPath)
|
||||
item.Size = info.Size
|
||||
}
|
||||
cleanedItems = append(cleanedItems, ReducedItem{
|
||||
Name: item.Name,
|
||||
Size: item.Size,
|
||||
IsDir: item.IsDir,
|
||||
ModTime: item.ModTime,
|
||||
Type: item.Type,
|
||||
})
|
||||
}
|
||||
|
||||
file.Items = nil
|
||||
file.ReducedItems = cleanedItems
|
||||
return file, nil
|
||||
}
|
||||
err = file.detectType(opts.Path, opts.Modify, opts.Content, true)
|
||||
@@ -113,113 +127,82 @@ func FileInfoFaster(opts FileOptions) (*FileInfo, error) {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
index := GetIndex(rootPath)
|
||||
trimmed := strings.TrimPrefix(opts.Path, "/")
|
||||
if trimmed == "" {
|
||||
trimmed = "/"
|
||||
adjustedPath := index.makeIndexPath(opts.Path, opts.IsDir)
|
||||
if opts.IsDir {
|
||||
info, exists := index.GetMetadataInfo(adjustedPath)
|
||||
if exists && !opts.Content {
|
||||
// Let's not refresh if less than a second has passed
|
||||
if time.Since(info.CacheTime) > time.Second {
|
||||
go RefreshFileInfo(opts) //nolint:errcheck
|
||||
}
|
||||
// refresh cache after
|
||||
return &info, nil
|
||||
}
|
||||
}
|
||||
adjustedPath := makeIndexPath(trimmed, index.Root)
|
||||
var info FileInfo
|
||||
info, exists := index.GetMetadataInfo(adjustedPath)
|
||||
if exists && !opts.Content {
|
||||
// Check if the cache time is less than 1 second
|
||||
if time.Since(info.CacheTime) > time.Second {
|
||||
go refreshFileInfo(opts)
|
||||
}
|
||||
// refresh cache after
|
||||
return &info, nil
|
||||
} else {
|
||||
// don't bother caching content
|
||||
if opts.Content {
|
||||
file, err := NewFileInfo(opts)
|
||||
return file, err
|
||||
}
|
||||
updated := refreshFileInfo(opts)
|
||||
if !updated {
|
||||
file, err := NewFileInfo(opts)
|
||||
return file, err
|
||||
}
|
||||
info, exists = index.GetMetadataInfo(adjustedPath)
|
||||
if !exists || info.Name == "" {
|
||||
return &FileInfo{}, errors.ErrEmptyKey
|
||||
}
|
||||
return &info, nil
|
||||
// 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 + "/" + filepath.Base(opts.Path))
|
||||
if !exists || info.Name == "" {
|
||||
return NewFileInfo(opts)
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func refreshFileInfo(opts FileOptions) bool {
|
||||
func RefreshFileInfo(opts FileOptions) error {
|
||||
if !opts.Checker.Check(opts.Path) {
|
||||
return false
|
||||
return fmt.Errorf("permission denied: %s", opts.Path)
|
||||
}
|
||||
index := GetIndex(rootPath)
|
||||
trimmed := strings.TrimPrefix(opts.Path, "/")
|
||||
if trimmed == "" {
|
||||
trimmed = "/"
|
||||
}
|
||||
adjustedPath := makeIndexPath(trimmed, index.Root)
|
||||
file, err := stat(opts.Path, opts) // Pass opts.Path here
|
||||
adjustedPath := index.makeIndexPath(opts.Path, opts.IsDir)
|
||||
file, err := stat(opts)
|
||||
if err != nil {
|
||||
return false
|
||||
return fmt.Errorf("File/folder does not exist to refresh data: %s", opts.Path)
|
||||
}
|
||||
_ = file.detectType(adjustedPath, true, opts.Content, opts.ReadHeader)
|
||||
_ = file.detectType(opts.Path, true, opts.Content, opts.ReadHeader)
|
||||
if file.IsDir {
|
||||
err := file.readListing(opts.Path, opts.Checker, opts.ReadHeader)
|
||||
if err != nil {
|
||||
return false
|
||||
return fmt.Errorf("Dir info could not be read: %s", opts.Path)
|
||||
}
|
||||
//_, exists := index.GetFileMetadata(adjustedPath)
|
||||
|
||||
return index.UpdateFileMetadata(adjustedPath, *file)
|
||||
} else {
|
||||
//_, exists := index.GetFileMetadata(adjustedPath)
|
||||
return index.UpdateFileMetadata(adjustedPath, *file)
|
||||
}
|
||||
result := index.UpdateFileMetadata(adjustedPath, *file)
|
||||
if !result {
|
||||
return fmt.Errorf("File/folder does not exist in metadata: %s", adjustedPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stat(path string, opts FileOptions) (*FileInfo, error) {
|
||||
var file *FileInfo
|
||||
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
|
||||
info, _, err := lstaterFs.LstatIfPossible(path)
|
||||
if err == nil {
|
||||
file = &FileInfo{
|
||||
Fs: opts.Fs,
|
||||
Path: opts.Path,
|
||||
Name: info.Name(),
|
||||
ModTime: info.ModTime(),
|
||||
Mode: info.Mode(),
|
||||
Size: info.Size(),
|
||||
Extension: filepath.Ext(info.Name()),
|
||||
Token: opts.Token,
|
||||
}
|
||||
if info.IsDir() {
|
||||
file.IsDir = true
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
file.IsSymlink = true
|
||||
}
|
||||
}
|
||||
func stat(opts FileOptions) (*FileInfo, error) {
|
||||
info, err := os.Lstat(opts.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if file == nil || file.IsSymlink {
|
||||
info, err := opts.Fs.Stat(opts.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if file != nil && file.IsSymlink {
|
||||
file.Size = info.Size()
|
||||
file.IsDir = info.IsDir()
|
||||
return file, nil
|
||||
}
|
||||
|
||||
file = &FileInfo{
|
||||
Fs: opts.Fs,
|
||||
Path: opts.Path,
|
||||
Name: info.Name(),
|
||||
ModTime: info.ModTime(),
|
||||
Mode: info.Mode(),
|
||||
IsDir: info.IsDir(),
|
||||
Size: info.Size(),
|
||||
Extension: filepath.Ext(info.Name()),
|
||||
Token: opts.Token,
|
||||
file := &FileInfo{
|
||||
Path: opts.Path,
|
||||
Name: info.Name(),
|
||||
ModTime: info.ModTime(),
|
||||
Mode: info.Mode(),
|
||||
Size: info.Size(),
|
||||
Extension: filepath.Ext(info.Name()),
|
||||
Token: opts.Token,
|
||||
}
|
||||
if info.IsDir() {
|
||||
file.IsDir = true
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
file.IsSymlink = true
|
||||
targetInfo, err := os.Stat(opts.Path)
|
||||
if err == nil {
|
||||
file.Size = targetInfo.Size()
|
||||
file.IsDir = targetInfo.IsDir()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +220,7 @@ func (i *FileInfo) Checksum(algo string) error {
|
||||
i.Checksums = map[string]string{}
|
||||
}
|
||||
|
||||
reader, err := i.Fs.Open(i.Path)
|
||||
reader, err := os.Open(i.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -266,37 +249,141 @@ func (i *FileInfo) Checksum(algo string) error {
|
||||
|
||||
// RealPath gets the real path for the file, resolving symlinks if supported.
|
||||
func (i *FileInfo) RealPath() string {
|
||||
if realPathFs, ok := i.Fs.(interface {
|
||||
RealPath(name string) (fPath string, err error)
|
||||
}); ok {
|
||||
realPath, err := realPathFs.RealPath(i.Path)
|
||||
if err == nil {
|
||||
return realPath
|
||||
}
|
||||
realPath, err := filepath.EvalSymlinks(i.Path)
|
||||
if err == nil {
|
||||
return realPath
|
||||
}
|
||||
return i.Path
|
||||
}
|
||||
|
||||
func GetRealPath(relativePath ...string) (string, bool, error) {
|
||||
combined := []string{settings.Config.Server.Root}
|
||||
for _, path := range relativePath {
|
||||
combined = append(combined, strings.TrimPrefix(path, settings.Config.Server.Root))
|
||||
}
|
||||
joinedPath := filepath.Join(combined...)
|
||||
// Convert relative path to absolute path
|
||||
absolutePath, err := filepath.Abs(joinedPath)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if !Exists(absolutePath) {
|
||||
return absolutePath, false, nil // return without error
|
||||
}
|
||||
// Resolve symlinks and get the real path
|
||||
return resolveSymlinks(absolutePath)
|
||||
}
|
||||
|
||||
func DeleteFiles(absPath string, opts FileOptions) error {
|
||||
err := os.RemoveAll(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Path = filepath.Dir(absPath)
|
||||
err = RefreshFileInfo(opts)
|
||||
if err != nil {
|
||||
return errors.ErrEmptyKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteDirectory(opts FileOptions) error {
|
||||
// Ensure the parent directories exist
|
||||
err := os.MkdirAll(opts.Path, 0775)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Path = filepath.Dir(opts.Path)
|
||||
err = RefreshFileInfo(opts)
|
||||
if err != nil {
|
||||
return errors.ErrEmptyKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteFile(opts FileOptions, in io.Reader) error {
|
||||
dst := opts.Path
|
||||
parentDir := filepath.Dir(dst)
|
||||
// Split the directory from the destination path
|
||||
dir := filepath.Dir(dst)
|
||||
|
||||
// Create the directory and all necessary parents
|
||||
err := os.MkdirAll(dir, 0775)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return i.Path
|
||||
// Open the file for writing (create if it doesn't exist, truncate if it does)
|
||||
file, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Copy the contents from the reader to the file
|
||||
_, err = io.Copy(file, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Path = parentDir
|
||||
err = RefreshFileInfo(opts)
|
||||
if err != nil {
|
||||
return errors.ErrEmptyKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveSymlinks resolves symlinks in the given path
|
||||
func resolveSymlinks(path string) (string, bool, error) {
|
||||
for {
|
||||
// Get the file info
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
// Check if it's a symlink
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
// Read the symlink target
|
||||
target, err := os.Readlink(path)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
// Resolve the target relative to the symlink's directory
|
||||
path = filepath.Join(filepath.Dir(path), target)
|
||||
} else {
|
||||
// Not a symlink, so return the resolved path and check if it's a directory
|
||||
return path, info.IsDir(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addContent reads and sets content based on the file type.
|
||||
func (i *FileInfo) addContent(path string) error {
|
||||
if !i.IsDir {
|
||||
afs := &afero.Afero{Fs: i.Fs}
|
||||
content, err := afs.ReadFile(path)
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := string(string(content))
|
||||
if !utf8.ValidString(c) {
|
||||
stringContent := string(content)
|
||||
if !utf8.ValidString(stringContent) {
|
||||
return nil
|
||||
}
|
||||
i.Content = string(c)
|
||||
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 {
|
||||
@@ -341,7 +428,6 @@ func (i *FileInfo) detectType(path string, modify, saveContent, readHeader bool)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if i.Type == "" {
|
||||
i.Type = "blob"
|
||||
if saveContent {
|
||||
@@ -354,15 +440,15 @@ func (i *FileInfo) detectType(path string, modify, saveContent, readHeader bool)
|
||||
|
||||
// readFirstBytes reads the first bytes of the file.
|
||||
func (i *FileInfo) readFirstBytes() []byte {
|
||||
reader, err := i.Fs.Open(i.Path)
|
||||
file, err := os.Open(i.Path)
|
||||
if err != nil {
|
||||
i.Type = "blob"
|
||||
return nil
|
||||
}
|
||||
defer reader.Close()
|
||||
defer file.Close()
|
||||
|
||||
buffer := make([]byte, 512) //nolint:gomnd
|
||||
n, err := reader.Read(buffer)
|
||||
n, err := file.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
i.Type = "blob"
|
||||
return nil
|
||||
@@ -383,7 +469,8 @@ func (i *FileInfo) detectSubtitles(parentDir string) {
|
||||
// Directory must have been deleted, remove it from the index
|
||||
return
|
||||
}
|
||||
// Read the directory contents
|
||||
defer dir.Close() // Ensure directory handle is closed
|
||||
|
||||
files, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -408,20 +495,24 @@ func (i *FileInfo) detectSubtitles(parentDir string) {
|
||||
|
||||
// readListing reads the contents of a directory and fills the listing.
|
||||
func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bool) error {
|
||||
afs := &afero.Afero{Fs: i.Fs}
|
||||
dir, err := afs.ReadDir(i.Path)
|
||||
dir, err := os.Open(i.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
files, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listing := &Listing{
|
||||
listing := &FileInfo{
|
||||
Items: []*FileInfo{},
|
||||
Path: i.Path,
|
||||
NumDirs: 0,
|
||||
NumFiles: 0,
|
||||
}
|
||||
|
||||
for _, f := range dir {
|
||||
for _, f := range files {
|
||||
name := f.Name()
|
||||
fPath := filepath.Join(i.Path, name)
|
||||
|
||||
@@ -432,7 +523,7 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
|
||||
isSymlink, isInvalidLink := false, false
|
||||
if IsSymlink(f.Mode()) {
|
||||
isSymlink = true
|
||||
info, err := i.Fs.Stat(fPath)
|
||||
info, err := os.Stat(fPath)
|
||||
if err == nil {
|
||||
f = info
|
||||
} else {
|
||||
@@ -471,9 +562,10 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
|
||||
listing.Items = append(listing.Items, file)
|
||||
}
|
||||
|
||||
i.Listing = listing
|
||||
i.Items = listing.Items
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsNamedPipe(mode os.FileMode) bool {
|
||||
return mode&os.ModeNamedPipe != 0
|
||||
}
|
||||
@@ -494,3 +586,14 @@ func getMutex(path string) *sync.Mutex {
|
||||
|
||||
return pathMutexes[path]
|
||||
}
|
||||
|
||||
func Exists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+101
-70
@@ -1,9 +1,9 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -11,22 +11,12 @@ import (
|
||||
"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
|
||||
Directories map[string]FileInfo
|
||||
NumDirs int
|
||||
NumFiles int
|
||||
inProgress bool
|
||||
quickList []File
|
||||
LastIndexed time.Time
|
||||
mu sync.RWMutex
|
||||
}
|
||||
@@ -48,16 +38,12 @@ func indexingScheduler(intervalMinutes uint32) {
|
||||
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
|
||||
@@ -79,89 +65,134 @@ func indexingScheduler(intervalMinutes uint32) {
|
||||
|
||||
// Define a function to recursively index files and directories
|
||||
func (si *Index) indexFiles(path string) error {
|
||||
// Check if the current directory has been modified since the last indexing
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
adjustedPath := makeIndexPath(path, si.Root)
|
||||
// Ensure path is cleaned and normalized
|
||||
adjustedPath := si.makeIndexPath(path, true)
|
||||
|
||||
// Open the directory
|
||||
dir, err := os.Open(path)
|
||||
if err != nil {
|
||||
// Directory must have been deleted, remove it from the index
|
||||
// If the directory can't be opened (e.g., deleted), remove it from the index
|
||||
si.RemoveDirectory(adjustedPath)
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
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()
|
||||
// Check if the directory is already up-to-date
|
||||
if dirInfo.ModTime().Before(si.LastIndexed) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read the directory contents
|
||||
// Read 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)
|
||||
|
||||
// Recursively process files and directories
|
||||
fileInfos := []*FileInfo{}
|
||||
var totalSize int64
|
||||
var numDirs, numFiles int
|
||||
|
||||
for _, file := range files {
|
||||
parentInfo := &FileInfo{
|
||||
Name: file.Name(),
|
||||
Size: file.Size(),
|
||||
ModTime: file.ModTime(),
|
||||
IsDir: file.IsDir(),
|
||||
}
|
||||
childInfo, err := si.InsertInfo(path, parentInfo)
|
||||
if err != nil {
|
||||
// Log error, but continue processing other files
|
||||
continue
|
||||
}
|
||||
|
||||
// Accumulate directory size and items
|
||||
totalSize += childInfo.Size
|
||||
if childInfo.IsDir {
|
||||
numDirs++
|
||||
} else {
|
||||
numFiles++
|
||||
}
|
||||
_ = childInfo.detectType(path, true, false, false)
|
||||
fileInfos = append(fileInfos, childInfo)
|
||||
}
|
||||
|
||||
// Create FileInfo for the current directory
|
||||
dirFileInfo := &FileInfo{
|
||||
Items: fileInfos,
|
||||
Name: filepath.Base(path),
|
||||
Size: totalSize,
|
||||
ModTime: dirInfo.ModTime(),
|
||||
CacheTime: time.Now(),
|
||||
IsDir: true,
|
||||
NumDirs: numDirs,
|
||||
NumFiles: numFiles,
|
||||
}
|
||||
|
||||
// Add directory to index
|
||||
si.mu.Lock()
|
||||
si.Directories[adjustedPath] = *dirFileInfo
|
||||
si.NumDirs += numDirs
|
||||
si.NumFiles += numFiles
|
||||
si.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (si *Index) InsertFiles(path string) {
|
||||
adjustedPath := makeIndexPath(path, si.Root)
|
||||
subDirectory := Directory{}
|
||||
buffer := bytes.Buffer{}
|
||||
// InsertInfo function to handle adding a file or directory into the index
|
||||
func (si *Index) InsertInfo(parentPath string, file *FileInfo) (*FileInfo, error) {
|
||||
filePath := filepath.Join(parentPath, file.Name)
|
||||
|
||||
for _, f := range si.GetQuickList() {
|
||||
if !f.IsDir {
|
||||
buffer.WriteString(f.Name + ";")
|
||||
si.UpdateCount("files")
|
||||
// Check if it's a directory and recursively index it
|
||||
if file.IsDir {
|
||||
// Recursively index directory
|
||||
err := si.indexFiles(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return directory info from the index
|
||||
adjustedPath := si.makeIndexPath(filePath, true)
|
||||
si.mu.RLock()
|
||||
dirInfo := si.Directories[adjustedPath]
|
||||
si.mu.RUnlock()
|
||||
return &dirInfo, nil
|
||||
}
|
||||
// Use GetMetadataInfo and SetFileMetadata for safer read and write operations
|
||||
subDirectory.Files = buffer.String()
|
||||
si.SetDirectoryInfo(adjustedPath, subDirectory)
|
||||
|
||||
// Create FileInfo for regular files
|
||||
fileInfo := &FileInfo{
|
||||
Path: filePath,
|
||||
Name: file.Name,
|
||||
Size: file.Size,
|
||||
ModTime: file.ModTime,
|
||||
IsDir: false,
|
||||
}
|
||||
|
||||
return fileInfo, nil
|
||||
}
|
||||
|
||||
func (si *Index) InsertDirs(path string) {
|
||||
adjustedPath := makeIndexPath(path, si.Root)
|
||||
for _, f := range si.GetQuickList() {
|
||||
if f.IsDir {
|
||||
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 makeIndexPath(path string, root string) string {
|
||||
if path == root {
|
||||
func (si *Index) makeIndexPath(subPath string, isDir bool) string {
|
||||
if si.Root == subPath {
|
||||
return "/"
|
||||
}
|
||||
adjustedPath := strings.TrimPrefix(path, root+"/")
|
||||
// clean path
|
||||
subPath = strings.TrimSuffix(subPath, "/")
|
||||
// remove index prefix
|
||||
adjustedPath := strings.TrimPrefix(subPath, si.Root)
|
||||
// remove trailing slash
|
||||
adjustedPath = strings.TrimSuffix(adjustedPath, "/")
|
||||
// add leading slash for root of index
|
||||
if adjustedPath == "" {
|
||||
adjustedPath = "/"
|
||||
} else if !isDir {
|
||||
adjustedPath = filepath.Dir(adjustedPath)
|
||||
}
|
||||
if !strings.HasPrefix(adjustedPath, "/") {
|
||||
adjustedPath = "/" + adjustedPath
|
||||
}
|
||||
return adjustedPath
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package files
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
@@ -23,18 +24,26 @@ func BenchmarkFillIndex(b *testing.B) {
|
||||
func (si *Index) createMockData(numDirs, numFilesPerDir int) {
|
||||
for i := 0; i < numDirs; i++ {
|
||||
dirName := generateRandomPath(rand.Intn(3) + 1)
|
||||
files := []File{}
|
||||
// Append a new Directory to the slice
|
||||
files := []*FileInfo{} // Slice of FileInfo
|
||||
|
||||
// Simulating files and directories with FileInfo
|
||||
for j := 0; j < numFilesPerDir; j++ {
|
||||
newFile := File{
|
||||
Name: "file-" + getRandomTerm() + getRandomExtension(),
|
||||
IsDir: false,
|
||||
newFile := &FileInfo{
|
||||
Name: "file-" + getRandomTerm() + getRandomExtension(),
|
||||
IsDir: false,
|
||||
Size: rand.Int63n(1000), // Random size
|
||||
ModTime: time.Now().Add(-time.Duration(rand.Intn(100)) * time.Hour), // Random mod time
|
||||
}
|
||||
files = append(files, newFile)
|
||||
}
|
||||
si.UpdateQuickListForTests(files)
|
||||
si.InsertFiles(dirName)
|
||||
si.InsertDirs(dirName)
|
||||
|
||||
// Simulate inserting files into index
|
||||
for _, file := range files {
|
||||
_, err := si.InsertInfo(dirName, file)
|
||||
if err != nil {
|
||||
fmt.Println("Error inserting file:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+89
-58
@@ -2,7 +2,6 @@ package files
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -30,12 +29,17 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
|
||||
continue
|
||||
}
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
for dirName, dir := range si.Directories {
|
||||
isDir := true
|
||||
files := strings.Split(dir.Files, ";")
|
||||
files := []string{}
|
||||
for _, item := range dir.Items {
|
||||
if !item.IsDir {
|
||||
files = append(files, item.Name)
|
||||
}
|
||||
}
|
||||
value, found := sessionInProgress.Load(sourceSession)
|
||||
if !found || value != runningHash {
|
||||
si.mu.Unlock()
|
||||
return []string{}, map[string]map[string]bool{}
|
||||
}
|
||||
if count > maxSearchResults {
|
||||
@@ -46,7 +50,9 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
|
||||
continue // path not matched
|
||||
}
|
||||
fileTypes := map[string]bool{}
|
||||
matches, fileType := containsSearchTerm(dirName, searchTerm, *searchOptions, isDir, fileTypes)
|
||||
si.mu.Unlock()
|
||||
matches, fileType := si.containsSearchTerm(dirName, searchTerm, *searchOptions, isDir, fileTypes)
|
||||
si.mu.Lock()
|
||||
if matches {
|
||||
fileListTypes[pathName] = fileType
|
||||
matching = append(matching, pathName)
|
||||
@@ -65,10 +71,11 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
|
||||
if count > maxSearchResults {
|
||||
break
|
||||
}
|
||||
fullName := pathName + file
|
||||
fullName := strings.TrimLeft(pathName+file, "/")
|
||||
fileTypes := map[string]bool{}
|
||||
|
||||
matches, fileType := containsSearchTerm(fullName, searchTerm, *searchOptions, isDir, fileTypes)
|
||||
si.mu.Unlock()
|
||||
matches, fileType := si.containsSearchTerm(fullName, searchTerm, *searchOptions, isDir, fileTypes)
|
||||
si.mu.Lock()
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
@@ -77,6 +84,7 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
|
||||
count++
|
||||
}
|
||||
}
|
||||
si.mu.Unlock()
|
||||
}
|
||||
// Sort the strings based on the number of elements after splitting by "/"
|
||||
sort.Slice(matching, func(i, j int) bool {
|
||||
@@ -102,65 +110,88 @@ func scopedPathNameFilter(pathName string, scope string, isDir bool) string {
|
||||
return pathName
|
||||
}
|
||||
|
||||
func containsSearchTerm(pathName string, searchTerm string, options SearchOptions, isDir bool, fileTypes map[string]bool) (bool, map[string]bool) {
|
||||
func (si *Index) containsSearchTerm(pathName string, searchTerm string, options SearchOptions, isDir bool, fileTypes map[string]bool) (bool, map[string]bool) {
|
||||
largerThan := int64(options.LargerThan) * 1024 * 1024
|
||||
smallerThan := int64(options.SmallerThan) * 1024 * 1024
|
||||
conditions := options.Conditions
|
||||
path := getLastPathComponent(pathName)
|
||||
// Convert to lowercase once
|
||||
fileName := filepath.Base(pathName)
|
||||
adjustedPath := si.makeIndexPath(pathName, isDir)
|
||||
|
||||
// Convert to lowercase if not exact match
|
||||
if !conditions["exact"] {
|
||||
path = strings.ToLower(path)
|
||||
fileName = strings.ToLower(fileName)
|
||||
searchTerm = strings.ToLower(searchTerm)
|
||||
}
|
||||
if strings.Contains(path, searchTerm) {
|
||||
// Calculate fileSize only if needed
|
||||
var fileSize int64
|
||||
matchesAllConditions := true
|
||||
extension := filepath.Ext(path)
|
||||
for _, k := range AllFiletypeOptions {
|
||||
if IsMatchingType(extension, k) {
|
||||
fileTypes[k] = true
|
||||
|
||||
// Check if the file name contains the search term
|
||||
if !strings.Contains(fileName, searchTerm) {
|
||||
return false, map[string]bool{}
|
||||
}
|
||||
|
||||
// Initialize file size and fileTypes map
|
||||
var fileSize int64
|
||||
extension := filepath.Ext(fileName)
|
||||
|
||||
// Collect file types
|
||||
for _, k := range AllFiletypeOptions {
|
||||
if IsMatchingType(extension, k) {
|
||||
fileTypes[k] = true
|
||||
}
|
||||
}
|
||||
fileTypes["dir"] = isDir
|
||||
// Get file info if needed for size-related conditions
|
||||
if largerThan > 0 || smallerThan > 0 {
|
||||
fileInfo, exists := si.GetMetadataInfo(adjustedPath)
|
||||
if !exists {
|
||||
return false, fileTypes
|
||||
} else if !isDir {
|
||||
// Look for specific file in ReducedItems
|
||||
for _, item := range fileInfo.ReducedItems {
|
||||
lower := strings.ToLower(item.Name)
|
||||
if strings.Contains(lower, searchTerm) {
|
||||
if item.Size == 0 {
|
||||
return false, fileTypes
|
||||
}
|
||||
fileSize = item.Size
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fileSize = fileInfo.Size
|
||||
}
|
||||
if fileSize == 0 {
|
||||
return false, fileTypes
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate all conditions
|
||||
for t, v := range conditions {
|
||||
if t == "exact" {
|
||||
continue
|
||||
}
|
||||
switch t {
|
||||
case "larger":
|
||||
if largerThan > 0 {
|
||||
if fileSize <= largerThan {
|
||||
return false, fileTypes
|
||||
}
|
||||
}
|
||||
case "smaller":
|
||||
if smallerThan > 0 {
|
||||
if fileSize >= smallerThan {
|
||||
return false, fileTypes
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Handle other file type conditions
|
||||
notMatchType := v != fileTypes[t]
|
||||
if notMatchType {
|
||||
return false, fileTypes
|
||||
}
|
||||
}
|
||||
fileTypes["dir"] = isDir
|
||||
for t, v := range conditions {
|
||||
if t == "exact" {
|
||||
continue
|
||||
}
|
||||
var matchesCondition bool
|
||||
switch t {
|
||||
case "larger":
|
||||
if fileSize == 0 {
|
||||
fileSize = getFileSize(pathName)
|
||||
}
|
||||
matchesCondition = fileSize > int64(options.LargerThan)*bytesInMegabyte
|
||||
case "smaller":
|
||||
if fileSize == 0 {
|
||||
fileSize = getFileSize(pathName)
|
||||
}
|
||||
matchesCondition = fileSize < int64(options.SmallerThan)*bytesInMegabyte
|
||||
default:
|
||||
matchesCondition = v == fileTypes[t]
|
||||
}
|
||||
if !matchesCondition {
|
||||
matchesAllConditions = false
|
||||
}
|
||||
}
|
||||
return matchesAllConditions, fileTypes
|
||||
}
|
||||
// Clear variables and return
|
||||
return false, map[string]bool{}
|
||||
}
|
||||
|
||||
func getFileSize(filepath string) int64 {
|
||||
fileInfo, err := os.Stat(rootPath + "/" + filepath)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return fileInfo.Size()
|
||||
}
|
||||
|
||||
func getLastPathComponent(path string) string {
|
||||
// Use filepath.Base to extract the last component of the path
|
||||
return filepath.Base(path)
|
||||
return true, fileTypes
|
||||
}
|
||||
|
||||
func generateRandomHash(length int) string {
|
||||
|
||||
+96
-168
@@ -11,7 +11,7 @@ func BenchmarkSearchAllIndexes(b *testing.B) {
|
||||
InitializeIndex(5, false)
|
||||
si := GetIndex(rootPath)
|
||||
|
||||
si.createMockData(50, 3) // 1000 dirs, 3 files per dir
|
||||
si.createMockData(50, 3) // 50 dirs, 3 files per dir
|
||||
|
||||
// Generate 100 random search terms
|
||||
searchTerms := generateRandomSearchTerms(100)
|
||||
@@ -26,87 +26,90 @@ func BenchmarkSearchAllIndexes(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
// loop over test files and compare output
|
||||
func TestParseSearch(t *testing.T) {
|
||||
value := ParseSearch("my test search")
|
||||
want := &SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
"exact": false,
|
||||
tests := []struct {
|
||||
input string
|
||||
want *SearchOptions
|
||||
}{
|
||||
{
|
||||
input: "my test search",
|
||||
want: &SearchOptions{
|
||||
Conditions: map[string]bool{"exact": false},
|
||||
Terms: []string{"my test search"},
|
||||
},
|
||||
},
|
||||
Terms: []string{"my test search"},
|
||||
}
|
||||
if !reflect.DeepEqual(value, want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||
}
|
||||
value = ParseSearch("case:exact my|test|search")
|
||||
want = &SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
"exact": true,
|
||||
{
|
||||
input: "case:exact my|test|search",
|
||||
want: &SearchOptions{
|
||||
Conditions: map[string]bool{"exact": true},
|
||||
Terms: []string{"my", "test", "search"},
|
||||
},
|
||||
},
|
||||
Terms: []string{"my", "test", "search"},
|
||||
}
|
||||
if !reflect.DeepEqual(value, want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||
}
|
||||
value = ParseSearch("type:largerThan=100 type:smallerThan=1000 test")
|
||||
want = &SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
"exact": false,
|
||||
"larger": true,
|
||||
{
|
||||
input: "type:largerThan=100 type:smallerThan=1000 test",
|
||||
want: &SearchOptions{
|
||||
Conditions: map[string]bool{"exact": false, "larger": true, "smaller": true},
|
||||
Terms: []string{"test"},
|
||||
LargerThan: 100,
|
||||
SmallerThan: 1000,
|
||||
},
|
||||
},
|
||||
Terms: []string{"test"},
|
||||
LargerThan: 100,
|
||||
SmallerThan: 1000,
|
||||
}
|
||||
if !reflect.DeepEqual(value, want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||
}
|
||||
value = ParseSearch("type:audio thisfile")
|
||||
want = &SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
"exact": false,
|
||||
"audio": true,
|
||||
{
|
||||
input: "type:audio thisfile",
|
||||
want: &SearchOptions{
|
||||
Conditions: map[string]bool{"exact": false, "audio": true},
|
||||
Terms: []string{"thisfile"},
|
||||
},
|
||||
},
|
||||
Terms: []string{"thisfile"},
|
||||
}
|
||||
if !reflect.DeepEqual(value, want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
value := ParseSearch(tt.input)
|
||||
if !reflect.DeepEqual(value, tt.want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
go si.createMockData(100, 100) // Creating mock data concurrently
|
||||
for _, term := range searchTerms {
|
||||
go si.Search(term, "/", "test")
|
||||
go si.Search(term, "/", "test") // Search concurrently
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchIndexes(t *testing.T) {
|
||||
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;",
|
||||
Directories: map[string]FileInfo{
|
||||
"test": {Items: []*FileInfo{{Name: "audio1.wav"}}},
|
||||
"test/path": {Items: []*FileInfo{{Name: "file.txt"}}},
|
||||
"new/test": {Items: []*FileInfo{
|
||||
{Name: "audio.wav"},
|
||||
{Name: "video.mp4"},
|
||||
{Name: "video.MP4"},
|
||||
}},
|
||||
"new/test/path": {Items: []*FileInfo{{Name: "archive.zip"}}},
|
||||
"/firstDir": {Items: []*FileInfo{
|
||||
{Name: "archive.zip", Size: 100},
|
||||
{Name: "thisIsDir", IsDir: true, Size: 2 * 1024 * 1024},
|
||||
}},
|
||||
"/firstDir/thisIsDir": {
|
||||
Items: []*FileInfo{
|
||||
{Name: "hi.txt"},
|
||||
},
|
||||
Size: 2 * 1024 * 1024,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
search string
|
||||
scope string
|
||||
@@ -118,7 +121,7 @@ func TestSearchIndexes(t *testing.T) {
|
||||
scope: "/new/",
|
||||
expectedResult: []string{"test/audio.wav"},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"test/audio.wav": map[string]bool{"audio": true, "dir": false},
|
||||
"test/audio.wav": {"audio": true, "dir": false},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -126,16 +129,41 @@ func TestSearchIndexes(t *testing.T) {
|
||||
scope: "/",
|
||||
expectedResult: []string{"test/", "new/test/"},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"test/": map[string]bool{"dir": true},
|
||||
"new/test/": map[string]bool{"dir": true},
|
||||
"test/": {"dir": true},
|
||||
"new/test/": {"dir": true},
|
||||
},
|
||||
},
|
||||
{
|
||||
search: "archive",
|
||||
scope: "/",
|
||||
expectedResult: []string{"new/test/path/archive.zip"},
|
||||
expectedResult: []string{"firstDir/archive.zip", "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": {"archive": true, "dir": false},
|
||||
"firstDir/archive.zip": {"archive": true, "dir": false},
|
||||
},
|
||||
},
|
||||
{
|
||||
search: "arch",
|
||||
scope: "/firstDir",
|
||||
expectedResult: []string{"archive.zip"},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"archive.zip": {"archive": true, "dir": false},
|
||||
},
|
||||
},
|
||||
{
|
||||
search: "isdir",
|
||||
scope: "/",
|
||||
expectedResult: []string{"firstDir/thisIsDir/"},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"firstDir/thisIsDir/": {"dir": true},
|
||||
},
|
||||
},
|
||||
{
|
||||
search: "dir type:largerThan=1",
|
||||
scope: "/",
|
||||
expectedResult: []string{"firstDir/thisIsDir/"},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"firstDir/thisIsDir/": {"dir": true},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -146,18 +174,17 @@ func TestSearchIndexes(t *testing.T) {
|
||||
"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": {"video": true, "dir": false},
|
||||
"new/test/video.mp4": {"video": true, "dir": false},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.search, func(t *testing.T) {
|
||||
actualResult, actualTypes := index.Search(tt.search, tt.scope, "")
|
||||
assert.Equal(t, tt.expectedResult, actualResult)
|
||||
if !reflect.DeepEqual(tt.expectedTypes, actualTypes) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", actualTypes, tt.expectedTypes)
|
||||
}
|
||||
assert.Equal(t, tt.expectedTypes, actualTypes)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -186,6 +213,7 @@ func Test_scopedPathNameFilter(t *testing.T) {
|
||||
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, tt.args.isDir); got != tt.want {
|
||||
@@ -194,103 +222,3 @@ func Test_scopedPathNameFilter(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isDoc(t *testing.T) {
|
||||
type args struct {
|
||||
extension string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := isDoc(tt.args.extension); got != tt.want {
|
||||
t.Errorf("isDoc() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getFileSize(t *testing.T) {
|
||||
type args struct {
|
||||
filepath string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int64
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := getFileSize(tt.args.filepath); got != tt.want {
|
||||
t.Errorf("getFileSize() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isArchive(t *testing.T) {
|
||||
type args struct {
|
||||
extension string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := isArchive(tt.args.extension); got != tt.want {
|
||||
t.Errorf("isArchive() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getLastPathComponent(t *testing.T) {
|
||||
type args struct {
|
||||
path string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := getLastPathComponent(tt.args.path); got != tt.want {
|
||||
t.Errorf("getLastPathComponent() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_generateRandomHash(t *testing.T) {
|
||||
type args struct {
|
||||
length int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := generateRandomHash(tt.args.length); got != tt.want {
|
||||
t.Errorf("generateRandomHash() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+25
-77
@@ -1,57 +1,32 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
)
|
||||
|
||||
// GetFileMetadata retrieves the FileInfo from the specified directory in the index.
|
||||
func (si *Index) GetFileMetadata(adjustedPath string) (FileInfo, bool) {
|
||||
si.mu.RLock()
|
||||
dir, exists := si.Directories[adjustedPath]
|
||||
si.mu.RUnlock()
|
||||
if exists {
|
||||
// Initialize the Metadata map if it is nil
|
||||
if dir.Metadata == nil {
|
||||
dir.Metadata = make(map[string]FileInfo)
|
||||
si.SetDirectoryInfo(adjustedPath, dir)
|
||||
return FileInfo{}, false
|
||||
} else {
|
||||
return dir.Metadata[adjustedPath], true
|
||||
}
|
||||
}
|
||||
return FileInfo{}, false
|
||||
}
|
||||
|
||||
// UpdateFileMetadata updates the FileInfo for the specified directory in the index.
|
||||
func (si *Index) UpdateFileMetadata(adjustedPath string, info FileInfo) bool {
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
dir, exists := si.Directories[adjustedPath]
|
||||
if !exists {
|
||||
// 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
|
||||
si.Directories[adjustedPath] = FileInfo{}
|
||||
}
|
||||
return si.SetFileMetadata(adjustedPath, info)
|
||||
return si.SetFileMetadata(adjustedPath, dir)
|
||||
}
|
||||
|
||||
// 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
|
||||
si.Directories[adjustedPath] = info
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -60,34 +35,40 @@ func (si *Index) GetMetadataInfo(adjustedPath string) (FileInfo, bool) {
|
||||
si.mu.RLock()
|
||||
dir, exists := si.Directories[adjustedPath]
|
||||
si.mu.RUnlock()
|
||||
if exists {
|
||||
// Initialize the Metadata map if it is nil
|
||||
if dir.Metadata == nil {
|
||||
dir.Metadata = make(map[string]FileInfo)
|
||||
si.SetDirectoryInfo(adjustedPath, dir)
|
||||
}
|
||||
info, metadataExists := dir.Metadata[adjustedPath]
|
||||
return info, metadataExists
|
||||
if !exists {
|
||||
return dir, exists
|
||||
}
|
||||
return FileInfo{}, false
|
||||
// remove recursive items, we only want this directories direct files
|
||||
cleanedItems := []ReducedItem{}
|
||||
for _, item := range dir.Items {
|
||||
cleanedItems = append(cleanedItems, ReducedItem{
|
||||
Name: item.Name,
|
||||
Size: item.Size,
|
||||
IsDir: item.IsDir,
|
||||
ModTime: item.ModTime,
|
||||
Type: item.Type,
|
||||
})
|
||||
}
|
||||
dir.Items = nil
|
||||
dir.ReducedItems = cleanedItems
|
||||
realPath, _, _ := GetRealPath(adjustedPath)
|
||||
dir.Path = realPath
|
||||
return dir, exists
|
||||
}
|
||||
|
||||
// SetDirectoryInfo sets the directory information in the index.
|
||||
func (si *Index) SetDirectoryInfo(adjustedPath string, dir Directory) {
|
||||
func (si *Index) SetDirectoryInfo(adjustedPath string, dir FileInfo) {
|
||||
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) {
|
||||
func (si *Index) GetDirectoryInfo(adjustedPath string) (FileInfo, bool) {
|
||||
si.mu.RLock()
|
||||
dir, exists := si.Directories[adjustedPath]
|
||||
si.mu.RUnlock()
|
||||
if exists {
|
||||
return dir, true
|
||||
}
|
||||
return Directory{}, false
|
||||
return dir, exists
|
||||
}
|
||||
|
||||
func (si *Index) RemoveDirectory(path string) {
|
||||
@@ -127,7 +108,7 @@ func GetIndex(root string) *Index {
|
||||
}
|
||||
newIndex := &Index{
|
||||
Root: rootPath,
|
||||
Directories: make(map[string]Directory), // Initialize the map
|
||||
Directories: map[string]FileInfo{},
|
||||
NumDirs: 0,
|
||||
NumFiles: 0,
|
||||
inProgress: false,
|
||||
@@ -137,36 +118,3 @@ func GetIndex(root string) *Index {
|
||||
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,262 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var testIndex Index
|
||||
|
||||
// Test for GetFileMetadata// Test for GetFileMetadata
|
||||
func TestGetFileMetadataSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
adjustedPath string
|
||||
expectedName string
|
||||
expectedSize int64
|
||||
}{
|
||||
{
|
||||
name: "testpath exists",
|
||||
adjustedPath: "/testpath",
|
||||
expectedName: "testfile.txt",
|
||||
expectedSize: 100,
|
||||
},
|
||||
{
|
||||
name: "testpath exists",
|
||||
adjustedPath: "/testpath",
|
||||
expectedName: "directory",
|
||||
expectedSize: 100,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fileInfo, _ := testIndex.GetMetadataInfo(tt.adjustedPath)
|
||||
// Iterate over fileInfo.Items to look for expectedName
|
||||
for _, item := range fileInfo.ReducedItems {
|
||||
// Assert the existence and the name
|
||||
if item.Name == tt.expectedName {
|
||||
assert.Equal(t, tt.expectedSize, item.Size)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test for GetFileMetadata// Test for GetFileMetadata
|
||||
func TestGetFileMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
adjustedPath string
|
||||
expectedName string
|
||||
expectedExists bool
|
||||
}{
|
||||
{
|
||||
name: "testpath exists",
|
||||
adjustedPath: "/testpath",
|
||||
expectedName: "testfile.txt",
|
||||
expectedExists: true,
|
||||
},
|
||||
{
|
||||
name: "testpath not exists",
|
||||
adjustedPath: "/testpath",
|
||||
expectedName: "nonexistent.txt",
|
||||
expectedExists: false,
|
||||
},
|
||||
{
|
||||
name: "File exists in /anotherpath",
|
||||
adjustedPath: "/anotherpath",
|
||||
expectedName: "afile.txt",
|
||||
expectedExists: true,
|
||||
},
|
||||
{
|
||||
name: "File does not exist in /anotherpath",
|
||||
adjustedPath: "/anotherpath",
|
||||
expectedName: "nonexistentfile.txt",
|
||||
expectedExists: false,
|
||||
},
|
||||
{
|
||||
name: "Directory does not exist",
|
||||
adjustedPath: "/nonexistentpath",
|
||||
expectedName: "",
|
||||
expectedExists: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fileInfo, _ := testIndex.GetMetadataInfo(tt.adjustedPath)
|
||||
found := false
|
||||
// Iterate over fileInfo.Items to look for expectedName
|
||||
for _, item := range fileInfo.ReducedItems {
|
||||
// Assert the existence and the name
|
||||
if item.Name == tt.expectedName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Equal(t, tt.expectedExists, found)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test for UpdateFileMetadata
|
||||
func TestUpdateFileMetadata(t *testing.T) {
|
||||
index := &Index{
|
||||
Directories: map[string]FileInfo{
|
||||
"/testpath": {
|
||||
Path: "/testpath",
|
||||
Name: "testpath",
|
||||
IsDir: true,
|
||||
ReducedItems: []ReducedItem{
|
||||
{Name: "testfile.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.ReducedItems[0].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]FileInfo{
|
||||
"/testpath": {
|
||||
Path: "/testpath",
|
||||
Name: "testpath",
|
||||
IsDir: true,
|
||||
Items: []*FileInfo{
|
||||
{Name: "testfile.txt"},
|
||||
{Name: "anotherfile.txt"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
dir := FileInfo{
|
||||
Path: "/newPath",
|
||||
Name: "newPath",
|
||||
IsDir: true,
|
||||
Items: []*FileInfo{
|
||||
{Name: "testfile.txt"},
|
||||
},
|
||||
}
|
||||
index.SetDirectoryInfo("/newPath", dir)
|
||||
storedDir, exists := index.Directories["/newPath"]
|
||||
if !exists || storedDir.Items[0].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.Items[0].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]FileInfo{
|
||||
"/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{
|
||||
Root: "/",
|
||||
NumFiles: 10,
|
||||
NumDirs: 5,
|
||||
inProgress: false,
|
||||
Directories: map[string]FileInfo{
|
||||
"/testpath": {
|
||||
Path: "/testpath",
|
||||
Name: "testpath",
|
||||
IsDir: true,
|
||||
NumDirs: 1,
|
||||
NumFiles: 2,
|
||||
Items: []*FileInfo{
|
||||
{Name: "testfile.txt", Size: 100},
|
||||
{Name: "anotherfile.txt", Size: 100},
|
||||
},
|
||||
},
|
||||
"/anotherpath": {
|
||||
Path: "/anotherpath",
|
||||
Name: "anotherpath",
|
||||
IsDir: true,
|
||||
NumDirs: 1,
|
||||
NumFiles: 1,
|
||||
Items: []*FileInfo{
|
||||
{Name: "directory", IsDir: true, Size: 100},
|
||||
{Name: "afile.txt", Size: 100},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
+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": {}
|
||||
}
|
||||
+10
-10
@@ -1,13 +1,13 @@
|
||||
module github.com/gtsteffaniak/filebrowser
|
||||
|
||||
go 1.22.0
|
||||
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.3
|
||||
github.com/goccy/go-yaml v1.12.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
@@ -15,13 +15,13 @@ require (
|
||||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
github.com/spf13/afero v1.11.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
|
||||
golang.org/x/crypto v0.24.0
|
||||
golang.org/x/image v0.17.0
|
||||
golang.org/x/text v0.16.0
|
||||
golang.org/x/crypto v0.26.0
|
||||
golang.org/x/image v0.19.0
|
||||
golang.org/x/text v0.17.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -47,10 +47,10 @@ require (
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.3.10 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // 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
|
||||
)
|
||||
+21
-21
@@ -7,7 +7,7 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
|
||||
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
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=
|
||||
@@ -51,8 +51,8 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I=
|
||||
github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
|
||||
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=
|
||||
@@ -116,8 +116,8 @@ github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/i
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
@@ -136,14 +136,14 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMx
|
||||
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.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||
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.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
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.17.0 h1:nTRVVdajgB8zCMZVsViyzhnMKPwYeroEERRC64JuLco=
|
||||
golang.org/x/image v0.17.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
|
||||
golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-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=
|
||||
@@ -151,10 +151,10 @@ golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -167,18 +167,18 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
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.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
|
||||
Binary file not shown.
@@ -2,7 +2,6 @@ package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -127,11 +126,10 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
|
||||
return http.StatusBadRequest, nil
|
||||
}
|
||||
|
||||
user := &users.User{
|
||||
Username: info.Username,
|
||||
Password: info.Password,
|
||||
}
|
||||
settings.Config.UserDefaults.Apply(user)
|
||||
user := users.ApplyDefaults(users.User{})
|
||||
user.Username = info.Username
|
||||
user.Password = info.Password
|
||||
|
||||
userHome, err := d.settings.MakeUserDir(user.Username, user.Scope, d.server.Root)
|
||||
if err != nil {
|
||||
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
||||
@@ -139,8 +137,7 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
|
||||
}
|
||||
user.Scope = userHome
|
||||
log.Printf("new user: %s, home dir: [%s].", user.Username, userHome)
|
||||
settings.Config.UserDefaults.Apply(user)
|
||||
err = d.store.Users.Save(user)
|
||||
err = d.store.Users.Save(&user)
|
||||
if err == errors.ErrExist {
|
||||
return http.StatusConflict, err
|
||||
} else if err != nil {
|
||||
@@ -157,7 +154,6 @@ var renewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data
|
||||
func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (int, error) {
|
||||
duration, err := time.ParseDuration(settings.Config.Auth.TokenExpirationTime)
|
||||
if err != nil {
|
||||
fmt.Println("Error parsing duration:", err)
|
||||
duration = time.Hour * 2
|
||||
}
|
||||
claims := &authToken{
|
||||
|
||||
+12
-3
@@ -15,11 +15,20 @@ type modifyRequest struct {
|
||||
Which []string `json:"which"` // Answer to: which fields?
|
||||
}
|
||||
|
||||
var (
|
||||
store *storage.Storage
|
||||
server *settings.Server
|
||||
fileCache FileCache
|
||||
)
|
||||
|
||||
func SetupEnv(storage *storage.Storage, s *settings.Server, cache FileCache) {
|
||||
store = storage
|
||||
server = s
|
||||
fileCache = cache
|
||||
}
|
||||
|
||||
func NewHandler(
|
||||
imgSvc ImgService,
|
||||
fileCache FileCache,
|
||||
store *storage.Storage,
|
||||
server *settings.Server,
|
||||
assetsFs fs.FS,
|
||||
) (http.Handler, error) {
|
||||
server.Clean()
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
@@ -46,7 +47,6 @@ func previewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, re
|
||||
}
|
||||
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
Path: "/" + vars["path"],
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: true,
|
||||
@@ -110,7 +110,7 @@ func handleImagePreview(
|
||||
|
||||
func createPreview(imgSvc ImgService, fileCache FileCache,
|
||||
file *files.FileInfo, previewSize PreviewSize) ([]byte, error) {
|
||||
fd, err := file.Fs.Open(file.Path)
|
||||
fd, err := os.Open(file.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+20
-22
@@ -2,13 +2,12 @@ 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"
|
||||
@@ -25,28 +24,22 @@ var withHashFile = func(fn handleFunc) handleFunc {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
if link.Hash != "" {
|
||||
status, err := authenticateShareRequest(r, link)
|
||||
if status != 0 || err != nil {
|
||||
var status int
|
||||
status, err = authenticateShareRequest(r, link)
|
||||
if err != nil || status != 0 {
|
||||
return status, err
|
||||
}
|
||||
}
|
||||
d.user = &users.PublicUser
|
||||
if path == "/" {
|
||||
path = link.Path
|
||||
} else if strings.HasPrefix("/"+path, link.Path) {
|
||||
path = "/" + path
|
||||
} else {
|
||||
path = link.Path + "/" + path
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, link.Path, path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
sharePath := settings.Config.Server.Root + path
|
||||
lastComponent := filepath.Base(sharePath)
|
||||
basePath := filepath.Dir(sharePath)
|
||||
fsPath := afero.NewBasePathFs(afero.NewOsFs(), basePath)
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Fs: fsPath,
|
||||
Path: lastComponent,
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
Expand: true,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
Checker: d,
|
||||
Token: link.Token,
|
||||
@@ -63,14 +56,15 @@ func ifPathWithName(r *http.Request) (id, filePath string) {
|
||||
pathElements := strings.Split(r.URL.Path, "/")
|
||||
id = pathElements[0]
|
||||
allButFirst := path.Join(pathElements[1:]...)
|
||||
if len(pathElements) == 1 {
|
||||
allButFirst = "/"
|
||||
}
|
||||
return id, allButFirst
|
||||
}
|
||||
|
||||
var publicShareHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
file := 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 {
|
||||
return renderJSON(w, r, file)
|
||||
@@ -86,7 +80,11 @@ var publicUserGetHandler = func(w http.ResponseWriter, r *http.Request, d *data)
|
||||
}
|
||||
|
||||
var publicDlHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/share"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/storage/bolt"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
@@ -74,8 +74,13 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
|
||||
t.Errorf("failed to close db: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
storage, err := bolt.NewStorage(db)
|
||||
authStore, userStore, shareStore, settingsStore, err := bolt.NewStorage(db)
|
||||
storage := &storage.Storage{
|
||||
Auth: authStore,
|
||||
Users: userStore,
|
||||
Share: shareStore,
|
||||
Settings: settingsStore,
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get storage: %v", err)
|
||||
}
|
||||
@@ -92,7 +97,6 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
|
||||
|
||||
storage.Users = &customFSUser{
|
||||
Store: storage.Users,
|
||||
fs: &afero.MemMapFs{},
|
||||
}
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
@@ -122,7 +126,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) {
|
||||
@@ -130,7 +133,5 @@ func (cu *customFSUser) Get(baseScope string, id interface{}) (*users.User, erro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.Fs = cu.fs
|
||||
|
||||
return user, nil
|
||||
}
|
||||
+10
-6
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
gopath "path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -80,10 +81,13 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
|
||||
if !d.user.Perm.Download {
|
||||
return http.StatusAccepted, nil
|
||||
}
|
||||
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
Path: r.URL.Path,
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
@@ -108,7 +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
|
||||
}
|
||||
@@ -117,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
|
||||
}
|
||||
@@ -198,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
|
||||
}
|
||||
|
||||
+74
-130
@@ -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,9 +18,13 @@ import (
|
||||
)
|
||||
|
||||
var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
Path: r.URL.Path,
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: true,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
@@ -32,19 +34,16 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
if file.IsDir {
|
||||
file.Listing.Sorting = d.user.Sorting
|
||||
return renderJSON(w, r, file)
|
||||
}
|
||||
if checksum := r.URL.Query().Get("checksum"); checksum != "" {
|
||||
err := file.Checksum(checksum)
|
||||
if err == errors.ErrInvalidOption {
|
||||
return http.StatusBadRequest, nil
|
||||
} else if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
if !file.IsDir {
|
||||
if checksum := r.URL.Query().Get("checksum"); checksum != "" {
|
||||
err := file.Checksum(checksum)
|
||||
if err == errors.ErrInvalidOption {
|
||||
return http.StatusBadRequest, nil
|
||||
} else if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return renderJSON(w, r, file)
|
||||
})
|
||||
|
||||
@@ -53,15 +52,19 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
|
||||
if r.URL.Path == "/" || !d.user.Perm.Delete {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
Path: r.URL.Path,
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
fileOpts := files.FileOptions{
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
Checker: d,
|
||||
})
|
||||
}
|
||||
file, err := files.FileInfoFaster(fileOpts)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
@@ -72,14 +75,10 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
err = d.RunHook(func() error {
|
||||
return d.user.Fs.RemoveAll(r.URL.Path)
|
||||
}, "delete", r.URL.Path, "", d.user)
|
||||
|
||||
err = files.DeleteFiles(realPath, fileOpts)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
})
|
||||
}
|
||||
@@ -89,21 +88,27 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
|
||||
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
// Directories creation on POST.
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
err := d.user.Fs.MkdirAll(r.URL.Path, 0775) //nolint:gomnd
|
||||
return errToStatus(err), err
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
Path: r.URL.Path,
|
||||
fileOpts := files.FileOptions{
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
Checker: d,
|
||||
})
|
||||
}
|
||||
// Directories creation on POST.
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
err = files.WriteDirectory(fileOpts) // Assign to the existing `err` variable
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
file, err := files.FileInfoFaster(fileOpts)
|
||||
if err == nil {
|
||||
if r.URL.Query().Get("override") != "true" {
|
||||
return http.StatusConflict, nil
|
||||
@@ -119,22 +124,7 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
}
|
||||
|
||||
err = d.RunHook(func() error {
|
||||
info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body)
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
|
||||
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
|
||||
w.Header().Set("ETag", etag)
|
||||
return nil
|
||||
}, "upload", r.URL.Path, "", d.user)
|
||||
|
||||
if err != nil {
|
||||
_ = d.user.Fs.RemoveAll(r.URL.Path)
|
||||
}
|
||||
|
||||
err = files.WriteFile(fileOpts, r.Body)
|
||||
return errToStatus(err), err
|
||||
})
|
||||
}
|
||||
@@ -149,28 +139,23 @@ var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
exists, err := afero.Exists(d.user.Fs, r.URL.Path)
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
if !exists {
|
||||
return http.StatusNotFound, nil
|
||||
fileOpts := files.FileOptions{
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: d.server.TypeDetectionByHeader,
|
||||
Checker: d,
|
||||
}
|
||||
|
||||
err = d.RunHook(func() error {
|
||||
info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body)
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
|
||||
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
|
||||
w.Header().Set("ETag", etag)
|
||||
return nil
|
||||
}, "save", r.URL.Path, "", d.user)
|
||||
|
||||
err = files.WriteFile(fileOpts, r.Body)
|
||||
return errToStatus(err), err
|
||||
})
|
||||
|
||||
// TODO fix and verify this function still works in tests
|
||||
func resourcePatchHandler(fileCache FileCache) handleFunc {
|
||||
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
src := r.URL.Path
|
||||
@@ -186,28 +171,20 @@ func resourcePatchHandler(fileCache FileCache) handleFunc {
|
||||
if dst == "/" || src == "/" {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
err = checkParent(src, dst)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
override := r.URL.Query().Get("override") == "true"
|
||||
rename := r.URL.Query().Get("rename") == "true"
|
||||
if !override && !rename {
|
||||
if _, err = d.user.Fs.Stat(dst); err == nil {
|
||||
if _, err = os.Stat(dst); err == nil {
|
||||
return http.StatusConflict, nil
|
||||
}
|
||||
}
|
||||
if rename {
|
||||
dst = addVersionSuffix(dst, d.user.Fs)
|
||||
dst = addVersionSuffix(dst)
|
||||
}
|
||||
|
||||
// Permission for overwriting the file
|
||||
if override && !d.user.Perm.Modify {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
err = d.RunHook(func() error {
|
||||
return patchAction(r.Context(), action, src, dst, d, fileCache)
|
||||
}, action, src, dst, d.user)
|
||||
@@ -216,65 +193,22 @@ func resourcePatchHandler(fileCache FileCache) handleFunc {
|
||||
})
|
||||
}
|
||||
|
||||
func checkParent(src, dst string) error {
|
||||
rel, err := filepath.Rel(src, dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rel = filepath.ToSlash(rel)
|
||||
if !strings.HasPrefix(rel, "../") && rel != ".." && rel != "." {
|
||||
return errors.ErrSourceIsParent
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addVersionSuffix(source string, fs afero.Fs) string {
|
||||
func addVersionSuffix(source string) string {
|
||||
counter := 1
|
||||
dir, name := path.Split(source)
|
||||
ext := filepath.Ext(name)
|
||||
base := strings.TrimSuffix(name, ext)
|
||||
|
||||
for {
|
||||
if _, err := fs.Stat(source); err != nil {
|
||||
if _, err := os.Stat(source); err != nil {
|
||||
break
|
||||
}
|
||||
renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext)
|
||||
source = path.Join(dir, renamed)
|
||||
counter++
|
||||
}
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
func writeFile(fs afero.Fs, dst string, in io.Reader) (os.FileInfo, error) {
|
||||
dir, _ := path.Split(dst)
|
||||
err := fs.MkdirAll(dir, 0775) //nolint:gomnd
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) //nolint:gomnd
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Gets the info about the file.
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func delThumbs(ctx context.Context, fileCache FileCache, file *files.FileInfo) error {
|
||||
for _, previewSizeName := range PreviewSizeNames() {
|
||||
size, _ := ParsePreviewSize(previewSizeName)
|
||||
@@ -294,17 +228,24 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
|
||||
return errors.ErrPermissionDenied
|
||||
}
|
||||
|
||||
return fileutils.Copy(d.user.Fs, src, dst)
|
||||
return fileutils.Copy(src, dst)
|
||||
case "rename":
|
||||
if !d.user.Perm.Rename {
|
||||
return errors.ErrPermissionDenied
|
||||
}
|
||||
src = path.Clean("/" + src)
|
||||
dst = path.Clean("/" + dst)
|
||||
|
||||
realDest, _, err := files.GetRealPath(d.user.Scope, dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
realSrc, isDir, err := files.GetRealPath(d.user.Scope, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
Path: src,
|
||||
Path: realSrc,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: false,
|
||||
@@ -320,7 +261,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
|
||||
return err
|
||||
}
|
||||
|
||||
return fileutils.MoveFile(d.user.Fs, src, dst)
|
||||
return fileutils.MoveFile(realSrc, realDest)
|
||||
default:
|
||||
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
|
||||
}
|
||||
@@ -332,9 +273,13 @@ type DiskUsageResponse struct {
|
||||
}
|
||||
|
||||
var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
file, err := files.FileInfoFaster(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
Path: r.URL.Path,
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: false,
|
||||
@@ -350,7 +295,6 @@ var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (
|
||||
Used: 0,
|
||||
})
|
||||
}
|
||||
|
||||
usage, err := disk.UsageWithContext(r.Context(), fPath)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
|
||||
@@ -15,20 +15,17 @@ type settingsData struct {
|
||||
Defaults settings.UserDefaults `json:"defaults"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
Frontend settings.Frontend `json:"frontend"`
|
||||
Shell []string `json:"shell"`
|
||||
Commands map[string][]string `json:"commands"`
|
||||
}
|
||||
|
||||
var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
data := &settingsData{
|
||||
Signup: settings.Config.Auth.Signup,
|
||||
CreateUserDir: settings.Config.Server.CreateUserDir,
|
||||
UserHomeBasePath: settings.Config.Server.UserHomeBasePath,
|
||||
Signup: d.settings.Auth.Signup,
|
||||
CreateUserDir: d.settings.Server.CreateUserDir,
|
||||
UserHomeBasePath: d.settings.Server.UserHomeBasePath,
|
||||
Defaults: d.settings.UserDefaults,
|
||||
Rules: d.settings.Rules,
|
||||
Frontend: d.settings.Frontend,
|
||||
Shell: d.settings.Shell,
|
||||
Commands: d.settings.Commands,
|
||||
}
|
||||
|
||||
return renderJSON(w, r, data)
|
||||
@@ -46,8 +43,7 @@ var settingsPutHandler = withAdmin(func(w http.ResponseWriter, r *http.Request,
|
||||
d.settings.UserDefaults = req.Defaults
|
||||
d.settings.Rules = req.Rules
|
||||
d.settings.Frontend = req.Frontend
|
||||
d.settings.Shell = req.Shell
|
||||
d.settings.Commands = req.Commands
|
||||
d.settings.Auth.Signup = req.Signup
|
||||
err = d.store.Settings.Save(d.settings)
|
||||
return errToStatus(err), err
|
||||
})
|
||||
@@ -34,6 +34,7 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
|
||||
"Color": d.settings.Frontend.Color,
|
||||
"BaseURL": d.server.BaseURL,
|
||||
"Version": version.Version,
|
||||
"CommitSHA": version.CommitSHA,
|
||||
"StaticURL": path.Join(d.server.BaseURL, "/static"),
|
||||
"Signup": settings.Config.Auth.Signup,
|
||||
"NoAuth": d.settings.Auth.Method == "noauth",
|
||||
@@ -65,7 +66,10 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
auther := raw.(*auth.JSONAuth)
|
||||
auther, ok := raw.(*auth.JSONAuth)
|
||||
if !ok {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type *auth.JSONAuth")
|
||||
}
|
||||
|
||||
if auther.ReCaptcha != nil {
|
||||
data["ReCaptcha"] = auther.ReCaptcha.Key != "" && auther.ReCaptcha.Secret != ""
|
||||
@@ -104,7 +108,7 @@ func getStaticHandlers(store *storage.Storage, server *settings.Server, assetsFs
|
||||
}
|
||||
|
||||
w.Header().Set("x-xss-protection", "1; mode=block")
|
||||
return handleWithStaticData(w, r, d, assetsFs, "index.html", "text/html; charset=utf-8")
|
||||
return handleWithStaticData(w, r, d, assetsFs, "public/index.html", "text/html; charset=utf-8")
|
||||
}, "", store, server)
|
||||
|
||||
static = handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
|
||||
+22
-29
@@ -2,8 +2,8 @@ package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
@@ -128,15 +130,7 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
|
||||
return http.StatusBadRequest, errors.ErrEmptyPassword
|
||||
}
|
||||
|
||||
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
|
||||
log.Printf("user: %s, home dir: [%s].", req.Data.Username, userHome)
|
||||
|
||||
err = d.store.Users.Save(req.Data)
|
||||
err = storage.CreateUser(*req.Data, req.Data.Perm.Admin)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -154,31 +148,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
|
||||
@@ -195,7 +189,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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+18
-23
@@ -3,10 +3,9 @@ package settings
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
var Config Settings
|
||||
@@ -19,22 +18,30 @@ func Initialize(configFile string) {
|
||||
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
||||
}
|
||||
Config.UserDefaults.Perm = Config.UserDefaults.Permissions
|
||||
Config.Server.Root = strings.TrimSuffix(Config.Server.Root, "/")
|
||||
// Convert relative path to absolute path
|
||||
realRoot, err := filepath.Abs(Config.Server.Root)
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting root path: %v", err)
|
||||
}
|
||||
_, err = os.Stat(realRoot)
|
||||
if err != nil {
|
||||
log.Fatalf("ERROR: Configured Root Path does not exist! %v", err)
|
||||
}
|
||||
Config.Server.Root = realRoot
|
||||
}
|
||||
|
||||
func loadConfigFile(configFile string) []byte {
|
||||
// Open and read the YAML file
|
||||
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)
|
||||
Config = setDefaults()
|
||||
return []byte{}
|
||||
log.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer yamlFile.Close()
|
||||
|
||||
stat, err := yamlFile.Stat()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting file information: %s", err.Error())
|
||||
log.Fatalf("error getting file information: %s", err.Error())
|
||||
}
|
||||
|
||||
yamlData := make([]byte, stat.Size())
|
||||
@@ -58,6 +65,7 @@ func setDefaults() Settings {
|
||||
Database: "database.db",
|
||||
Log: "stdout",
|
||||
Root: "/srv",
|
||||
Indexing: true,
|
||||
},
|
||||
Auth: Auth{
|
||||
TokenExpirationTime: "2h",
|
||||
@@ -70,13 +78,15 @@ func setDefaults() Settings {
|
||||
},
|
||||
},
|
||||
UserDefaults: UserDefaults{
|
||||
StickySidebar: true,
|
||||
Scope: ".",
|
||||
LockPassword: false,
|
||||
HideDotfiles: true,
|
||||
DarkMode: false,
|
||||
DisableSettings: false,
|
||||
ViewMode: "normal",
|
||||
Locale: "en",
|
||||
Permissions: users.Permissions{
|
||||
Permissions: Permissions{
|
||||
Create: false,
|
||||
Rename: false,
|
||||
Modify: false,
|
||||
@@ -88,18 +98,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
|
||||
|
||||
@@ -39,3 +39,15 @@ func GenerateKey() ([]byte, error) {
|
||||
func GetSettingsConfig(nameType string, Value string) string {
|
||||
return nameType + Value
|
||||
}
|
||||
|
||||
func AdminPerms() Permissions {
|
||||
return Permissions{
|
||||
Create: true,
|
||||
Rename: true,
|
||||
Modify: true,
|
||||
Delete: true,
|
||||
Share: true,
|
||||
Download: true,
|
||||
Admin: true,
|
||||
}
|
||||
}
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package settings
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/rules"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
@@ -54,6 +53,7 @@ type Server struct {
|
||||
Root string `json:"root"`
|
||||
UserHomeBasePath string `json:"userHomeBasePath"`
|
||||
CreateUserDir bool `json:"createUserDir"`
|
||||
Indexing bool `json:"indexing"`
|
||||
}
|
||||
|
||||
type Frontend struct {
|
||||
@@ -67,21 +67,34 @@ type Frontend struct {
|
||||
// UserDefaults is a type that holds the default values
|
||||
// for some fields on User.
|
||||
type UserDefaults struct {
|
||||
StickySidebar bool `json:"stickySidebar"`
|
||||
DarkMode bool `json:"darkMode"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
DisableSettings bool `json:"disableSettings,omitempty"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
GallerySize int `json:"gallerySize"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
Sorting struct {
|
||||
By string `json:"by"`
|
||||
Asc bool `json:"asc"`
|
||||
} `json:"sorting"`
|
||||
Perm users.Permissions `json:"perm"`
|
||||
Permissions users.Permissions `json:"permissions"`
|
||||
Commands []string `json:"commands,omitempty"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
Perm Permissions `json:"perm"`
|
||||
Permissions Permissions `json:"permissions"`
|
||||
Commands []string `json:"commands,omitempty"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
}
|
||||
|
||||
type Permissions struct {
|
||||
Admin bool `json:"admin"`
|
||||
Execute bool `json:"execute"`
|
||||
Create bool `json:"create"`
|
||||
Rename bool `json:"rename"`
|
||||
Modify bool `json:"modify"`
|
||||
Delete bool `json:"delete"`
|
||||
Share bool `json:"share"`
|
||||
Download bool `json:"download"`
|
||||
}
|
||||
@@ -28,5 +28,5 @@ func (s authBackend) Get(t string) (auth.Auther, error) {
|
||||
}
|
||||
|
||||
func (s authBackend) Save(a auth.Auther) error {
|
||||
return save(s.db, "auther", a)
|
||||
return Save(s.db, "auther", a)
|
||||
}
|
||||
@@ -6,26 +6,14 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/share"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
// NewStorage creates a storage.Storage based on Bolt DB.
|
||||
func NewStorage(db *storm.DB) (*storage.Storage, error) {
|
||||
func NewStorage(db *storm.DB) (*auth.Storage, *users.Storage, *share.Storage, *settings.Storage, error) {
|
||||
userStore := users.NewStorage(usersBackend{db: db})
|
||||
shareStore := share.NewStorage(shareBackend{db: db})
|
||||
settingsStore := settings.NewStorage(settingsBackend{db: db})
|
||||
authStore := auth.NewStorage(authBackend{db: db}, userStore)
|
||||
|
||||
err := save(db, "version", 2) //nolint:gomnd
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &storage.Storage{
|
||||
Auth: authStore,
|
||||
Users: userStore,
|
||||
Share: shareStore,
|
||||
Settings: settingsStore,
|
||||
}, nil
|
||||
return authStore, userStore, shareStore, settingsStore, nil
|
||||
}
|
||||
@@ -15,7 +15,7 @@ func (s settingsBackend) Get() (*settings.Settings, error) {
|
||||
}
|
||||
|
||||
func (s settingsBackend) Save(set *settings.Settings) error {
|
||||
return save(s.db, "settings", set)
|
||||
return Save(s.db, "settings", set)
|
||||
}
|
||||
|
||||
func (s settingsBackend) GetServer() (*settings.Server, error) {
|
||||
@@ -27,5 +27,5 @@ func (s settingsBackend) GetServer() (*settings.Server, error) {
|
||||
}
|
||||
|
||||
func (s settingsBackend) SaveServer(server *settings.Server) error {
|
||||
return save(s.db, "server", server)
|
||||
return Save(s.db, "server", server)
|
||||
}
|
||||
@@ -55,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() {
|
||||
@@ -63,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
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,6 @@ func get(db *storm.DB, name string, to interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func save(db *storm.DB, name string, from interface{}) error {
|
||||
func Save(db *storm.DB, name string, from interface{}) error {
|
||||
return db.Set("config", name, from)
|
||||
}
|
||||
@@ -1,10 +1,20 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/gtsteffaniak/filebrowser/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/share"
|
||||
"github.com/gtsteffaniak/filebrowser/storage/bolt"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
)
|
||||
|
||||
// Storage is a storage powered by a Backend which makes the necessary
|
||||
@@ -15,3 +25,112 @@ type Storage struct {
|
||||
Auth *auth.Storage
|
||||
Settings *settings.Storage
|
||||
}
|
||||
|
||||
var store *Storage
|
||||
|
||||
func InitializeDb(path string) (*Storage, bool, error) {
|
||||
exists, err := dbExists(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
db, err := storm.Open(path)
|
||||
|
||||
utils.CheckErr(fmt.Sprintf("storm.Open path %v", path), err)
|
||||
authStore, userStore, shareStore, settingsStore, err := bolt.NewStorage(db)
|
||||
if err != nil {
|
||||
return nil, exists, err
|
||||
}
|
||||
|
||||
err = bolt.Save(db, "version", 2) //nolint:gomnd
|
||||
if err != nil {
|
||||
return nil, exists, err
|
||||
}
|
||||
store = &Storage{
|
||||
Auth: authStore,
|
||||
Users: userStore,
|
||||
Share: shareStore,
|
||||
Settings: settingsStore,
|
||||
}
|
||||
if !exists {
|
||||
quickSetup(store)
|
||||
}
|
||||
|
||||
return store, exists, err
|
||||
}
|
||||
|
||||
func dbExists(path string) (bool, error) {
|
||||
stat, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return stat.Size() != 0, nil
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
d := filepath.Dir(path)
|
||||
_, err = os.Stat(d)
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet,gomnd
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
func quickSetup(store *Storage) {
|
||||
settings.Config.Auth.Key = utils.GenerateKey()
|
||||
if settings.Config.Auth.Method == "noauth" {
|
||||
err := store.Auth.Save(&auth.NoAuth{})
|
||||
utils.CheckErr("store.Auth.Save", err)
|
||||
} else {
|
||||
settings.Config.Auth.Method = "password"
|
||||
err := store.Auth.Save(&auth.JSONAuth{})
|
||||
utils.CheckErr("store.Auth.Save", err)
|
||||
}
|
||||
err := store.Settings.Save(&settings.Config)
|
||||
utils.CheckErr("store.Settings.Save", err)
|
||||
err = store.Settings.SaveServer(&settings.Config.Server)
|
||||
utils.CheckErr("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 = settings.AdminPerms()
|
||||
err = store.Users.Save(&user)
|
||||
utils.CheckErr("store.Users.Save", err)
|
||||
}
|
||||
|
||||
// create new user
|
||||
func CreateUser(userInfo users.User, asAdmin bool) error {
|
||||
// must have username or password to create
|
||||
if userInfo.Username == "" || userInfo.Password == "" {
|
||||
return errors.ErrInvalidRequestParams
|
||||
}
|
||||
newUser := users.ApplyDefaults(userInfo)
|
||||
if asAdmin {
|
||||
newUser.Perm = settings.AdminPerms()
|
||||
}
|
||||
// create new home directory
|
||||
userHome, err := settings.Config.MakeUserDir(newUser.Username, newUser.Scope, settings.Config.Server.Root)
|
||||
if err != nil {
|
||||
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
||||
return err
|
||||
}
|
||||
newUser.Scope = userHome
|
||||
log.Printf("user: %s, home dir: [%s].", newUser.Username, userHome)
|
||||
_, _, err = files.GetRealPath(settings.Config.Server.Root, newUser.Scope)
|
||||
if err != nil {
|
||||
log.Println("user path is not valid", newUser.Scope)
|
||||
return nil
|
||||
}
|
||||
err = store.Users.Save(&newUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -54,9 +53,6 @@ func (s *Storage) Get(baseScope string, id interface{}) (user *User, err error)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := user.Clean(baseScope); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
|
||||
@@ -66,23 +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
|
||||
}
|
||||
@@ -137,11 +122,6 @@ func (s *Storage) DeleteRule(userID string, ruleID string) error {
|
||||
|
||||
// Save saves the user in a storage.
|
||||
func (s *Storage) Save(user *User) error {
|
||||
log.Println("Saving new user:", user.Username)
|
||||
if err := user.Clean(""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.back.Save(user)
|
||||
}
|
||||
|
||||
|
||||
+37
-53
@@ -1,25 +1,12 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/rules"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
)
|
||||
|
||||
type Permissions struct {
|
||||
Admin bool `json:"admin"`
|
||||
Execute bool `json:"execute"`
|
||||
Create bool `json:"create"`
|
||||
Rename bool `json:"rename"`
|
||||
Modify bool `json:"modify"`
|
||||
Delete bool `json:"delete"`
|
||||
Share bool `json:"share"`
|
||||
Download bool `json:"download"`
|
||||
}
|
||||
|
||||
// SortingSettings represents the sorting settings.
|
||||
type Sorting struct {
|
||||
By string `json:"by"`
|
||||
@@ -28,23 +15,24 @@ type Sorting struct {
|
||||
|
||||
// User describes a user.
|
||||
type User struct {
|
||||
DarkMode bool `json:"darkMode"`
|
||||
DisableSettings bool `json:"disableSettings"`
|
||||
ID uint `storm:"id,increment" json:"id"`
|
||||
Username string `storm:"unique" json:"username"`
|
||||
Password string `json:"password"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Perm Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
Sorting Sorting `json:"sorting"`
|
||||
Fs afero.Fs `json:"-" yaml:"-"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
StickySidebar bool `json:"stickySidebar"`
|
||||
DarkMode bool `json:"darkMode"`
|
||||
DisableSettings bool `json:"disableSettings"`
|
||||
ID uint `storm:"id,increment" json:"id"`
|
||||
Username string `storm:"unique" json:"username"`
|
||||
Password string `json:"password"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Perm settings.Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
Sorting Sorting `json:"sorting"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
GallerySize int `json:"gallerySize"`
|
||||
}
|
||||
|
||||
var PublicUser = User{
|
||||
@@ -53,8 +41,7 @@ var PublicUser = User{
|
||||
Scope: "./",
|
||||
ViewMode: "normal",
|
||||
LockPassword: true,
|
||||
Fs: afero.NewMemMapFs(),
|
||||
Perm: Permissions{
|
||||
Perm: settings.Permissions{
|
||||
Create: false,
|
||||
Rename: false,
|
||||
Modify: false,
|
||||
@@ -70,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 {
|
||||
@@ -104,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
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
)
|
||||
|
||||
func CheckErr(source string, err error) {
|
||||
if err != nil {
|
||||
log.Fatalf("%s: %v", source, err)
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateKey() []byte {
|
||||
k, err := settings.GenerateKey()
|
||||
CheckErr("generateKey", err)
|
||||
return k
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package version
|
||||
|
||||
var (
|
||||
// Version is the current File Browser version.
|
||||
Version = "(0.2.5)"
|
||||
// CommitSHA is the commmit sha.
|
||||
CommitSHA = "(unknown)"
|
||||
// Dynamically updated during build via build args
|
||||
Version = "untracked"
|
||||
CommitSHA = "untracked"
|
||||
)
|
||||
@@ -11,6 +11,7 @@ server:
|
||||
CreateUserDir: false
|
||||
UserHomeBasePath: ""
|
||||
indexingInterval: 5
|
||||
indexing: true
|
||||
numImageProcessors: 4
|
||||
socket: ""
|
||||
tlsKey: ""
|
||||
@@ -34,9 +35,7 @@ auth:
|
||||
tokenExpirationTime: 2h
|
||||
header: ""
|
||||
method: json
|
||||
command: ""
|
||||
signup: false
|
||||
shell: ""
|
||||
frontend:
|
||||
name: ""
|
||||
disableExternal: false
|
||||
@@ -62,7 +61,6 @@ userDefaults:
|
||||
delete: true
|
||||
share: true
|
||||
download: true
|
||||
commands: []
|
||||
hideDotfiles: false
|
||||
dateFormat: false
|
||||
```
|
||||
@@ -113,6 +111,8 @@ userDefaults:
|
||||
|
||||
- `indexingInterval`: This is the time in minutes the system waits before checking for filesystem changes. Default: `5`
|
||||
|
||||
- `indexing`: This enables or disables indexing. (Note: search will not work without indexing) Default: `true`
|
||||
|
||||
- `numImageProcessors`: This is the number of image processors available. Default: `4`
|
||||
|
||||
- `socket`: This is the socket configuration.
|
||||
@@ -162,10 +162,6 @@ userDefaults:
|
||||
- `oath` - oath authentication
|
||||
- `noauth` - no authentication/login required.
|
||||
|
||||
- `command`: Deprecated: This is the authentication command.
|
||||
|
||||
- `shell`: This is the shell configuration.
|
||||
|
||||
- `Signup`: This boolean value indicates whether user signup is enabled on the login page. NOTE: Be mindful of `userDefaults` settings if enabled. Default: `false`
|
||||
|
||||
- `AdminUsername`: This is the username of the admin user. Default: `admin`
|
||||
@@ -224,7 +220,6 @@ userDefaults:
|
||||
|
||||
- `download`: This boolean value determines whether download permissions are granted.
|
||||
|
||||
- `commands`: Deprecated: This is a list of commands.
|
||||
|
||||
- `hideDotfiles`: This boolean value determines whether dotfiles are hidden. (`true` or `false`)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Contributing Guide
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Getting Started using FileBrowser Quantum
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Migration help
|
||||
|
||||
It is possible to use the same database as used by filebrowser/filebrowser,
|
||||
but you will need to follow the following process:
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Note: share links will not work and will need to be re-created after migration.
|
||||
|
||||
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 may not work
|
||||
the same way, but all user configuration should be available.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Planned Roadmap
|
||||
|
||||
upcoming 0.2.x releases:
|
||||
|
||||
- Replace http routes for gorilla/mux with stdlib
|
||||
- Theme configuration from settings
|
||||
- File syncronization improvements
|
||||
- more filetype previews
|
||||
|
||||
next major 0.3.0 release :
|
||||
|
||||
- multiple sources https://github.com/filebrowser/filebrowser/issues/2514
|
||||
- introduce jobs as replacement to runners.
|
||||
- Add Job status to the sidebar
|
||||
- index status.
|
||||
- Job status from users
|
||||
- upload status
|
||||
|
||||
Unplanned Future releases:
|
||||
- Add tools to sidebar
|
||||
- duplicate file detector.
|
||||
- bulk rename https://github.com/filebrowser/filebrowser/issues/2473
|
||||
- metrics tracker - user access, file access, download count, last login, etc
|
||||
- support minio, s3, and backblaze sources https://github.com/filebrowser/filebrowser/issues/2544
|
||||
@@ -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,3 +0,0 @@
|
||||
module.exports = {
|
||||
presets: ["@vue/app"],
|
||||
};
|
||||
Whitespace-only changes.
Symlink
+1
@@ -0,0 +1 @@
|
||||
frontend
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"name": "filebrowser-frontend",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
// vue 3 changes needed
|
||||
"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 ."
|
||||
},
|
||||
"dependencies": {
|
||||
"ace-builds": "^1.24.2",
|
||||
"clipboard": "^2.0.4",
|
||||
"css-vars-ponyfill": "^2.4.3",
|
||||
"file-loader": "^6.2.0", // UNNECESSARY IN VITE
|
||||
X"js-base64": "^2.5.1", // REPLACE WITH EQUIVALENT JS
|
||||
"lodash.clonedeep": "^4.5.0", // TOO OLD - REPLACE WITH JS
|
||||
"lodash.throttle": "^4.1.1", // TOO OLD - REPLACE WITH JS
|
||||
"material-icons": "^1.10.5",
|
||||
"moment": "^2.29.4", // REPLACE WITH EQUIVALENT JS
|
||||
"normalize.css": "^8.0.1", // REPLACE WITH EQUIVALENT JS
|
||||
"noty": "^3.2.0-beta", // REPLACE WITH EQUIVALENT JS
|
||||
X"pretty-bytes": "^6.0.0", // REPLACE WITH EQUIVALENT JS
|
||||
"qrcode.vue": "^1.7.0", // UPDATE TO LATEST for VUE3
|
||||
"utif": "^3.1.0", // SPIKE investigate replacement
|
||||
"vue": "^2.6.10", // UPDATE to vue 3
|
||||
"vue-async-computed": "^3.9.0", // REPLACE WITH EQUIVALENT JS
|
||||
"vue-i18n": "^8.15.3", // REMOVE
|
||||
"vue-lazyload": "^1.3.3", // REMOVE
|
||||
"vue-router": "^3.1.3", // UPDATE to vue 3 @vue4 https://www.npmjs.com/package/vue-router
|
||||
"vue-simple-progress": "^1.1.1", // REPLACE WITH EQUIVALENT JS
|
||||
"vuex": "^3.1.2", // SPIKE: HOW TO REMOVE
|
||||
"vuex-router-sync": "^5.0.0", // SPIKE: HOW TO REMOVE
|
||||
X"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-service": "^5.0.8", // REMOVE for VUE3
|
||||
"compression-webpack-plugin": "^10.0.0", // REPLACE VUE3
|
||||
"eslint": "^8.51.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"vue-template-compiler": "^2.6.10" // REPLACE VUE3
|
||||
}
|
||||
}
|
||||
Generated
-8423
File diff suppressed because it is too large.
Load diff
+30
-25
@@ -1,15 +1,22 @@
|
||||
{
|
||||
"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 src/",
|
||||
"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 ."
|
||||
"format": "prettier --write .",
|
||||
"test": "npx playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"ace-builds": "^1.24.2",
|
||||
@@ -17,25 +24,23 @@
|
||||
"css-vars-ponyfill": "^2.4.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"material-icons": "^1.10.5",
|
||||
"moment": "^2.29.4",
|
||||
"normalize.css": "^8.0.1",
|
||||
"noty": "^3.2.0-beta",
|
||||
"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"
|
||||
"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": "^9.4.0",
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"vue-template-compiler": "^2.6.17"
|
||||
"@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,content=false) {
|
||||
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}?content=${content}`, {});
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -84,5 +84,6 @@ export function getDownloadURL(share, inline = false) {
|
||||
if (share.path == undefined) {
|
||||
share.path = ""
|
||||
}
|
||||
return createURL("api/public/dl/" + share.hash + "/"+share.path, params, false);
|
||||
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;
|
||||
}
|
||||
}
|
||||
Loaded 100 of 197 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user