Compare commits
36
Commits
v0.3.5
...
v0.5.3-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f2051e5c0 | ||
|
|
3e491a86e3 | ||
|
|
084e0d87ad | ||
|
|
8abe8d79d8 | ||
|
|
cb9d96859d | ||
|
|
4cc831195e | ||
|
|
2f831e258c | ||
|
|
0b35eecfb9 | ||
|
|
8bac00134e | ||
|
|
c84a3b0d41 | ||
|
|
545a831c66 | ||
|
|
e269fc8c0a | ||
|
|
0bc789f9bd | ||
|
|
02482ab8d4 | ||
|
|
087c72507f | ||
|
|
7f9e6ac184 | ||
|
|
c168599c91 | ||
|
|
a30bfcf7d0 | ||
|
|
b273f44cac | ||
|
|
f9a31f08c3 | ||
|
|
09493392db | ||
|
|
7e68db9858 | ||
|
|
34f6bea461 | ||
|
|
1e35473998 | ||
|
|
bd4d108b58 | ||
|
|
288ea8f59b | ||
|
|
f18c388c88 | ||
|
|
e922a92aa6 | ||
|
|
237769fa9e | ||
|
|
9a33e7b379 | ||
|
|
cd3e111e39 | ||
|
|
9cc30f25e4 | ||
|
|
7ab400eb7f | ||
|
|
4a4eb7ce31 | ||
|
|
f864790175 | ||
|
|
6494ca1991 |
No files matched your search
@@ -6,7 +6,32 @@ on:
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
test_frontend:
|
||||
name: Test Playwright
|
||||
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
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i && npm run build
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go build -o filebrowser .
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./_docker/Dockerfile.playwright
|
||||
push: false
|
||||
push_latest_to_registry:
|
||||
needs: [ test_frontend ]
|
||||
name: Push latest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -14,9 +39,9 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Find latest tag
|
||||
- name: Find latest beta tag
|
||||
run: |
|
||||
echo "LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV
|
||||
echo "LATEST_TAG=$(git describe --tags $(git rev-list --tags="*-beta*" --max-count=1))" >> $GITHUB_ENV
|
||||
echo "latest tag is $LATEST_TAG"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
@@ -40,7 +65,7 @@ jobs:
|
||||
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
|
||||
file: ./_docker/Dockerfile
|
||||
push: true
|
||||
tags: 'gtstef/filebrowser:latest'
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
+29
-20
@@ -1,28 +1,37 @@
|
||||
name: pr-request
|
||||
name: PR Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "dev/v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "beta/v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "stable/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
|
||||
test_frontend:
|
||||
name: Test Playwright
|
||||
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
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i && npm run build
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go build -o filebrowser .
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./_docker/Dockerfile.playwright
|
||||
push: false
|
||||
push_pr_to_registry:
|
||||
name: Push PR
|
||||
runs-on: ubuntu-latest
|
||||
@@ -47,7 +56,7 @@ jobs:
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
file: ./_docker/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
name: dev tests
|
||||
name: regular tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "*"
|
||||
- "**"
|
||||
|
||||
jobs:
|
||||
test-backend:
|
||||
@@ -21,10 +21,10 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23.3'
|
||||
go-version: 'stable'
|
||||
- uses: golangci/golangci-lint-action@v5
|
||||
with:
|
||||
version: v1.60
|
||||
version: 'v1.64'
|
||||
working-directory: backend
|
||||
format-backend:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
name: beta release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "beta/v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
test_frontend:
|
||||
name: Test Playwright
|
||||
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
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i && npm run build
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go build -o filebrowser .
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./_docker/Dockerfile.playwright
|
||||
push: false
|
||||
create_release_tag:
|
||||
needs: [ test_frontend ]
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
run: |
|
||||
original_branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
|
||||
echo "branch_name=$transformed_branch" >> $GITHUB_OUTPUT
|
||||
tag_name=$(echo "$original_branch" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+')-beta
|
||||
echo "tag_name=$tag_name" >> $GITHUB_OUTPUT
|
||||
id: extract_branch
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
target_commitish: ${{ github.sha }}
|
||||
token: ${{ secrets.PAT }}
|
||||
tag_name: ${{ steps.extract_branch.outputs.tag_name }}
|
||||
prerelease: false # change this to false when stable gets released
|
||||
make_latest: true # change this to false when stable gets released
|
||||
draft: false
|
||||
generate_release_notes: true
|
||||
name: ${{ steps.extract_branch.outputs.tag_name }}
|
||||
|
||||
push_release_to_registry:
|
||||
needs: [ 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: 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:beta\/v/filebrowser:beta_v/')
|
||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||
- name: Build and push
|
||||
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: ./_docker/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -3,7 +3,7 @@ name: dev release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "dev_v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "dev/v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
run: |
|
||||
JSON="${{ steps.meta.outputs.tags }}"
|
||||
# Use jq to remove 'v' from the version field
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:dev\/v/filebrowser:dev_v/')
|
||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
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
|
||||
file: ./Dockerfile
|
||||
file: ./_docker/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -1,15 +1,40 @@
|
||||
name: version release
|
||||
name: stable release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "stable/v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create_release:
|
||||
test_frontend:
|
||||
name: Test Playwright
|
||||
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
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i && npm run build
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go build -o filebrowser .
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./_docker/Dockerfile.playwright
|
||||
push: false
|
||||
create_release_tag:
|
||||
needs: [ test_frontend ]
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -19,23 +44,28 @@ jobs:
|
||||
token: ${{ secrets.PAT }}
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
|
||||
run: |
|
||||
original_branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
|
||||
echo "branch_name=$transformed_branch" >> $GITHUB_OUTPUT
|
||||
tag_name=$(echo "$original_branch" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+')-stable
|
||||
echo "tag_name=$tag_name" >> $GITHUB_OUTPUT
|
||||
id: extract_branch
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22.2
|
||||
go-version: 'stable'
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
target_commitish: ${{ steps.extract_branch.outputs.branch }}
|
||||
target_commitish: ${{ github.sha }}
|
||||
token: ${{ secrets.PAT }}
|
||||
tag_name: ${{ steps.extract_branch.outputs.branch }}
|
||||
tag_name: ${{ steps.extract_branch.outputs.tag_name }}
|
||||
prerelease: false
|
||||
draft: false
|
||||
generate_release_notes: true
|
||||
name: ${{ steps.extract_branch.outputs.branch }}
|
||||
name: ${{ steps.extract_branch.outputs.tag_name }}
|
||||
|
||||
push_release_to_registry:
|
||||
needs: [ test_frontend ]
|
||||
name: Push release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -60,7 +90,7 @@ jobs:
|
||||
run: |
|
||||
JSON="${{ steps.meta.outputs.tags }}"
|
||||
# Use jq to remove 'v' from the version field
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:stable\/v/filebrowser:/')
|
||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -70,7 +100,7 @@ jobs:
|
||||
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
|
||||
file: ./_docker/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -1,9 +1,9 @@
|
||||
name: tag
|
||||
name: tag update
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
- "v[0-9]+.[0-9]+.[0-9]+*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
+8
-2
@@ -1,19 +1,25 @@
|
||||
*.db
|
||||
*.bak
|
||||
*.log
|
||||
*.mjs
|
||||
_old
|
||||
rice-box.go
|
||||
.idea/
|
||||
/filebrowser
|
||||
/filebrowser.exe
|
||||
/backend/backend
|
||||
/backend/filebrowser
|
||||
/backend/filebrowser.exe
|
||||
/backend/backend.exe
|
||||
/frontend/dist
|
||||
/frontend/pkg
|
||||
/frontend/test-results
|
||||
/frontend/loginAuth.json
|
||||
/frontend/package-lock.json
|
||||
/backend/vendor
|
||||
/backend/*.cov
|
||||
/backend/test_config.yaml
|
||||
/backend/srv
|
||||
/backend/http/dist
|
||||
/backend/http/embed/*
|
||||
|
||||
.DS_Store
|
||||
node_modules
|
||||
|
||||
+177
-1
@@ -2,12 +2,188 @@
|
||||
|
||||
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.5.3-beta
|
||||
|
||||
**New Features**
|
||||
- onlyoffice disable filetypes for user specified file types. https://github.com/gtsteffaniak/filebrowser/issues/346
|
||||
|
||||
**Notes**:
|
||||
- navbar/sidebar lightmode style tweaks.
|
||||
- any item that has utf formatted text will get editor.
|
||||
- tweaks to create options on context menu.
|
||||
- removed small delay on preview before detecting the file.
|
||||
|
||||
**BugFixes**:
|
||||
- fix `/files/` prefix loading issue https://github.com/gtsteffaniak/filebrowser/issues/362
|
||||
- fix special characters in filename issue https://github.com/gtsteffaniak/filebrowser/issues/357
|
||||
- fix drag and drop issue https://github.com/gtsteffaniak/filebrowser/issues/361
|
||||
- fix conflict issue with creating same file after deletion.
|
||||
- fix mimetype detection https://github.com/gtsteffaniak/filebrowser/issues/327
|
||||
- subtitles for videos https://github.com/gtsteffaniak/filebrowser/issues/358
|
||||
- supports caption sidecar files : ".vtt", ".srt", ".lrc", ".sbv", ".ass", ".ssa", ".sub", ".smi"
|
||||
- embedded subtitles not yet supported.
|
||||
|
||||
## v0.5.2-beta
|
||||
|
||||
**New Features**:
|
||||
- Markdown file preview https://github.com/gtsteffaniak/filebrowser/issues/343
|
||||
- Easy access download button https://github.com/gtsteffaniak/filebrowser/issues/341
|
||||
|
||||
**Notes**:
|
||||
- Adds message about what sharing means when creating a link.
|
||||
- api log duration is now always in milliseconds for consistency.
|
||||
- advanced index config option `fileEndsWith` is now respected.
|
||||
- Added Informative error for missing files for certificate load https://github.com/gtsteffaniak/filebrowser/issues/354
|
||||
|
||||
**BugFixes**:
|
||||
- onlyoffice close window missing files issue https://github.com/gtsteffaniak/filebrowser/issues/345
|
||||
- fixed download link inside file preview
|
||||
|
||||
## v0.5.1-beta
|
||||
|
||||
> Note: I changed the [config](https://github.com/gtsteffaniak/filebrowser/wiki/Configuration#example-auth-config) for password auth again... It was a mistake just to make it a boolean, so now you can provide options, going forward this allows for more.
|
||||
|
||||
**New Features**:
|
||||
- password length requirement config via `auth.methods.password.minLength` as a number of characters required.
|
||||
|
||||
**Bugfixes**:
|
||||
- NoAuth error message "resource not found"
|
||||
- CLI user configuration works and simplified see examples in the [Wiki](https://github.com/gtsteffaniak/filebrowser/wiki/CLI)
|
||||
|
||||
## v0.5.0-beta
|
||||
|
||||
> Note: This Beta release includes a configuration change: `auth.method` is now deprecated. This is done to allow multiple login methods at once. Auth methods are specified via `auth.methods` instead. see [example on the wiki](https://github.com/gtsteffaniak/filebrowser/wiki/Configuration#example-auth-config).
|
||||
|
||||
**New Features**:
|
||||
- Upload progress notification https://github.com/gtsteffaniak/filebrowser/issues/303
|
||||
- proxy auth auto create user when `auth.methods.proxy.createUser: true` while using proxy auth.
|
||||
|
||||
**Notes**:
|
||||
- Context menu positioning tweaks.
|
||||
- using /tmp cachedir is disabled by default, cache dir can be specified via `server.cacheDir: /tmp` to enable it. https://github.com/gtsteffaniak/filebrowser/issues/326
|
||||
|
||||
**Bugfixes**:
|
||||
- Gracefully shutdown to protect database. https://github.com/gtsteffaniak/filebrowser/issues/317
|
||||
- validates auth method provided before server startup.
|
||||
- fix sidebar disk space usage calculation. https://github.com/gtsteffaniak/filebrowser/issues/315
|
||||
- Fixed proxy auth header support (make sure your proxy and server are secure!). https://github.com/gtsteffaniak/filebrowser/issues/322
|
||||
|
||||
## v0.4.2-beta
|
||||
|
||||
**New Features**:
|
||||
- Hidden files changes
|
||||
- windows hidden file properties are respected -- when running on windows binary (not docker) with NTFS filesystem.
|
||||
- windows "system" files are considered hidden.
|
||||
- changed user property from `hideDotFiles` to `showHidden`. Defaults to false, so a user would need to must unhide hidden files if they want to view hidden files.
|
||||
|
||||
**Notes**:
|
||||
- cleaned up old and deprecated config.
|
||||
- removed unneeded "Global settings". All system configuration is done on config yaml, See configuration wiki for more help.
|
||||
|
||||
**Bugfixes**:
|
||||
- Another fix for memory https://github.com/gtsteffaniak/filebrowser/issues/298
|
||||
|
||||
## v0.4.1-beta
|
||||
|
||||
**New Features**:
|
||||
- right-click actions are available on search. https://github.com/gtsteffaniak/filebrowser/issues/273
|
||||
|
||||
**Notes**:
|
||||
- delete prompt now lists all items that will be affected by delete
|
||||
- Debug and logger output tweaks.
|
||||
|
||||
**Bugfixes**:
|
||||
- calculating checksums errors.
|
||||
- copy/move issues for some circumstances.
|
||||
- The previous position wasn't returned when closing a preview window https://github.com/gtsteffaniak/filebrowser/issues/298
|
||||
- fixed sources configuration mapping error (advanced `server.sources` config)
|
||||
|
||||
## v0.4.0-beta
|
||||
|
||||
**New Features**:
|
||||
- Better logging https://github.com/gtsteffaniak/filebrowser/issues/288
|
||||
- highly configurable
|
||||
- api logs include user
|
||||
- onlyOffice support for editing only office files (inspired from https://github.com/filebrowser/filebrowser/pull/2954)
|
||||
|
||||
**Notes**:
|
||||
- Breadcrumbs will only show on file listing (not on previews or editors)
|
||||
- Config file is now optional. It will run with default settings without one and throw a `[WARN ]` message.
|
||||
- Added more descriptions to swagger API
|
||||
|
||||
## v0.3.7-beta
|
||||
|
||||
**Notes**:
|
||||
- Adding windows builds back to automated process... will replace manually if they throw malicious defender warnings.
|
||||
- Adding playwright tests to all pr's against dev/beta/release branches.
|
||||
- These playwright tests should help keep release more reliably stable.
|
||||
|
||||
**Bugfixes**:
|
||||
- closing with the default bar issue.
|
||||
- tar.gz archive creation issue
|
||||
|
||||
## v0.3.6-beta
|
||||
|
||||
**New Features**:
|
||||
- Adds "externalUrl" server config https://github.com/gtsteffaniak/filebrowser/issues/272
|
||||
|
||||
**Notes**:
|
||||
- All views modes to show header bar for sorting.
|
||||
- other small style changes
|
||||
|
||||
**Bugfixes**:
|
||||
- select and info bug after sorting https://github.com/gtsteffaniak/filebrowser/issues/277
|
||||
- downloading from shares with public user
|
||||
- Ctrl and Shift key modifiers work on listing views as expected.
|
||||
- copy/move file/folder error and show errors https://github.com/gtsteffaniak/filebrowser/issues/278
|
||||
- file move/copy context fix.
|
||||
|
||||
## v0.3.5
|
||||
|
||||
**New Features**:
|
||||
- More indexing configuration options possible. However consider waiting on using this feature, because I will soon have a full onboarding experience in the UI to manage sources instead.
|
||||
- added config file options "sources" in the server config.
|
||||
- can enable/disable indexing a specified list of directories/files
|
||||
- can enable/disable indexing hidden files
|
||||
- prepped for multiple sources (not supported yet!)
|
||||
- Theme and Branding support (see updates to [configuration wiki](https://github.com/gtsteffaniak/filebrowser/wiki/Configuration) on how to use)
|
||||
- Automatically expire shares https://github.com/gtsteffaniak/filebrowser/issues/208
|
||||
|
||||
**Notes**:
|
||||
- MacOS application files (ending in ".app") were previously treated as folders, now they are treated as a single file.
|
||||
- No longer indexes "$RECYCLE.BIN" or "System Volume Information" directories.
|
||||
- Icon styling tweaked so all icons have a background.
|
||||
- Updated Login page styling.
|
||||
- Settings profile menu has been simplified, password changes happen in user management.
|
||||
- Improved windows compatibility and built on windows platform to fix false windows defender warning.
|
||||
- If no "root" location is provided in the server config, the default is the **current directory** (rather than `/srv` like before)
|
||||
|
||||
**Bugfixes**:
|
||||
- Fixed setting share expiration time would not work due to type conversion error.
|
||||
- More safari fixes related to text-selection.
|
||||
- Sort by name value sorting ignores the extension, only sorts by name https://github.com/gtsteffaniak/filebrowser/issues/230
|
||||
- Fixed manual language selection issue.
|
||||
- Fixed exact date time issue.
|
||||
|
||||
New login page:
|
||||
|
||||
<img width="300" alt="image" src="https://github.com/user-attachments/assets/d3ed359e-a969-4f6a-9f72-94d2b68aba49" />
|
||||
|
||||
|
||||
Example branding in sidebar:
|
||||
|
||||
<img width="500" alt="image2" src="https://github.com/user-attachments/assets/d8ee14ca-4495-4106-9d26-631a5937e134" />
|
||||
|
||||
Example user settings page:
|
||||
|
||||
<img width="500" alt="image3" src="https://github.com/user-attachments/assets/79757a11-669e-4597-bd3d-e41efd667a1e" />
|
||||
|
||||
## v0.3.4
|
||||
|
||||
**Bugfixes**:
|
||||
- Safari right-click actions.
|
||||
- Some small image viewer behavior
|
||||
- Progressive webapp "install to homescreen" fix.
|
||||
- Progressive webapp "install to homescreen" fix.
|
||||
|
||||
## v0.3.3
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
FROM golang:1.23-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/config.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
|
||||
@@ -1,61 +1,56 @@
|
||||
<p align="center">
|
||||
<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">
|
||||
</p>
|
||||
<h3 align="center">FileBrowser Quantum - A modern web-based file manager</h3>
|
||||
<p align="center">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/e4a47229-66f8-4838-9575-dd2413596688" title="Main Screenshot">
|
||||
</p>
|
||||
<div align="center">
|
||||
|
||||
> [!Note]
|
||||
> Starting with v0.3.3, configuration file mapping is different to support non-root user. Now, the default config file name is `config.yaml` and in docker the path is `/home/filebrowser/config.yaml` and `/home/filebrowser/<database_file>`. Please read the usage below to properly update your config to point the new config location. (open an issue for any help needed)
|
||||
[](https://goreportcard.com/report/github.com/gtsteffaniak/filebrowser/backend)
|
||||
[](https://www.codacy.com/gh/gtsteffaniak/filebrowser/dashboard)
|
||||
[](https://github.com/gtsteffaniak/filebrowser/releases)
|
||||
[](https://hub.docker.com/r/gtstef/filebrowser)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
[](https://github.com/gtsteffaniak/filebrowser/discussions/368)
|
||||
[](https://www.paypal.com/donate/?business=W5XKNXHJM2WPE&no_recurring=0¤cy_code=USD)
|
||||
|
||||
<img width="150" src="https://github.com/user-attachments/assets/59986a2a-f960-4536-aa35-4a9a7c98ad48" title="Logo">
|
||||
<h3>FileBrowser Quantum</h3>
|
||||
A modern web-based file manager
|
||||
<br/><br/>
|
||||
<img width="800" src="https://github.com/user-attachments/assets/c991fc69-a05b-4f34-b915-0d3cded887a7" title="Main Screenshot">
|
||||
</div>
|
||||
|
||||
> [!WARNING]
|
||||
> - There is no stable version yet. Always check release notes for bug fixes on functionality that may have been changed. If you notice any unexpected behavior -- please open an issue to have it fixed soon.
|
||||
> There is no stable version -- planned 2025.
|
||||
|
||||
FileBrowser Quantum is a fork of the file browser opensource project with the following changes:
|
||||
FileBrowser Quantum is a fork of the file browser open-source project with the following changes:
|
||||
|
||||
1. [x] Indexes files efficiently. (See [indexing readme](./docs/indexing.md) for more info.)
|
||||
- Real-time search results as you type!
|
||||
1. ✅ Indexes files (See [indexing Wiki](https://github.com/gtsteffaniak/filebrowser/wiki/Indexing) for more info.)
|
||||
- Real-time search results as you type
|
||||
- Search supports file/folder sizes and many file type filters.
|
||||
- Enhanced interactive results that show file/folder sizes.
|
||||
2. [x] Revamped and simplified GUI navbar and sidebar menu.
|
||||
- Additional compact view mode as well as refreshed view mode
|
||||
styles.
|
||||
2. ✅ Revamped UI
|
||||
- Additional compact view mode as well as refreshed view mode styles.
|
||||
- Many graphical and user experience improvements.
|
||||
- right-click context menu
|
||||
3. [x] Revamped and simplified configuration via `config.yaml` config file.
|
||||
4. [x] Better listing browsing
|
||||
- Switching view modes is instant
|
||||
- Folder sizes are shown as well
|
||||
- Changing Sort order is instant
|
||||
- The entire directory is loaded in 1/3 the time
|
||||
5. [x] Developer API support
|
||||
3. ✅ Revamped and simplified configuration via `config.yaml` config file.
|
||||
4. ✅ Better listing browsing
|
||||
- Instantly Switches view modes and sort order without reloading data.
|
||||
- Folder sizes are displayed
|
||||
- Navigating remembers the scroll position, navigating back keeps the last scroll position.
|
||||
5. ✅ Developer API support
|
||||
- Ability to create long-live API Tokens.
|
||||
- Helpful Swagger page available at `/swagger` endpoint.
|
||||
|
||||
Notable features that this fork *does not* have (removed):
|
||||
|
||||
- jobs/runners are not supported yet (planned).
|
||||
- shell commands are completely removed and will not be returned.
|
||||
- Themes and branding are not fully supported yet (planned).
|
||||
- see feature matrix below for more.
|
||||
- pagination for directory items, so large directories with more than 100,000 items may be slow to load or not load at all.
|
||||
- ❌ jobs/runners are not supported yet (planned).
|
||||
- ❌ per-user rules are not supported yet (planned).
|
||||
- ❌ shell commands are completely removed and will not be returned.
|
||||
|
||||
## About
|
||||
|
||||
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 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.
|
||||
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 from the original.
|
||||
There are hundreds of thousands of lines changed and they are generally
|
||||
@@ -66,7 +61,7 @@ focus of this fork is on a few key principles:
|
||||
- Minimize external dependencies and standard library usage.
|
||||
- Of course -- adding much-needed features.
|
||||
|
||||
For more questions, see the [Q&A Readme](./docs/questions.md)
|
||||
For more, see the [Q&A Wiki](https://github.com/gtsteffaniak/filebrowser/wiki/Q&A)
|
||||
|
||||
## Look
|
||||
|
||||
@@ -84,121 +79,36 @@ a popup menu.
|
||||
<p align="center">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/2be7a6c5-0f95-4d9f-bc05-484ee71246d8" title="Search GIF">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/f55a6f1f-b930-4399-98b5-94da6e90527a" title="Navigation GIF">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/75226dc4-9802-46f0-9e3c-e4403d3275da" title="Main Screenshot">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/93b019de-d38f-4aaa-bde3-3ba4e99ecd25" title="Main Screenshot">
|
||||
</p>
|
||||
|
||||
## Install
|
||||
## Install and Configuration
|
||||
|
||||
Using docker:
|
||||
|
||||
1. docker run (no persistent db):
|
||||
|
||||
```
|
||||
docker run -it -v /path/to/folder:/srv -v $(pwd)/config.yaml:/home/filebrowser/config.yaml -p 80:80 gtstef/filebrowser
|
||||
```
|
||||
|
||||
or optionally, as non-root filebrowser user:
|
||||
|
||||
```
|
||||
docker run -u filebrowser -it -v $(pwd)/config.yaml:/home/filebrowser/config.yaml -v /path/to/folder:/srv -p 80:80 gtstef/filebrowser
|
||||
```
|
||||
|
||||
1. docker compose:
|
||||
|
||||
- with local storage
|
||||
|
||||
```
|
||||
services:
|
||||
filebrowser:
|
||||
volumes:
|
||||
- '/path/to/folder:/srv' # required (for now not configurable)
|
||||
# optional if you want db to persist - configure a path under "database" dir in config file.
|
||||
- './database:/home/filebrowser/database'
|
||||
- './config.yaml:/home/filebrowser/config.yaml'
|
||||
ports:
|
||||
- '80:80'
|
||||
image: gtstef/filebrowser
|
||||
# optionally run as non-root filebrowser user
|
||||
#user: filebrowser
|
||||
restart: always
|
||||
```
|
||||
|
||||
- with network share
|
||||
|
||||
```
|
||||
services:
|
||||
filebrowser:
|
||||
volumes:
|
||||
- 'storage:/srv' # required (for now not configurable)
|
||||
# optional if you want db to persist - configure a path under "database" dir in config file.
|
||||
- './database:/home/filebrowser/database'
|
||||
- './config.yaml:/home/filebrowser/config.yaml'
|
||||
ports:
|
||||
- '80:80'
|
||||
image: gtstef/filebrowser
|
||||
restart: always
|
||||
volumes:
|
||||
storage:
|
||||
driver_opts:
|
||||
type: cifs
|
||||
o: "username=admin,password=password,rw" # enter valid info here
|
||||
device: "//192.168.1.100/share/" # enter valid info here
|
||||
|
||||
```
|
||||
|
||||
Not using docker (not recommended), download your binary from releases and run with your custom config file:
|
||||
|
||||
```
|
||||
./filebrowser -c <config.yaml or other /path/to/config.yaml>
|
||||
```
|
||||
See the [Configuration Wiki](https://github.com/gtsteffaniak/filebrowser/wiki/Configuration)
|
||||
|
||||
## Command Line Usage
|
||||
|
||||
There are very few commands available. There are 3 actions done via the command line:
|
||||
|
||||
1. Running the program, as shown in the install step. The only argument used is the config file if you choose to override the default "config.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"]`
|
||||
See the [CLI Wiki](https://github.com/gtsteffaniak/filebrowser/wiki/CLI)
|
||||
|
||||
## API Usage
|
||||
|
||||
API tokens can be created to perform actions, access file information, and update user settings just like what can be done from the UI. You can create API tokens from the settings page via "API Management" section. This section will only show up if the user has "API" permissions, which can be granted by editing the user in user management.
|
||||
|
||||
Regardless of whether a user has API permissions, anyone can visit the swagger page which is found at `/swagger`. This swagger page uses a short-live token (2-hour exp) that the UI uses, but allows for quick access to all the API's and their described usage and requirements:
|
||||
|
||||

|
||||
|
||||
When using the API outside of swagger, you will need to set the API token as a bearer token authentication type. This means the authorization header will look like `Authorization: Bearer <token>`. For example in Postman:
|
||||
|
||||
Successful Request:
|
||||
|
||||
<p align="center"><img width="500" alt="image" src="https://github.com/user-attachments/assets/4f18fa8a-8d87-4f40-9dc7-3d4407769b59"></p>
|
||||
|
||||
Failed Request
|
||||
|
||||
<p align="center"><img width="500" alt="image" src="https://github.com/user-attachments/assets/4da0deae-f93d-4d94-83b1-68806afb343a"></p>
|
||||
|
||||
See the [API Wiki](https://github.com/gtsteffaniak/filebrowser/wiki/API)
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration is now done via a single configuration file:
|
||||
`config.yaml`, here is an example of minimal [configuration
|
||||
file](./backend/config.yaml).
|
||||
Configuration is done via the `config.yaml`, see the [Configuration Wiki](https://github.com/gtsteffaniak/filebrowser/wiki/Configuration) for available configuration options and other help.
|
||||
|
||||
View the [Configuration Help Page](./docs/configuration.md) for available
|
||||
configuration options and other help.
|
||||
## Office File Support
|
||||
|
||||
See [Office Support Wiki](https://github.com/gtsteffaniak/filebrowser/wiki/Office-Support#adding-open-office-integration-for-docker) on how to enable office file editing and office related features.
|
||||
|
||||
## Migration from the original filebrowser
|
||||
|
||||
I would recommend that you start fresh without reusing the database. However,
|
||||
If you want to migrate your existing database to FileBrowser Quantum,
|
||||
visit the [migration
|
||||
readme](./docs/migration.md)
|
||||
See the [Migration Wiki](https://github.com/gtsteffaniak/filebrowser/wiki/Migration)
|
||||
|
||||
## Comparison Chart
|
||||
|
||||
Application Name | <img width="48" src="frontend/public/img/icons/favicon-256x256.png" > Quantum | <img width="48" src="https://github.com/filebrowser/filebrowser/blob/master/frontend/public/img/logo.svg" > Filebrowser | <img width="48" src="https://github.com/mickael-kerjean/filestash/blob/master/public/assets/logo/app_icon.png?raw=true" > Filestash | <img width="48" src="https://avatars.githubusercontent.com/u/19211038?s=200&v=4" > Nextcloud | <img width="48" src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/da/Google_Drive_logo.png/480px-Google_Drive_logo.png" > Google_Drive | <img width="48" src="https://avatars.githubusercontent.com/u/6422152?v=4" > FileRun
|
||||
Application Name | <img width="48" src="https://github.com/user-attachments/assets/59986a2a-f960-4536-aa35-4a9a7c98ad48" > 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 | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
@@ -208,7 +118,7 @@ Self hostable | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
Has Stable Release? | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
S3 support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
webdav support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
ftp support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
FTP support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
Dedicated docs site? | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
Multiple sources at once | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
Docker image size | 31 MB | 31 MB | 240 MB (main image) | 250 MB | ❌ | > 2 GB |
|
||||
@@ -235,18 +145,15 @@ Event-based notifications | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
Metrics | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
file space quotas | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||
text-based files editor | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
office file support | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||
office file support | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||
Office file previews | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||
Themes | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
|
||||
Branding support | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
|
||||
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](./docs/roadmap.md)
|
||||
Share collections of files | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
Can archive selected files | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
Can browse archive files | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.23-alpine AS base
|
||||
FROM golang:1.24-alpine AS base
|
||||
ARG VERSION
|
||||
ARG REVISION
|
||||
WORKDIR /app
|
||||
@@ -6,8 +6,8 @@ COPY ./backend ./
|
||||
#RUN swag init --output swagger/docs
|
||||
RUN ln -s swagger /usr/local/go/src/
|
||||
RUN go build -ldflags="-w -s \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/version.Version=${VERSION}' \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/version.CommitSHA=${REVISION}'" \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/version.Version=${VERSION}' \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/version.CommitSHA=${REVISION}'" \
|
||||
-o filebrowser .
|
||||
|
||||
FROM node:slim AS nbuild
|
||||
@@ -0,0 +1,6 @@
|
||||
FROM gtstef/playwright-base
|
||||
WORKDIR /app/frontend
|
||||
COPY [ "./frontend/", "./" ]
|
||||
WORKDIR /app/backend/
|
||||
COPY [ "./backend/filebrowser*", "./"]
|
||||
RUN ./filebrowser -c filebrowser-playwright.yaml & sleep 2 && cd ../frontend && npx playwright test
|
||||
@@ -0,0 +1,4 @@
|
||||
FROM node:22-slim
|
||||
WORKDIR /app/frontend
|
||||
RUN npm i @playwright/test
|
||||
RUN npx playwright install --with-deps firefox
|
||||
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
nginx-proxy-auth:
|
||||
image: nginx
|
||||
container_name: nginx-proxy-auth
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./src/default.conf:/etc/nginx/conf.d/default.conf
|
||||
filebrowser:
|
||||
volumes:
|
||||
- '../frontend:/home/frontend'
|
||||
- "./src/config.yaml:/home/filebrowser/config.yaml"
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: ./_docker/Dockerfile
|
||||
@@ -0,0 +1,8 @@
|
||||
server:
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
root: "../frontend/tests/playwright-files"
|
||||
auth:
|
||||
method: proxy
|
||||
header: X-Username
|
||||
signup: false
|
||||
@@ -0,0 +1,13 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost 127.0.0.1;
|
||||
|
||||
location / {
|
||||
proxy_pass http://filebrowser;
|
||||
proxy_set_header X-Username "proxy-user";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
+32
-15
@@ -6,36 +6,53 @@ 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 }}
|
||||
- -s -w -X github.com/gtsteffaniak/filebrowser/backend/version.Version={{ .Version }} -X github.com/gtsteffaniak/filebrowser/backend/version.CommitSHA={{ .ShortCommit }}
|
||||
main: main.go
|
||||
binary: filebrowser
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
- "6"
|
||||
- "7"
|
||||
hooks:
|
||||
post:
|
||||
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
|
||||
- id: windows
|
||||
ldflags: *ldflags
|
||||
main: main.go
|
||||
binary: filebrowser
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
hooks:
|
||||
post:
|
||||
- upx {{ .Path }} # Compress the binary with UPX
|
||||
|
||||
# Build configuration for windows without arm & upx
|
||||
- id: macos
|
||||
ldflags: *ldflags
|
||||
main: main.go
|
||||
binary: filebrowser
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
|
||||
archives:
|
||||
- name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
|
||||
- name_template: >
|
||||
{{- if eq .Os "windows" -}}
|
||||
{{.ProjectName}}
|
||||
{{- else -}}
|
||||
{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{.ProjectName}}
|
||||
{{- end -}}
|
||||
format: binary
|
||||
|
||||
checksum:
|
||||
|
||||
+14
-14
@@ -3,13 +3,13 @@ package auth
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
@@ -146,16 +146,16 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||
if u == nil {
|
||||
// create user with the provided credentials
|
||||
d := &users.User{
|
||||
Username: a.Cred.Username,
|
||||
Password: a.Cred.Password,
|
||||
Scope: a.Settings.UserDefaults.Scope,
|
||||
Locale: a.Settings.UserDefaults.Locale,
|
||||
ViewMode: a.Settings.UserDefaults.ViewMode,
|
||||
SingleClick: a.Settings.UserDefaults.SingleClick,
|
||||
Sorting: a.Settings.UserDefaults.Sorting,
|
||||
Perm: a.Settings.UserDefaults.Perm,
|
||||
Commands: a.Settings.UserDefaults.Commands,
|
||||
HideDotfiles: a.Settings.UserDefaults.HideDotfiles,
|
||||
Username: a.Cred.Username,
|
||||
Password: a.Cred.Password,
|
||||
Scope: a.Settings.UserDefaults.Scope,
|
||||
Locale: a.Settings.UserDefaults.Locale,
|
||||
ViewMode: a.Settings.UserDefaults.ViewMode,
|
||||
SingleClick: a.Settings.UserDefaults.SingleClick,
|
||||
Sorting: a.Settings.UserDefaults.Sorting,
|
||||
Perm: a.Settings.UserDefaults.Perm,
|
||||
Commands: a.Settings.UserDefaults.Commands,
|
||||
ShowHidden: a.Settings.UserDefaults.ShowHidden,
|
||||
}
|
||||
u = a.GetUser(d)
|
||||
|
||||
@@ -164,7 +164,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||
return nil, fmt.Errorf("user: failed to mkdir user home dir: [%s]", userHome)
|
||||
}
|
||||
u.Scope = userHome
|
||||
log.Printf("user: %s, home dir: [%s].", u.Username, userHome)
|
||||
logger.Debug(fmt.Sprintf("user: %s, home dir: [%s].", u.Username, userHome))
|
||||
|
||||
err = a.Users.Save(u)
|
||||
if err != nil {
|
||||
@@ -209,7 +209,7 @@ func (a *HookAuth) GetUser(d *users.User) *users.User {
|
||||
By: d.Sorting.By,
|
||||
},
|
||||
Commands: d.Commands,
|
||||
HideDotfiles: d.HideDotfiles,
|
||||
ShowHidden: d.ShowHidden,
|
||||
Perm: perms,
|
||||
LockPassword: true,
|
||||
}
|
||||
@@ -232,7 +232,7 @@ var validHookFields = []string{
|
||||
"user.sorting.by",
|
||||
"user.sorting.asc",
|
||||
"user.commands",
|
||||
"user.hideDotfiles",
|
||||
"user.showHidden",
|
||||
"user.perm.admin",
|
||||
"user.perm.execute",
|
||||
"user.perm.create",
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
@@ -24,7 +24,6 @@ type JSONAuth struct {
|
||||
|
||||
// Auth authenticates the user via a json in content body.
|
||||
func (a JSONAuth) Auth(r *http.Request, userStore *users.Storage) (*users.User, error) {
|
||||
config := &settings.Config
|
||||
var cred jsonCred
|
||||
|
||||
if r.Body == nil {
|
||||
@@ -47,7 +46,7 @@ func (a JSONAuth) Auth(r *http.Request, userStore *users.Storage) (*users.User,
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
}
|
||||
u, err := userStore.Get(config.Server.Root, cred.Username)
|
||||
u, err := userStore.Get(files.RootPaths["default"], cred.Username)
|
||||
if err != nil || !users.CheckPwd(cred.Password, u.Password) {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package auth
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ type NoAuth struct{}
|
||||
|
||||
// Auth uses authenticates user 1.
|
||||
func (a NoAuth) Auth(r *http.Request, usr *users.Storage) (*users.User, error) {
|
||||
return usr.Get(settings.Config.Server.Root, uint(1))
|
||||
return usr.Get(files.RootPaths["default"], uint(1))
|
||||
}
|
||||
|
||||
// LoginPage tells that no auth doesn't require a login page.
|
||||
|
||||
@@ -4,9 +4,8 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
@@ -21,7 +20,7 @@ type ProxyAuth struct {
|
||||
// Auth authenticates the user via an HTTP header.
|
||||
func (a ProxyAuth) Auth(r *http.Request, usr *users.Storage) (*users.User, error) {
|
||||
username := r.Header.Get(a.Header)
|
||||
user, err := usr.Get(settings.Config.Server.Root, username)
|
||||
user, err := usr.Get(files.RootPaths["default"], username)
|
||||
if err == errors.ErrNotExist {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
+19
-2
@@ -17,8 +17,25 @@ type Storage struct {
|
||||
}
|
||||
|
||||
// NewStorage creates a auth storage from a backend.
|
||||
func NewStorage(back StorageBackend, userStore *users.Storage) *Storage {
|
||||
return &Storage{back: back, users: userStore}
|
||||
func NewStorage(back StorageBackend, userStore *users.Storage) (*Storage, error) {
|
||||
store := &Storage{back: back, users: userStore}
|
||||
err := store.Save(&JSONAuth{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = store.Save(&ProxyAuth{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = store.Save(&HookAuth{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = store.Save(&NoAuth{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
|
||||
// Get wraps a StorageBackend.Get.
|
||||
|
||||
@@ -1,43 +1,66 @@
|
||||
|
||||
== Running benchmark ==
|
||||
/usr/local/go/bin/go
|
||||
? github.com/gtsteffaniak/filebrowser [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/auth [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/cmd [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/backend [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/backend/auth [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/backend/cmd [no test files]
|
||||
PASS
|
||||
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
|
||||
ok github.com/gtsteffaniak/filebrowser/backend/diskcache 0.005s
|
||||
? github.com/gtsteffaniak/filebrowser/backend/errors [no test files]
|
||||
/home/graham/git/filebrowser /home/graham/git/filebrowser
|
||||
/home/graham/git/filebrowser/files/file.go /home/graham/git/filebrowser
|
||||
/home/graham/git/filebrowser/mnt/doesnt/exist /home/graham/git/filebrowser
|
||||
2025/01/04 14:04:55 Initializing index and assessing file system complexity
|
||||
2025/01/04 14:04:55 Starting full scan
|
||||
2025/01/04 14:04:55 Index assessment : complexity=simple directories=0 files=0
|
||||
2025/01/04 14:04:55 Time Spent Indexing : 0 seconds
|
||||
2025/01/04 14:04:55 Next scan in 1m0s
|
||||
2025/01/04 14:04:56 Initializing index and assessing file system complexity
|
||||
2025/01/04 14:04:56 Starting full scan
|
||||
2025/01/04 14:04:56 Index assessment : complexity=simple directories=0 files=0
|
||||
2025/01/04 14:04:56 Time Spent Indexing : 0 seconds
|
||||
2025/01/04 14:04:56 Next scan in 1m0s
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
pkg: github.com/gtsteffaniak/filebrowser/files
|
||||
pkg: github.com/gtsteffaniak/filebrowser/backend/files
|
||||
cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz
|
||||
BenchmarkFillIndex-8 10 3847878 ns/op 758424 B/op 5567 allocs/op
|
||||
BenchmarkSearchAllIndexes-8 10 780431 ns/op 173444 B/op 2014 allocs/op
|
||||
BenchmarkFillIndex-8 2025/01/04 14:04:57 Initializing index and assessing file system complexity
|
||||
2025/01/04 14:04:57 Starting full scan
|
||||
2025/01/04 14:04:57 Index assessment : complexity=simple directories=0 files=0
|
||||
2025/01/04 14:04:57 Time Spent Indexing : 0 seconds
|
||||
2025/01/04 14:04:57 Next scan in 1m0s
|
||||
10 3515090 ns/op 34273 B/op 451 allocs/op
|
||||
BenchmarkCheckIndexExclude-8 10 156.2 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkCheckIndexConditionsInclude-8 10 98.00 ns/op 0 B/op 0 allocs/op
|
||||
2025/01/04 14:04:58 Initializing index and assessing file system complexity
|
||||
2025/01/04 14:04:58 Starting full scan
|
||||
2025/01/04 14:04:58 Index assessment : complexity=simple directories=0 files=0
|
||||
2025/01/04 14:04:58 Time Spent Indexing : 0 seconds
|
||||
2025/01/04 14:04:58 Next scan in 1m0s
|
||||
BenchmarkSearchAllIndexes-8 2025/01/04 14:04:59 Initializing index and assessing file system complexity
|
||||
2025/01/04 14:04:59 Starting full scan
|
||||
2025/01/04 14:04:59 Index assessment : complexity=simple directories=0 files=0
|
||||
2025/01/04 14:04:59 Time Spent Indexing : 0 seconds
|
||||
2025/01/04 14:04:59 Next scan in 1m0s
|
||||
10 766822 ns/op 34230 B/op 900 allocs/op
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/files 0.073s
|
||||
ok github.com/gtsteffaniak/filebrowser/backend/files 5.094s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/fileutils 0.003s
|
||||
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>
|
||||
ok github.com/gtsteffaniak/filebrowser/backend/fileutils 0.002s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/http 0.080s
|
||||
ok github.com/gtsteffaniak/filebrowser/backend/http 0.184s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/img 0.137s
|
||||
ok github.com/gtsteffaniak/filebrowser/backend/img 0.123s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/rules 0.002s
|
||||
ok github.com/gtsteffaniak/filebrowser/backend/runner 0.004s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/runner 0.003s
|
||||
ok github.com/gtsteffaniak/filebrowser/backend/settings 0.005s
|
||||
? github.com/gtsteffaniak/filebrowser/backend/share [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/backend/storage [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/backend/storage/bolt [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/backend/swagger/docs [no test files]
|
||||
PASS
|
||||
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]
|
||||
ok github.com/gtsteffaniak/filebrowser/backend/users 0.003s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/users 0.002s
|
||||
? github.com/gtsteffaniak/filebrowser/utils [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/version [no test files]
|
||||
ok github.com/gtsteffaniak/filebrowser/backend/utils 0.002s
|
||||
? github.com/gtsteffaniak/filebrowser/backend/version [no test files]
|
||||
+12
-5
@@ -1,4 +1,4 @@
|
||||
package utils
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
@@ -6,12 +6,13 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
DiskUsageCache = newCache(30*time.Second, 24*time.Hour)
|
||||
RealPathCache = newCache(48*time.Hour, 72*time.Hour)
|
||||
SearchResultsCache = newCache(15*time.Second, time.Hour)
|
||||
DiskUsage = NewCache(30*time.Second, 24*time.Hour)
|
||||
RealPath = NewCache(48*time.Hour, 72*time.Hour)
|
||||
SearchResults = NewCache(15*time.Second, time.Hour)
|
||||
OnlyOffice = NewCache(48*time.Hour, 1*time.Hour)
|
||||
)
|
||||
|
||||
func newCache(expires time.Duration, cleanup time.Duration) *KeyCache {
|
||||
func NewCache(expires time.Duration, cleanup time.Duration) *KeyCache {
|
||||
newCache := KeyCache{
|
||||
data: make(map[string]cachedValue),
|
||||
expiresAfter: expires, // default
|
||||
@@ -40,6 +41,12 @@ func (c *KeyCache) Set(key string, value interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *KeyCache) Delete(key string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.data, key)
|
||||
}
|
||||
|
||||
func (c *KeyCache) SetWithExp(key string, value interface{}, exp time.Duration) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
+119
-44
@@ -1,16 +1,19 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/diskcache"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
fbhttp "github.com/gtsteffaniak/filebrowser/backend/http"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/img"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/swagger/docs"
|
||||
@@ -25,20 +28,22 @@ func getStore(config string) (*storage.Storage, bool) {
|
||||
settings.Initialize(config)
|
||||
store, hasDB, err := storage.InitializeDb(settings.Config.Server.Database)
|
||||
if err != nil {
|
||||
log.Fatal("could not load db info: ", err)
|
||||
logger.Fatal(fmt.Sprintf("could not load db info: %v", err))
|
||||
}
|
||||
return store, hasDB
|
||||
}
|
||||
|
||||
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")
|
||||
fmt.Printf(`usage: ./filebrowser <command> [options]
|
||||
commands:
|
||||
-h Print help
|
||||
-c Print the default config file
|
||||
version Print version information
|
||||
set -u Username and password for the new user
|
||||
set -a Create user as admin
|
||||
set -s Specify a user scope
|
||||
set -h Print this help message
|
||||
`)
|
||||
}
|
||||
|
||||
func StartFilebrowser() {
|
||||
@@ -69,47 +74,79 @@ func StartFilebrowser() {
|
||||
setCmd.StringVar(&scope, "s", "", "Specify a user scope, otherwise default user config scope is used")
|
||||
setCmd.StringVar(&dbConfig, "c", "config.yaml", "Path to the config file, default: config.yaml")
|
||||
|
||||
// Create context and channels for graceful shutdown
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
done := make(chan struct{}) // Signals server has stopped
|
||||
shutdownComplete := make(chan struct{}) // Signals shutdown process is complete
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// 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)
|
||||
err := setCmd.Parse(os.Args[2:])
|
||||
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\"")
|
||||
fmt.Printf("not enough info to create user: \"set -u username,password\", only provided %v\n", userInfo)
|
||||
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)
|
||||
store, ok := getStore(dbConfig)
|
||||
if !ok {
|
||||
logger.Fatal("could not load db info")
|
||||
}
|
||||
newUser := users.User{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
if scope != "" {
|
||||
newUser.Scope = scope
|
||||
}
|
||||
err = storage.CreateUser(newUser, asAdmin)
|
||||
user, err := store.Users.Get("", username)
|
||||
if err != nil {
|
||||
log.Fatal("Could not create user: ", err)
|
||||
newUser := users.User{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
if scope != "" {
|
||||
newUser.Scope = scope
|
||||
} else {
|
||||
newUser.Scope = settings.Config.UserDefaults.Scope
|
||||
}
|
||||
// Create the user logic
|
||||
if asAdmin {
|
||||
logger.Info(fmt.Sprintf("Creating user as admin: %s\n", username))
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Creating non-admin user: %s\n", username))
|
||||
}
|
||||
err = storage.CreateUser(newUser, asAdmin)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("could not create user: %v", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
user.Password = password
|
||||
if scope != "" {
|
||||
user.Scope = scope
|
||||
}
|
||||
if asAdmin {
|
||||
user.Perm.Admin = true
|
||||
}
|
||||
err = store.Users.Save(user)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("could not update user: %v", err))
|
||||
}
|
||||
fmt.Printf("successfully updated user: %s\n", username)
|
||||
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)
|
||||
fmt.Printf(`FileBrowser Quantum - A modern web-based file manager
|
||||
Version : %v
|
||||
Commit : %v
|
||||
Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v
|
||||
`, version.Version, version.CommitSHA, version.Version)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -118,30 +155,68 @@ func StartFilebrowser() {
|
||||
if !dbExists {
|
||||
database = fmt.Sprintf("Creating new database : %v", settings.Config.Server.Database)
|
||||
}
|
||||
log.Printf("Initializing FileBrowser Quantum (%v)\n", version.Version)
|
||||
log.Printf("Using Config file : %v", configPath)
|
||||
log.Println("Embeded frontend :", os.Getenv("FILEBROWSER_NO_EMBEDED") != "true")
|
||||
log.Println(database)
|
||||
log.Println("Sources :", settings.Config.Server.Root)
|
||||
sources := []string{}
|
||||
for _, v := range settings.Config.Server.Sources {
|
||||
sources = append(sources, v.Name+": "+v.Path)
|
||||
}
|
||||
|
||||
authMethods := []string{}
|
||||
if settings.Config.Auth.Methods.PasswordAuth.Enabled {
|
||||
authMethods = append(authMethods, "Password")
|
||||
}
|
||||
if settings.Config.Auth.Methods.ProxyAuth.Enabled {
|
||||
authMethods = append(authMethods, "Proxy")
|
||||
}
|
||||
if settings.Config.Auth.Methods.NoAuth {
|
||||
logger.Warning("Configured with no authentication, this is not recommended.")
|
||||
authMethods = []string{"Disabled"}
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("Initializing FileBrowser Quantum (%v)", version.Version))
|
||||
logger.Info(fmt.Sprintf("Using Config file : %v", configPath))
|
||||
logger.Info(fmt.Sprintf("Auth Methods : %v", authMethods))
|
||||
logger.Info(database)
|
||||
logger.Info(fmt.Sprintf("Sources : %v", sources))
|
||||
serverConfig := settings.Config.Server
|
||||
swagInfo := docs.SwaggerInfo
|
||||
swagInfo.BasePath = serverConfig.BaseURL
|
||||
swag.Register(docs.SwaggerInfo.InstanceName(), swagInfo)
|
||||
// initialize indexing and schedule indexing ever n minutes (default 5)
|
||||
go files.InitializeIndex(serverConfig.Indexing)
|
||||
if err := rootCMD(store, &serverConfig); err != nil {
|
||||
log.Fatal("Error starting filebrowser:", err)
|
||||
sourceConfigs := settings.Config.Server.Sources
|
||||
if len(sourceConfigs) == 0 {
|
||||
logger.Fatal("No sources configured, exiting...")
|
||||
}
|
||||
for _, source := range sourceConfigs {
|
||||
go files.Initialize(source)
|
||||
}
|
||||
// Start the rootCMD in a goroutine
|
||||
go func() {
|
||||
if err := rootCMD(ctx, store, &serverConfig, shutdownComplete); err != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error starting filebrowser: %v", err))
|
||||
}
|
||||
close(done) // Signal that the server has stopped
|
||||
}()
|
||||
// Wait for a shutdown signal or the server to stop
|
||||
select {
|
||||
case <-signalChan:
|
||||
logger.Info("Received shutdown signal. Shutting down gracefully...")
|
||||
cancel() // Trigger context cancellation
|
||||
case <-done:
|
||||
logger.Info("Server stopped unexpectedly. Shutting down...")
|
||||
}
|
||||
|
||||
<-shutdownComplete // Ensure we don't exit prematurely
|
||||
// Wait for the server to stop
|
||||
logger.Info("Shutdown complete.")
|
||||
}
|
||||
|
||||
func rootCMD(store *storage.Storage, serverConfig *settings.Server) error {
|
||||
func rootCMD(ctx context.Context, store *storage.Storage, serverConfig *settings.Server, shutdownComplete chan struct{}) error {
|
||||
if serverConfig.NumImageProcessors < 1 {
|
||||
log.Fatal("Image resize workers count could not be < 1")
|
||||
logger.Fatal("Image resize workers count could not be < 1")
|
||||
}
|
||||
imgSvc := img.New(serverConfig.NumImageProcessors)
|
||||
|
||||
cacheDir := "/tmp"
|
||||
cacheDir := settings.Config.Server.CacheDir
|
||||
var fileCache diskcache.Interface
|
||||
|
||||
// Use file cache if cacheDir is specified
|
||||
@@ -149,13 +224,13 @@ func rootCMD(store *storage.Storage, serverConfig *settings.Server) error {
|
||||
var err error
|
||||
fileCache, err = diskcache.NewFileCache(cacheDir)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create file cache: %v", err)
|
||||
logger.Fatal(fmt.Sprintf("failed to create file cache: %v", err))
|
||||
}
|
||||
} else {
|
||||
// No-op cache if no cacheDir is specified
|
||||
fileCache = diskcache.NewNoOp()
|
||||
}
|
||||
fbhttp.StartHttp(imgSvc, store, fileCache)
|
||||
fbhttp.StartHttp(ctx, imgSvc, store, fileCache, shutdownComplete)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rulesCmd.AddCommand(rulesRmCommand)
|
||||
rulesRmCommand.Flags().Uint("index", 0, "index of rule to remove")
|
||||
_ = rulesRmCommand.MarkFlagRequired("index")
|
||||
}
|
||||
|
||||
var rulesRmCommand = &cobra.Command{
|
||||
Use: "rm <index> [index_end]",
|
||||
Short: "Remove a global rule or user rule",
|
||||
Long: `Remove a global rule or user rule. The provided index
|
||||
is the same that's printed when you run 'rules ls'. Note
|
||||
that after each removal/addition, the index of the
|
||||
commands change. So be careful when removing them after each
|
||||
other.
|
||||
|
||||
You can also specify an optional parameter (index_end) so
|
||||
you can remove all commands from 'index' to 'index_end',
|
||||
including 'index_end'.`,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil { //nolint:gomnd
|
||||
return err
|
||||
}
|
||||
|
||||
for _, arg := range args {
|
||||
if _, err := strconv.Atoi(arg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
i, err := strconv.Atoi(args[0])
|
||||
utils.CheckErr("strconv.Atoi", err)
|
||||
f := i
|
||||
if len(args) == 2 { //nolint:gomnd
|
||||
f, err = strconv.Atoi(args[1])
|
||||
utils.CheckErr("strconv.Atoi", err)
|
||||
}
|
||||
|
||||
user := func(u *users.User) {
|
||||
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
|
||||
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 := store.Settings.Save(s)
|
||||
utils.CheckErr("store.Settings.Save", err)
|
||||
}
|
||||
|
||||
runRules(store, cmd, user, global)
|
||||
}),
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
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")
|
||||
}
|
||||
|
||||
var rulesCmd = &cobra.Command{
|
||||
Use: "rules",
|
||||
Short: "Rules management utility",
|
||||
Long: `On each subcommand you'll have available at least two flags:
|
||||
"username" and "id". You must either set only one of them
|
||||
or none. If you set one of them, the command will apply to
|
||||
an user, otherwise it will be applied to the global set or
|
||||
rules.`,
|
||||
Args: cobra.NoArgs,
|
||||
}
|
||||
|
||||
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User), globalFn func(*settings.Settings)) {
|
||||
id := getUserIdentifier(cmd.Flags())
|
||||
if id != nil {
|
||||
user, err := st.Users.Get("", id)
|
||||
utils.CheckErr("st.Users.Get", err)
|
||||
|
||||
if usersFn != nil {
|
||||
usersFn(user)
|
||||
}
|
||||
|
||||
printRules(user.Rules, id)
|
||||
return
|
||||
}
|
||||
|
||||
s, err := st.Settings.Get()
|
||||
utils.CheckErr("st.Settings.Get", err)
|
||||
|
||||
if globalFn != nil {
|
||||
globalFn(s)
|
||||
}
|
||||
|
||||
printRules(s.Rules, id)
|
||||
}
|
||||
|
||||
func getUserIdentifier(flags *pflag.FlagSet) interface{} {
|
||||
id := mustGetUint(flags, "id")
|
||||
username := mustGetString(flags, "username")
|
||||
|
||||
if id != 0 {
|
||||
return id
|
||||
} else if username != "" {
|
||||
return username
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printRules(rulez []users.Rule, id interface{}) {
|
||||
|
||||
for id, rule := range rulez {
|
||||
fmt.Printf("(%d) ", id)
|
||||
if rule.Regex {
|
||||
if rule.Allow {
|
||||
fmt.Printf("Allow Regex: \t%s\n", rule.Regexp.Raw)
|
||||
} else {
|
||||
fmt.Printf("Disallow Regex: \t%s\n", rule.Regexp.Raw)
|
||||
}
|
||||
} else {
|
||||
if rule.Allow {
|
||||
fmt.Printf("Allow Path: \t%s\n", rule.Path)
|
||||
} else {
|
||||
fmt.Printf("Disallow Path: \t%s\n", rule.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rulesCmd.AddCommand(rulesAddCmd)
|
||||
rulesAddCmd.Flags().BoolP("allow", "a", false, "indicates this is an allow rule")
|
||||
rulesAddCmd.Flags().BoolP("regex", "r", false, "indicates this is a regex rule")
|
||||
}
|
||||
|
||||
var rulesAddCmd = &cobra.Command{
|
||||
Use: "add <path|expression>",
|
||||
Short: "Add a global rule or user rule",
|
||||
Long: `Add a global rule or user rule.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
allow := mustGetBool(cmd.Flags(), "allow")
|
||||
regex := mustGetBool(cmd.Flags(), "regex")
|
||||
exp := args[0]
|
||||
|
||||
if regex {
|
||||
regexp.MustCompile(exp)
|
||||
}
|
||||
|
||||
rule := users.Rule{
|
||||
Allow: allow,
|
||||
Regex: regex,
|
||||
}
|
||||
|
||||
if regex {
|
||||
rule.Regexp = &users.Regexp{Raw: exp}
|
||||
} else {
|
||||
rule.Path = exp
|
||||
}
|
||||
|
||||
user := func(u *users.User) {
|
||||
u.Rules = append(u.Rules, rule)
|
||||
err := store.Users.Save(u)
|
||||
utils.CheckErr("store.Users.Save", err)
|
||||
}
|
||||
|
||||
global := func(s *settings.Settings) {
|
||||
s.Rules = append(s.Rules, rule)
|
||||
err := store.Settings.Save(s)
|
||||
utils.CheckErr("store.Settings.Save", err)
|
||||
}
|
||||
|
||||
runRules(store, cmd, user, global)
|
||||
}),
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rulesCmd.AddCommand(rulesLsCommand)
|
||||
}
|
||||
|
||||
var rulesLsCommand = &cobra.Command{
|
||||
Use: "ls",
|
||||
Short: "List global rules or user specific rules",
|
||||
Long: `List global rules or user specific rules.`,
|
||||
Args: cobra.NoArgs,
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
runRules(store, cmd, nil, nil)
|
||||
}),
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
var usersCmd = &cobra.Command{
|
||||
Use: "users",
|
||||
Short: "Users management utility",
|
||||
Long: `Users management utility.`,
|
||||
Args: cobra.NoArgs,
|
||||
}
|
||||
|
||||
func printUsers(usrs []*users.User) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd
|
||||
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
|
||||
|
||||
for _, u := range usrs {
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
|
||||
u.ID,
|
||||
u.Username,
|
||||
u.Scope,
|
||||
u.Locale,
|
||||
u.ViewMode,
|
||||
u.SingleClick,
|
||||
u.Perm.Admin,
|
||||
u.Perm.Execute,
|
||||
u.Perm.Create,
|
||||
u.Perm.Rename,
|
||||
u.Perm.Modify,
|
||||
u.Perm.Delete,
|
||||
u.Perm.Share,
|
||||
u.Perm.Download,
|
||||
u.LockPassword,
|
||||
)
|
||||
}
|
||||
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
func parseUsernameOrID(arg string) (username string, id uint) {
|
||||
id64, err := strconv.ParseUint(arg, 10, 64)
|
||||
if err != nil {
|
||||
return arg, 0
|
||||
}
|
||||
return "", uint(id64)
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
usersCmd.AddCommand(usersAddCmd)
|
||||
}
|
||||
|
||||
var usersAddCmd = &cobra.Command{
|
||||
Use: "add <username> <password>",
|
||||
Short: "Create a new user",
|
||||
Long: `Create a new user and add it to the database.`,
|
||||
Args: cobra.ExactArgs(2), //nolint:gomnd
|
||||
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 := 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 := store.Settings.Get()
|
||||
utils.CheckErr("store.Settings.Get()", err)
|
||||
|
||||
userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root)
|
||||
utils.CheckErr("s2.MakeUserDir", err)
|
||||
user.Scope = userHome
|
||||
|
||||
err = store.Users.Save(user)
|
||||
utils.CheckErr("store.Users.Save", err)
|
||||
printUsers([]*users.User{user})
|
||||
}),
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
usersCmd.AddCommand(usersExportCmd)
|
||||
}
|
||||
|
||||
var usersExportCmd = &cobra.Command{
|
||||
Use: "export <path>",
|
||||
Short: "Export all users to a file.",
|
||||
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: 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)
|
||||
utils.CheckErr("marshal", err)
|
||||
}),
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
usersCmd.AddCommand(usersFindCmd)
|
||||
usersCmd.AddCommand(usersLsCmd)
|
||||
}
|
||||
|
||||
var usersFindCmd = &cobra.Command{
|
||||
Use: "find <id|username>",
|
||||
Short: "Find a user by username or id",
|
||||
Long: `Find a user by username or id. If no flag is set, all users will be printed.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: findUsers,
|
||||
}
|
||||
|
||||
var usersLsCmd = &cobra.Command{
|
||||
Use: "ls",
|
||||
Short: "List all users.",
|
||||
Args: cobra.NoArgs,
|
||||
Run: findUsers,
|
||||
}
|
||||
|
||||
var findUsers = cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
var (
|
||||
list []*users.User
|
||||
user *users.User
|
||||
err error
|
||||
)
|
||||
|
||||
if len(args) == 1 {
|
||||
username, id := parseUsernameOrID(args[0])
|
||||
if username != "" {
|
||||
user, err = store.Users.Get("", username)
|
||||
} else {
|
||||
user, err = store.Users.Get("", id)
|
||||
}
|
||||
|
||||
list = []*users.User{user}
|
||||
} else {
|
||||
list, err = store.Users.Gets("")
|
||||
}
|
||||
|
||||
utils.CheckErr("findUsers", err)
|
||||
printUsers(list)
|
||||
})
|
||||
@@ -1,88 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
usersCmd.AddCommand(usersImportCmd)
|
||||
usersImportCmd.Flags().Bool("overwrite", false, "overwrite users with the same id/username combo")
|
||||
usersImportCmd.Flags().Bool("replace", false, "replace the entire user base")
|
||||
}
|
||||
|
||||
var usersImportCmd = &cobra.Command{
|
||||
Use: "import <path>",
|
||||
Short: "Import users from a file",
|
||||
Long: `Import users from a file. The path must be for a json or yaml
|
||||
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: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
fd, err := os.Open(args[0])
|
||||
utils.CheckErr("os.Open", err)
|
||||
defer fd.Close()
|
||||
|
||||
list := []*users.User{}
|
||||
err = unmarshal(args[0], &list)
|
||||
utils.CheckErr("unmarshal", err)
|
||||
|
||||
if mustGetBool(cmd.Flags(), "replace") {
|
||||
oldUsers, err := store.Users.Gets("")
|
||||
utils.CheckErr("store.Users.Gets", err)
|
||||
|
||||
err = marshal("users.backup.json", list)
|
||||
utils.CheckErr("marshal users.backup.json", err)
|
||||
|
||||
for _, user := range oldUsers {
|
||||
err = store.Users.Delete(user.ID)
|
||||
utils.CheckErr("store.Users.Delete", err)
|
||||
}
|
||||
}
|
||||
|
||||
overwrite := mustGetBool(cmd.Flags(), "overwrite")
|
||||
|
||||
for _, user := range list {
|
||||
onDB, err := store.Users.Get("", user.ID)
|
||||
|
||||
// User exists in DB.
|
||||
if err == nil {
|
||||
if !overwrite {
|
||||
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 := store.Users.Get("", user.Username); err == nil { //nolint:govet
|
||||
newErr := usernameConflictError(user.Username, conflictuous.ID, user.ID)
|
||||
utils.CheckErr("usernameConflictError", newErr)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If it doesn't exist, set the ID to 0 to automatically get a new
|
||||
// one that make sense in this DB.
|
||||
user.ID = 0
|
||||
}
|
||||
|
||||
err = store.Users.Save(user)
|
||||
utils.CheckErr("store.Users.Save", err)
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
func usernameConflictError(username string, originalID, newID uint) error {
|
||||
return fmt.Errorf(`can't import user with ID %d and username "%s" because the username is already registred with the user %d`,
|
||||
newID, username, originalID)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
usersCmd.AddCommand(usersRmCmd)
|
||||
}
|
||||
|
||||
var usersRmCmd = &cobra.Command{
|
||||
Use: "rm <id|username>",
|
||||
Short: "Delete a user by username or id",
|
||||
Long: `Delete a user by username or id`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
username, id := parseUsernameOrID(args[0])
|
||||
var err error
|
||||
|
||||
if username != "" {
|
||||
err = store.Users.Delete(username)
|
||||
} else {
|
||||
err = store.Users.Delete(id)
|
||||
}
|
||||
|
||||
utils.CheckErr("usersRmCmd", err)
|
||||
log.Println("user deleted successfully")
|
||||
}),
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
usersCmd.AddCommand(usersUpdateCmd)
|
||||
}
|
||||
|
||||
var usersUpdateCmd = &cobra.Command{
|
||||
Use: "update <id|username>",
|
||||
Short: "Updates an existing user",
|
||||
Long: `Updates an existing user. Set the flags for the
|
||||
options you want to change.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
|
||||
username, id := parseUsernameOrID(args[0])
|
||||
|
||||
var (
|
||||
err error
|
||||
user *users.User
|
||||
)
|
||||
|
||||
if id != 0 {
|
||||
user, err = store.Users.Get("", id)
|
||||
} else {
|
||||
user, err = store.Users.Get("", username)
|
||||
}
|
||||
utils.CheckErr("store.Users.Get", err)
|
||||
|
||||
err = store.Users.Update(user)
|
||||
utils.CheckErr("store.Users.Update", err)
|
||||
printUsers([]*users.User{user})
|
||||
}),
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func mustGetString(flags *pflag.FlagSet, flag string) string {
|
||||
s, err := flags.GetString(flag)
|
||||
utils.CheckErr("mustGetString", err)
|
||||
return s
|
||||
}
|
||||
|
||||
func mustGetBool(flags *pflag.FlagSet, flag string) bool {
|
||||
b, err := flags.GetBool(flag)
|
||||
utils.CheckErr("mustGetBool", err)
|
||||
return b
|
||||
}
|
||||
|
||||
func mustGetUint(flags *pflag.FlagSet, flag string) uint {
|
||||
b, err := flags.GetUint(flag)
|
||||
utils.CheckErr("mustGetUint", err)
|
||||
return b
|
||||
}
|
||||
|
||||
type cobraFunc func(cmd *cobra.Command, args []string)
|
||||
type pythonFunc func(cmd *cobra.Command, args []string, store *storage.Storage)
|
||||
|
||||
func marshal(filename string, data interface{}) error {
|
||||
fd, err := os.Create(filename)
|
||||
|
||||
utils.CheckErr("os.Create", err)
|
||||
defer fd.Close()
|
||||
|
||||
switch ext := filepath.Ext(filename); ext {
|
||||
case ".json":
|
||||
encoder := json.NewEncoder(fd)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(data)
|
||||
case ".yml", ".yaml": //nolint:goconst
|
||||
_, err := yaml.Marshal(fd)
|
||||
return err
|
||||
default:
|
||||
return errors.New("invalid format: " + ext)
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshal(filename string, data interface{}) error {
|
||||
fd, err := os.Open(filename)
|
||||
utils.CheckErr("os.Open", err)
|
||||
defer fd.Close()
|
||||
|
||||
switch ext := filepath.Ext(filename); ext {
|
||||
case ".json":
|
||||
return json.NewDecoder(fd).Decode(data)
|
||||
case ".yml", ".yaml":
|
||||
return yaml.NewDecoder(fd).Decode(data)
|
||||
default:
|
||||
return errors.New("invalid format: " + ext)
|
||||
}
|
||||
}
|
||||
|
||||
func jsonYamlArg(cmd *cobra.Command, args []string) error {
|
||||
if err := cobra.ExactArgs(1)(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch ext := filepath.Ext(args[0]); ext {
|
||||
case ".json", ".yml", ".yaml":
|
||||
return nil
|
||||
default:
|
||||
return errors.New("invalid format: " + ext)
|
||||
}
|
||||
}
|
||||
|
||||
func cobraCmd(fn pythonFunc) cobraFunc {
|
||||
return func(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,11 @@ server:
|
||||
baseURL: "/"
|
||||
root: "/srv"
|
||||
auth:
|
||||
method: password
|
||||
signup: false
|
||||
userDefaults:
|
||||
darkMode: true
|
||||
disableSettings: false
|
||||
scope: "."
|
||||
hideDotfiles: true
|
||||
singleClick: false
|
||||
permissions:
|
||||
admin: false
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
server:
|
||||
root: "/srv"
|
||||
@@ -0,0 +1,27 @@
|
||||
server:
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
root: "../frontend/tests/playwright-files"
|
||||
auth:
|
||||
method: password
|
||||
signup: false
|
||||
frontend:
|
||||
name: "Graham's Filebrowser"
|
||||
disableDefaultLinks: true
|
||||
externalLinks:
|
||||
- text: "A playwright test"
|
||||
url: "https://playwright.dev/"
|
||||
title: "Playwright"
|
||||
userDefaults:
|
||||
darkMode: true
|
||||
disableSettings: false
|
||||
scope: "."
|
||||
singleClick: false
|
||||
permissions:
|
||||
admin: false
|
||||
create: false
|
||||
rename: false
|
||||
modify: false
|
||||
delete: false
|
||||
share: false
|
||||
download: false
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package files
|
||||
|
||||
func checkWindowsHidden(realpath string) bool {
|
||||
// Non-Windows platforms don't support hidden attributes in the same way
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package files
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func checkWindowsHidden(realpath string) bool {
|
||||
// Convert the realpath to a UTF-16 pointer
|
||||
pointer, err := windows.UTF16PtrFromString(realpath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get the file attributes
|
||||
attributes, err := windows.GetFileAttributes(pointer)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the hidden attribute is set
|
||||
if attributes&windows.FILE_ATTRIBUTE_HIDDEN != 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Optional: Check for system attribute
|
||||
if attributes&windows.FILE_ATTRIBUTE_SYSTEM != 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package files
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -52,6 +53,17 @@ var documentTypes = []string{
|
||||
".fb2", // FictionBook
|
||||
}
|
||||
|
||||
var onlyOfficeSupported = []string{
|
||||
".doc", ".docm", ".docx", ".dot", ".dotm", ".dotx", ".epub",
|
||||
".fb2", ".fodt", ".htm", ".html", ".mht", ".mhtml", ".odt",
|
||||
".ott", ".rtf", ".stw", ".sxw", ".txt", ".wps", ".wpt", ".xml",
|
||||
".csv", ".et", ".ett", ".fods", ".ods", ".ots", ".sxc", ".xls",
|
||||
".xlsb", ".xlsm", ".xlsx", ".xlt", ".xltm", ".xltx", ".dps",
|
||||
".dpt", ".fodp", ".odp", ".otp", ".pot", ".potm", ".potx",
|
||||
".pps", ".ppsm", ".ppsx", ".ppt", ".pptm", ".pptx", ".sxi",
|
||||
".djvu", ".docxf", ".oform", ".oxps", ".pdf", ".xps",
|
||||
}
|
||||
|
||||
// Text-based file extensions
|
||||
var textTypes = []string{
|
||||
// Common Text Formats
|
||||
@@ -256,3 +268,13 @@ func isArchive(extension string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isOnlyOffice(name string) bool {
|
||||
extention := filepath.Ext(name)
|
||||
for _, typefile := range onlyOfficeSupported {
|
||||
if extention == typefile {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
+108
-114
@@ -21,8 +21,10 @@ import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/cache"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/fileutils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
@@ -34,34 +36,39 @@ var (
|
||||
)
|
||||
|
||||
type ItemInfo struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
ModTime time.Time `json:"modified"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"` // name of the file
|
||||
Size int64 `json:"size"` // length in bytes for regular files
|
||||
ModTime time.Time `json:"modified"` // modification time
|
||||
Type string `json:"type"` // type of the file, either "directory" or a file mimetype
|
||||
Hidden bool `json:"hidden"` // whether the file is hidden
|
||||
}
|
||||
|
||||
// FileInfo describes a file.
|
||||
// reduced item is non-recursive reduced "Items", used to pass flat items array
|
||||
type FileInfo struct {
|
||||
ItemInfo
|
||||
Files []ItemInfo `json:"files"`
|
||||
Folders []ItemInfo `json:"folders"`
|
||||
Path string `json:"path"`
|
||||
Files []ItemInfo `json:"files"` // files in the directory
|
||||
Folders []ItemInfo `json:"folders"` // folders in the directory
|
||||
Path string `json:"path"` // path scoped to the associated index
|
||||
}
|
||||
|
||||
// for efficiency, a response will be a pointer to the data
|
||||
// extra calculated fields can be added here
|
||||
type ExtendedFileInfo struct {
|
||||
*FileInfo
|
||||
Content string `json:"content,omitempty"`
|
||||
Subtitles []string `json:"subtitles,omitempty"`
|
||||
Checksums map[string]string `json:"checksums,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
FileInfo
|
||||
Content string `json:"content,omitempty"` // text content of a file, if requested
|
||||
Subtitles []string `json:"subtitles,omitempty"` // subtitles for video files
|
||||
Checksums map[string]string `json:"checksums,omitempty"` // checksums for the file
|
||||
Token string `json:"token,omitempty"` // token for the file -- used for sharing
|
||||
OnlyOfficeId string `json:"onlyOfficeId,omitempty"` // id for onlyoffice files
|
||||
Source string `json:"source"` // associated index source for the file
|
||||
RealPath string `json:"-"`
|
||||
}
|
||||
|
||||
// FileOptions are the options when getting a file info.
|
||||
type FileOptions struct {
|
||||
Path string // realpath
|
||||
Source string
|
||||
IsDir bool
|
||||
Modify bool
|
||||
Expand bool
|
||||
@@ -75,9 +82,15 @@ func (f FileOptions) Components() (string, string) {
|
||||
}
|
||||
|
||||
func FileInfoFaster(opts FileOptions) (ExtendedFileInfo, error) {
|
||||
index := GetIndex(rootPath)
|
||||
opts.Path = index.makeIndexPath(opts.Path)
|
||||
response := ExtendedFileInfo{}
|
||||
if opts.Source == "" {
|
||||
opts.Source = "default"
|
||||
}
|
||||
index := GetIndex(opts.Source)
|
||||
if index == nil {
|
||||
return response, fmt.Errorf("could not get index: %v ", opts.Source)
|
||||
}
|
||||
opts.Path = index.makeIndexPath(opts.Path)
|
||||
// Lock access for the specific path
|
||||
pathMutex := getMutex(opts.Path)
|
||||
pathMutex.Lock()
|
||||
@@ -86,7 +99,7 @@ func FileInfoFaster(opts FileOptions) (ExtendedFileInfo, error) {
|
||||
return response, os.ErrPermission
|
||||
}
|
||||
|
||||
_, isDir, err := GetRealPath(opts.Path)
|
||||
realPath, isDir, err := index.GetRealPath(opts.Path)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
@@ -117,19 +130,38 @@ func FileInfoFaster(opts FileOptions) (ExtendedFileInfo, error) {
|
||||
}
|
||||
info, exists := index.GetReducedMetadata(opts.Path, opts.IsDir)
|
||||
if !exists {
|
||||
return response, err
|
||||
return response, fmt.Errorf("could not get metadata for path: %v", opts.Path)
|
||||
}
|
||||
if opts.Content {
|
||||
content, err := getContent(opts.Path)
|
||||
content, err := getContent("default", opts.Path)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
response.Content = content
|
||||
}
|
||||
response.FileInfo = info
|
||||
response.FileInfo = *info
|
||||
response.RealPath = realPath
|
||||
response.Source = opts.Source
|
||||
if settings.Config.Integrations.OnlyOffice.Secret != "" && info.Type != "directory" && isOnlyOffice(info.Name) {
|
||||
response.OnlyOfficeId = generateOfficeId(realPath)
|
||||
}
|
||||
if strings.HasPrefix(info.Type, "video") {
|
||||
response.detectSubtitles(realPath)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func generateOfficeId(realPath string) string {
|
||||
key, ok := cache.OnlyOffice.Get(realPath).(string)
|
||||
if !ok {
|
||||
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
documentKey := utils.HashSHA256(realPath + timestamp)
|
||||
cache.OnlyOffice.Set(realPath, documentKey)
|
||||
return documentKey
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// Checksum checksums a given File for a given User, using a specific
|
||||
// algorithm. The checksums data is saved on File object.
|
||||
func GetChecksum(fullPath, algo string) (map[string]string, error) {
|
||||
@@ -160,49 +192,13 @@ func GetChecksum(fullPath, algo string) (map[string]string, error) {
|
||||
return subs, nil
|
||||
}
|
||||
|
||||
// RealPath gets the real path for the file, resolving symlinks if supported.
|
||||
func (i *FileInfo) RealPath() string {
|
||||
realPath, _, _ := GetRealPath(rootPath, i.Path)
|
||||
realPath, err := filepath.EvalSymlinks(realPath)
|
||||
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...)
|
||||
|
||||
isDir, _ := utils.RealPathCache.Get(joinedPath + ":isdir").(bool)
|
||||
cached, ok := utils.RealPathCache.Get(joinedPath).(string)
|
||||
if ok && cached != "" {
|
||||
return cached, isDir, nil
|
||||
}
|
||||
// Convert relative path to absolute path
|
||||
absolutePath, err := filepath.Abs(joinedPath)
|
||||
if err != nil {
|
||||
return absolutePath, false, fmt.Errorf("could not get real path: %v, %s", combined, err)
|
||||
}
|
||||
// Resolve symlinks and get the real path
|
||||
realPath, isDir, err := resolveSymlinks(absolutePath)
|
||||
if err == nil {
|
||||
utils.RealPathCache.Set(joinedPath, realPath)
|
||||
utils.RealPathCache.Set(joinedPath+":isdir", isDir)
|
||||
}
|
||||
return realPath, isDir, err
|
||||
}
|
||||
|
||||
func DeleteFiles(absPath string, opts FileOptions) error {
|
||||
func DeleteFiles(source, absPath string, dirPath string) error {
|
||||
err := os.RemoveAll(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index := GetIndex(rootPath)
|
||||
refreshConfig := FileOptions{Path: filepath.Dir(opts.Path), IsDir: true}
|
||||
index := GetIndex(source)
|
||||
refreshConfig := FileOptions{Path: dirPath, IsDir: true}
|
||||
err = index.RefreshFileInfo(refreshConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -210,19 +206,19 @@ func DeleteFiles(absPath string, opts FileOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func MoveResource(realsrc, realdst string, isSrcDir bool) error {
|
||||
func MoveResource(source, realsrc, realdst string, isSrcDir bool) error {
|
||||
err := fileutils.MoveFile(realsrc, realdst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index := GetIndex(rootPath)
|
||||
index := GetIndex(source)
|
||||
// refresh info for source and dest
|
||||
err = index.RefreshFileInfo(FileOptions{
|
||||
Path: realsrc,
|
||||
IsDir: isSrcDir,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.ErrEmptyKey
|
||||
return fmt.Errorf("could not refresh index for source: %v", err)
|
||||
}
|
||||
refreshConfig := FileOptions{Path: realdst, IsDir: true}
|
||||
if !isSrcDir {
|
||||
@@ -230,17 +226,17 @@ func MoveResource(realsrc, realdst string, isSrcDir bool) error {
|
||||
}
|
||||
err = index.RefreshFileInfo(refreshConfig)
|
||||
if err != nil {
|
||||
return errors.ErrEmptyKey
|
||||
return fmt.Errorf("could not refresh index for dest: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CopyResource(realsrc, realdst string, isSrcDir bool) error {
|
||||
func CopyResource(source, realsrc, realdst string, isSrcDir bool) error {
|
||||
err := fileutils.CopyFile(realsrc, realdst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index := GetIndex(rootPath)
|
||||
index := GetIndex(source)
|
||||
refreshConfig := FileOptions{Path: realdst, IsDir: true}
|
||||
if !isSrcDir {
|
||||
refreshConfig.Path = filepath.Dir(realdst)
|
||||
@@ -253,14 +249,14 @@ func CopyResource(realsrc, realdst string, isSrcDir bool) error {
|
||||
}
|
||||
|
||||
func WriteDirectory(opts FileOptions) error {
|
||||
realPath, _, _ := GetRealPath(rootPath, opts.Path)
|
||||
idx := GetIndex(opts.Source)
|
||||
realPath, _, _ := idx.GetRealPath(opts.Path)
|
||||
// Ensure the parent directories exist
|
||||
err := os.MkdirAll(realPath, 0775)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index := GetIndex(rootPath)
|
||||
err = index.RefreshFileInfo(opts)
|
||||
err = idx.RefreshFileInfo(opts)
|
||||
if err != nil {
|
||||
return errors.ErrEmptyKey
|
||||
}
|
||||
@@ -268,7 +264,8 @@ func WriteDirectory(opts FileOptions) error {
|
||||
}
|
||||
|
||||
func WriteFile(opts FileOptions, in io.Reader) error {
|
||||
dst, _, _ := GetRealPath(rootPath, opts.Path)
|
||||
idx := GetIndex(opts.Source)
|
||||
dst, _, _ := idx.GetRealPath(opts.Path)
|
||||
parentDir := filepath.Dir(dst)
|
||||
// Create the directory and all necessary parents
|
||||
err := os.MkdirAll(parentDir, 0775)
|
||||
@@ -290,8 +287,7 @@ func WriteFile(opts FileOptions, in io.Reader) error {
|
||||
}
|
||||
opts.Path = parentDir
|
||||
opts.IsDir = true
|
||||
index := GetIndex(rootPath)
|
||||
return index.RefreshFileInfo(opts)
|
||||
return idx.RefreshFileInfo(opts)
|
||||
}
|
||||
|
||||
// resolveSymlinks resolves symlinks in the given path
|
||||
@@ -323,8 +319,9 @@ func resolveSymlinks(path string) (string, bool, error) {
|
||||
}
|
||||
|
||||
// addContent reads and sets content based on the file type.
|
||||
func getContent(path string) (string, error) {
|
||||
realPath, _, err := GetRealPath(rootPath, path)
|
||||
func getContent(source, path string) (string, error) {
|
||||
idx := GetIndex(source)
|
||||
realPath, _, err := idx.GetRealPath(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -344,19 +341,22 @@ func getContent(path string) (string, error) {
|
||||
}
|
||||
|
||||
// DetectType detects the MIME type of a file and updates the ItemInfo struct.
|
||||
func (i *ItemInfo) DetectType(path string, saveContent bool) {
|
||||
func (i *ItemInfo) DetectType(realPath string, saveContent bool) {
|
||||
name := i.Name
|
||||
ext := filepath.Ext(name)
|
||||
|
||||
// Attempt MIME detection by file extension
|
||||
if ext == ".md" {
|
||||
i.Type = "text/markdown"
|
||||
return
|
||||
}
|
||||
i.Type = strings.Split(mime.TypeByExtension(ext), ";")[0]
|
||||
if i.Type == "" {
|
||||
i.Type = extendedMimeTypeCheck(ext)
|
||||
}
|
||||
if i.Type == "blob" {
|
||||
realpath, _, _ := GetRealPath(path)
|
||||
// Read only the first 512 bytes for efficient MIME detection
|
||||
file, err := os.Open(realpath)
|
||||
file, err := os.Open(realPath)
|
||||
if err != nil {
|
||||
|
||||
} else {
|
||||
@@ -370,42 +370,32 @@ func (i *ItemInfo) DetectType(path string, saveContent bool) {
|
||||
|
||||
// TODO add subtitles back
|
||||
// detectSubtitles detects subtitles for video files.
|
||||
//func (i *FileInfo) detectSubtitles(path string) {
|
||||
// if i.Type != "video" {
|
||||
// return
|
||||
// }
|
||||
// parentDir := filepath.Dir(path)
|
||||
// fileName := filepath.Base(path)
|
||||
// i.Subtitles = []string{}
|
||||
// ext := filepath.Ext(fileName)
|
||||
// dir, err := os.Open(parentDir)
|
||||
// if err != nil {
|
||||
// // Directory must have been deleted, remove it from the index
|
||||
// return
|
||||
// }
|
||||
// defer dir.Close() // Ensure directory handle is closed
|
||||
//
|
||||
// files, err := dir.Readdir(-1)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// base := strings.TrimSuffix(fileName, ext)
|
||||
// subtitleExts := []string{".vtt", ".txt", ".srt", ".lrc"}
|
||||
//
|
||||
// for _, f := range files {
|
||||
// if f.IsDir() || !strings.HasPrefix(f.Name(), base) {
|
||||
// continue
|
||||
// }
|
||||
//
|
||||
// for _, subtitleExt := range subtitleExts {
|
||||
// if strings.HasSuffix(f.Name(), subtitleExt) {
|
||||
// i.Subtitles = append(i.Subtitles, filepath.Join(parentDir, f.Name()))
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
func (i *ExtendedFileInfo) detectSubtitles(path string) {
|
||||
if !strings.HasPrefix(i.Type, "video") {
|
||||
logger.Debug("subtitles are not supported for this file : " + path)
|
||||
return
|
||||
}
|
||||
|
||||
idx := GetIndex(i.Source)
|
||||
parentInfo, exists := idx.GetReducedMetadata(filepath.Dir(i.Path), true)
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
base := strings.Split(i.Name, ".")[0]
|
||||
for _, f := range parentInfo.Files {
|
||||
baseName := strings.Split(f.Name, ".")[0]
|
||||
if baseName != base {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, subtitleExt := range []string{".vtt", ".srt", ".lrc", ".sbv", ".ass", ".ssa", ".sub", ".smi"} {
|
||||
if strings.HasSuffix(f.Name, subtitleExt) {
|
||||
fullPathBase := strings.Split(i.Path, ".")[0]
|
||||
i.Subtitles = append(i.Subtitles, fullPathBase+subtitleExt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func IsNamedPipe(mode os.FileMode) bool {
|
||||
return mode&os.ModeNamedPipe != 0
|
||||
@@ -441,9 +431,11 @@ func Exists(path string) bool {
|
||||
|
||||
func (info *FileInfo) SortItems() {
|
||||
sort.Slice(info.Folders, func(i, j int) bool {
|
||||
nameWithoutExt := strings.Split(info.Folders[i].Name, ".")[0]
|
||||
nameWithoutExt2 := strings.Split(info.Folders[j].Name, ".")[0]
|
||||
// Convert strings to integers for numeric sorting if both are numeric
|
||||
numI, errI := strconv.Atoi(info.Folders[i].Name)
|
||||
numJ, errJ := strconv.Atoi(info.Folders[j].Name)
|
||||
numI, errI := strconv.Atoi(nameWithoutExt)
|
||||
numJ, errJ := strconv.Atoi(nameWithoutExt2)
|
||||
if errI == nil && errJ == nil {
|
||||
return numI < numJ
|
||||
}
|
||||
@@ -451,9 +443,11 @@ func (info *FileInfo) SortItems() {
|
||||
return strings.ToLower(info.Folders[i].Name) < strings.ToLower(info.Folders[j].Name)
|
||||
})
|
||||
sort.Slice(info.Files, func(i, j int) bool {
|
||||
nameWithoutExt := strings.Split(info.Files[i].Name, ".")[0]
|
||||
nameWithoutExt2 := strings.Split(info.Files[j].Name, ".")[0]
|
||||
// Convert strings to integers for numeric sorting if both are numeric
|
||||
numI, errI := strconv.Atoi(info.Files[i].Name)
|
||||
numJ, errJ := strconv.Atoi(info.Files[j].Name)
|
||||
numI, errI := strconv.Atoi(nameWithoutExt)
|
||||
numJ, errJ := strconv.Atoi(nameWithoutExt2)
|
||||
if errI == nil && errJ == nil {
|
||||
return numI < numJ
|
||||
}
|
||||
|
||||
+25
-17
@@ -6,6 +6,8 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
func Test_GetRealPath(t *testing.T) {
|
||||
@@ -13,7 +15,7 @@ func Test_GetRealPath(t *testing.T) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
trimPrefix := filepath.Dir(filepath.Dir(cwd)) + "/"
|
||||
trimPrefix := filepath.Dir(filepath.Dir(cwd))
|
||||
tests := []struct {
|
||||
name string
|
||||
paths []string
|
||||
@@ -31,20 +33,20 @@ func Test_GetRealPath(t *testing.T) {
|
||||
path string
|
||||
isDir bool
|
||||
}{
|
||||
path: "backend/files",
|
||||
path: "",
|
||||
isDir: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "current directory",
|
||||
paths: []string{
|
||||
"./file.go",
|
||||
"./files/file.go",
|
||||
},
|
||||
want: struct {
|
||||
path string
|
||||
isDir bool
|
||||
}{
|
||||
path: "backend/files/file.go",
|
||||
path: "/files/file.go",
|
||||
isDir: false,
|
||||
},
|
||||
},
|
||||
@@ -62,9 +64,15 @@ func Test_GetRealPath(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
idx := Index{
|
||||
Source: settings.Source{
|
||||
Path: trimPrefix,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
realPath, isDir, _ := GetRealPath(tt.paths...)
|
||||
realPath, isDir, _ := idx.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)
|
||||
@@ -83,30 +91,30 @@ func TestSortItems(t *testing.T) {
|
||||
name: "Numeric and Lexicographical Sorting",
|
||||
input: FileInfo{
|
||||
Folders: []ItemInfo{
|
||||
{Name: "10"},
|
||||
{Name: "2"},
|
||||
{Name: "10.txt"},
|
||||
{Name: "2.txt"},
|
||||
{Name: "apple"},
|
||||
{Name: "Banana"},
|
||||
},
|
||||
Files: []ItemInfo{
|
||||
{Name: "File2"},
|
||||
{Name: "File10"},
|
||||
{Name: "File2.txt"},
|
||||
{Name: "File10.txt"},
|
||||
{Name: "File1"},
|
||||
{Name: "banana"},
|
||||
},
|
||||
},
|
||||
expected: FileInfo{
|
||||
Folders: []ItemInfo{
|
||||
{Name: "2"},
|
||||
{Name: "10"},
|
||||
{Name: "2.txt"},
|
||||
{Name: "10.txt"},
|
||||
{Name: "apple"},
|
||||
{Name: "Banana"},
|
||||
},
|
||||
Files: []ItemInfo{
|
||||
{Name: "banana"},
|
||||
{Name: "File1"},
|
||||
{Name: "File10"},
|
||||
{Name: "File2"},
|
||||
{Name: "File10.txt"},
|
||||
{Name: "File2.txt"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -114,8 +122,8 @@ func TestSortItems(t *testing.T) {
|
||||
name: "Only Lexicographical Sorting",
|
||||
input: FileInfo{
|
||||
Folders: []ItemInfo{
|
||||
{Name: "dog"},
|
||||
{Name: "Cat"},
|
||||
{Name: "dog.txt"},
|
||||
{Name: "Cat.txt"},
|
||||
{Name: "apple"},
|
||||
},
|
||||
Files: []ItemInfo{
|
||||
@@ -127,8 +135,8 @@ func TestSortItems(t *testing.T) {
|
||||
expected: FileInfo{
|
||||
Folders: []ItemInfo{
|
||||
{Name: "apple"},
|
||||
{Name: "Cat"},
|
||||
{Name: "dog"},
|
||||
{Name: "Cat.txt"},
|
||||
{Name: "dog.txt"},
|
||||
},
|
||||
Files: []ItemInfo{
|
||||
{Name: "apple"},
|
||||
|
||||
+170
-49
@@ -2,19 +2,22 @@ package files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/cache"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
type Index struct {
|
||||
Root string
|
||||
settings.Source
|
||||
Directories map[string]*FileInfo
|
||||
NumDirs uint64
|
||||
NumFiles uint64
|
||||
@@ -30,32 +33,42 @@ type Index struct {
|
||||
}
|
||||
|
||||
var (
|
||||
rootPath string = "/srv"
|
||||
indexes []*Index
|
||||
indexes map[string]*Index
|
||||
indexesMutex sync.RWMutex
|
||||
RootPaths map[string]string
|
||||
)
|
||||
|
||||
func InitializeIndex(enabled bool) {
|
||||
if enabled {
|
||||
func Initialize(source settings.Source) {
|
||||
indexesMutex.RLock()
|
||||
newIndex := Index{
|
||||
Source: source,
|
||||
Directories: make(map[string]*FileInfo),
|
||||
}
|
||||
if RootPaths == nil {
|
||||
RootPaths = make(map[string]string)
|
||||
}
|
||||
RootPaths[source.Name] = source.Path
|
||||
indexes = make(map[string]*Index)
|
||||
indexes[newIndex.Source.Name] = &newIndex
|
||||
indexesMutex.RUnlock()
|
||||
|
||||
if !newIndex.Source.Config.Disabled {
|
||||
time.Sleep(time.Second)
|
||||
if settings.Config.Server.Root != "" {
|
||||
rootPath = settings.Config.Server.Root
|
||||
}
|
||||
si := GetIndex(rootPath)
|
||||
log.Println("Initializing index and assessing file system complexity")
|
||||
si.RunIndexing("/", false)
|
||||
go si.setupIndexingScanners()
|
||||
logger.Info(fmt.Sprintf("initializing index: [%v]", newIndex.Source.Name))
|
||||
newIndex.RunIndexing("/", false)
|
||||
go newIndex.setupIndexingScanners()
|
||||
} else {
|
||||
logger.Debug("indexing disabled for source: " + newIndex.Source.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Define a function to recursively index files and directories
|
||||
func (si *Index) indexDirectory(adjustedPath string, quick, recursive bool) error {
|
||||
realPath := strings.TrimRight(si.Root, "/") + adjustedPath
|
||||
|
||||
func (idx *Index) indexDirectory(adjustedPath string, quick, recursive bool) error {
|
||||
realPath := strings.TrimRight(idx.Source.Path, "/") + adjustedPath
|
||||
// Open the directory
|
||||
dir, err := os.Open(realPath)
|
||||
if err != nil {
|
||||
si.RemoveDirectory(adjustedPath) // Remove, must have been deleted
|
||||
idx.RemoveDirectory(adjustedPath) // Remove, must have been deleted
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
@@ -69,32 +82,32 @@ func (si *Index) indexDirectory(adjustedPath string, quick, recursive bool) erro
|
||||
combinedPath = "/"
|
||||
}
|
||||
// get whats currently in cache
|
||||
si.mu.RLock()
|
||||
idx.mu.RLock()
|
||||
cacheDirItems := []ItemInfo{}
|
||||
modChange := true // default to true
|
||||
cachedDir, exists := si.Directories[adjustedPath]
|
||||
cachedDir, exists := idx.Directories[adjustedPath]
|
||||
if exists && quick {
|
||||
modChange = dirInfo.ModTime() != cachedDir.ModTime
|
||||
cacheDirItems = cachedDir.Folders
|
||||
}
|
||||
si.mu.RUnlock()
|
||||
idx.mu.RUnlock()
|
||||
|
||||
// If the directory has not been modified since the last index, skip expensive readdir
|
||||
// recursively check cached dirs for mod time changes as well
|
||||
if !modChange && recursive {
|
||||
for _, item := range cacheDirItems {
|
||||
err = si.indexDirectory(combinedPath+item.Name, quick, true)
|
||||
err = idx.indexDirectory(combinedPath+item.Name, quick, true)
|
||||
if err != nil {
|
||||
fmt.Printf("error indexing directory %v : %v", combinedPath+item.Name, err)
|
||||
logger.Error(fmt.Sprintf("error indexing directory %v : %v", combinedPath+item.Name, err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if quick {
|
||||
si.mu.Lock()
|
||||
si.FilesChangedDuringIndexing = true
|
||||
si.mu.Unlock()
|
||||
idx.mu.Lock()
|
||||
idx.FilesChangedDuringIndexing = true
|
||||
idx.mu.Unlock()
|
||||
}
|
||||
|
||||
// Read directory contents
|
||||
@@ -109,36 +122,57 @@ func (si *Index) indexDirectory(adjustedPath string, quick, recursive bool) erro
|
||||
|
||||
// Process each file and directory in the current directory
|
||||
for _, file := range files {
|
||||
isHidden := isHidden(file, idx.Source.Path+combinedPath)
|
||||
isDir := file.IsDir()
|
||||
fullCombined := combinedPath + file.Name()
|
||||
if idx.shouldSkip(isDir, isHidden, fullCombined) {
|
||||
continue
|
||||
}
|
||||
itemInfo := &ItemInfo{
|
||||
Name: file.Name(),
|
||||
ModTime: file.ModTime(),
|
||||
Hidden: isHidden,
|
||||
}
|
||||
if file.IsDir() {
|
||||
|
||||
// fix for .app files on macos which are technically directories, but we don't want to treat them as such
|
||||
if isDir && strings.HasSuffix(file.Name(), ".app") {
|
||||
isDir = false
|
||||
}
|
||||
if isDir {
|
||||
|
||||
// skip non-indexable dirs.
|
||||
if file.Name() == "$RECYCLE.BIN" || file.Name() == "System Volume Information" {
|
||||
continue
|
||||
}
|
||||
|
||||
dirPath := combinedPath + file.Name()
|
||||
if recursive {
|
||||
// Recursively index the subdirectory
|
||||
err = si.indexDirectory(dirPath, quick, recursive)
|
||||
err = idx.indexDirectory(dirPath, quick, recursive)
|
||||
if err != nil {
|
||||
log.Printf("Failed to index directory %s: %v", dirPath, err)
|
||||
logger.Error(fmt.Sprintf("Failed to index directory %s: %v", dirPath, err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
realDirInfo, exists := si.GetMetadataInfo(dirPath, true)
|
||||
realDirInfo, exists := idx.GetMetadataInfo(dirPath, true)
|
||||
if exists {
|
||||
itemInfo.Size = realDirInfo.Size
|
||||
}
|
||||
totalSize += itemInfo.Size
|
||||
itemInfo.Type = "directory"
|
||||
dirInfos = append(dirInfos, *itemInfo)
|
||||
si.NumDirs++
|
||||
idx.NumDirs++
|
||||
} else {
|
||||
itemInfo.DetectType(combinedPath+file.Name(), false)
|
||||
itemInfo.DetectType(fullCombined, false)
|
||||
itemInfo.Size = file.Size()
|
||||
fileInfos = append(fileInfos, *itemInfo)
|
||||
totalSize += itemInfo.Size
|
||||
si.NumFiles++
|
||||
idx.NumFiles++
|
||||
}
|
||||
}
|
||||
if totalSize == 0 && idx.Source.Config.IgnoreZeroSizeFolders {
|
||||
return nil
|
||||
}
|
||||
// Create FileInfo for the current directory
|
||||
dirFileInfo := &FileInfo{
|
||||
Path: adjustedPath,
|
||||
@@ -155,22 +189,23 @@ func (si *Index) indexDirectory(adjustedPath string, quick, recursive bool) erro
|
||||
dirFileInfo.SortItems()
|
||||
|
||||
// Update the current directory metadata in the index
|
||||
si.UpdateMetadata(dirFileInfo)
|
||||
idx.UpdateMetadata(dirFileInfo)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (si *Index) makeIndexPath(subPath string) string {
|
||||
func (idx *Index) makeIndexPath(subPath string) string {
|
||||
if strings.HasPrefix(subPath, "./") {
|
||||
subPath = strings.TrimPrefix(subPath, ".")
|
||||
}
|
||||
if si.Root == subPath || subPath == "." {
|
||||
if idx.Source.Path == subPath || subPath == "." {
|
||||
return "/"
|
||||
}
|
||||
// clean path
|
||||
subPath = strings.TrimSuffix(subPath, "/")
|
||||
// remove index prefix
|
||||
adjustedPath := strings.TrimPrefix(subPath, si.Root)
|
||||
adjustedPath := strings.TrimPrefix(subPath, idx.Source.Path)
|
||||
adjustedPath = strings.ReplaceAll(adjustedPath, "\\", "/")
|
||||
// remove trailing slash
|
||||
adjustedPath = strings.TrimSuffix(adjustedPath, "/")
|
||||
if !strings.HasPrefix(adjustedPath, "/") {
|
||||
@@ -179,43 +214,64 @@ func (si *Index) makeIndexPath(subPath string) string {
|
||||
return adjustedPath
|
||||
}
|
||||
|
||||
func (si *Index) recursiveUpdateDirSizes(childInfo *FileInfo, previousSize int64) {
|
||||
func (idx *Index) recursiveUpdateDirSizes(childInfo *FileInfo, previousSize int64) {
|
||||
parentDir := utils.GetParentDirectoryPath(childInfo.Path)
|
||||
parentInfo, exists := si.GetMetadataInfo(parentDir, true)
|
||||
parentInfo, exists := idx.GetMetadataInfo(parentDir, true)
|
||||
if !exists || parentDir == "" {
|
||||
return
|
||||
}
|
||||
newSize := parentInfo.Size - previousSize + childInfo.Size
|
||||
parentInfo.Size += newSize
|
||||
si.UpdateMetadata(parentInfo)
|
||||
si.recursiveUpdateDirSizes(parentInfo, newSize)
|
||||
idx.UpdateMetadata(parentInfo)
|
||||
idx.recursiveUpdateDirSizes(parentInfo, newSize)
|
||||
}
|
||||
|
||||
func (si *Index) RefreshFileInfo(opts FileOptions) error {
|
||||
func (idx *Index) GetRealPath(relativePath ...string) (string, bool, error) {
|
||||
combined := append([]string{idx.Source.Path}, relativePath...)
|
||||
joinedPath := filepath.Join(combined...)
|
||||
isDir, _ := cache.RealPath.Get(joinedPath + ":isdir").(bool)
|
||||
cached, ok := cache.RealPath.Get(joinedPath).(string)
|
||||
if ok && cached != "" {
|
||||
return cached, isDir, nil
|
||||
}
|
||||
// Convert relative path to absolute path
|
||||
absolutePath, err := filepath.Abs(joinedPath)
|
||||
if err != nil {
|
||||
return absolutePath, false, fmt.Errorf("could not get real path: %v, %s", joinedPath, err)
|
||||
}
|
||||
// Resolve symlinks and get the real path
|
||||
realPath, isDir, err := resolveSymlinks(absolutePath)
|
||||
if err == nil {
|
||||
cache.RealPath.Set(joinedPath, realPath)
|
||||
cache.RealPath.Set(joinedPath+":isdir", isDir)
|
||||
}
|
||||
return realPath, isDir, err
|
||||
}
|
||||
|
||||
func (idx *Index) RefreshFileInfo(opts FileOptions) error {
|
||||
refreshOptions := FileOptions{
|
||||
Path: opts.Path,
|
||||
IsDir: opts.IsDir,
|
||||
}
|
||||
|
||||
if !refreshOptions.IsDir {
|
||||
refreshOptions.Path = si.makeIndexPath(filepath.Dir(refreshOptions.Path))
|
||||
refreshOptions.Path = idx.makeIndexPath(filepath.Dir(refreshOptions.Path))
|
||||
refreshOptions.IsDir = true
|
||||
} else {
|
||||
refreshOptions.Path = si.makeIndexPath(refreshOptions.Path)
|
||||
refreshOptions.Path = idx.makeIndexPath(refreshOptions.Path)
|
||||
}
|
||||
err := si.indexDirectory(refreshOptions.Path, false, false)
|
||||
err := idx.indexDirectory(refreshOptions.Path, false, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file/folder does not exist to refresh data: %s", refreshOptions.Path)
|
||||
}
|
||||
file, exists := si.GetMetadataInfo(refreshOptions.Path, true)
|
||||
file, exists := idx.GetMetadataInfo(refreshOptions.Path, true)
|
||||
if !exists {
|
||||
return fmt.Errorf("file/folder does not exist in metadata: %s", refreshOptions.Path)
|
||||
}
|
||||
|
||||
current, firstExisted := si.GetMetadataInfo(refreshOptions.Path, true)
|
||||
current, firstExisted := idx.GetMetadataInfo(refreshOptions.Path, true)
|
||||
refreshParentInfo := firstExisted && current.Size != file.Size
|
||||
//utils.PrintStructFields(*file)
|
||||
result := si.UpdateMetadata(file)
|
||||
result := idx.UpdateMetadata(file)
|
||||
if !result {
|
||||
return fmt.Errorf("file/folder does not exist in metadata: %s", refreshOptions.Path)
|
||||
}
|
||||
@@ -223,7 +279,72 @@ func (si *Index) RefreshFileInfo(opts FileOptions) error {
|
||||
return nil
|
||||
}
|
||||
if refreshParentInfo {
|
||||
si.recursiveUpdateDirSizes(file, current.Size)
|
||||
idx.recursiveUpdateDirSizes(file, current.Size)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isHidden(file os.FileInfo, srcPath string) bool {
|
||||
// Check if the file starts with a dot (common on Unix systems)
|
||||
if file.Name()[0] == '.' {
|
||||
return true
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
return checkWindowsHidden(filepath.Join(srcPath, file.Name()))
|
||||
}
|
||||
|
||||
// Default behavior for non-Windows systems
|
||||
return false
|
||||
}
|
||||
|
||||
func (idx *Index) shouldSkip(isDir bool, isHidden bool, fullCombined string) bool {
|
||||
// check inclusions first
|
||||
if isDir && len(idx.Source.Config.Include.Folders) > 0 {
|
||||
if !slices.Contains(idx.Source.Config.Include.Folders, fullCombined) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if !isDir && len(idx.Source.Config.Include.Files) > 0 {
|
||||
if !slices.Contains(idx.Source.Config.Include.Files, fullCombined) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if !isDir && len(idx.Source.Config.Include.FileEndsWith) > 0 {
|
||||
shouldSkip := true
|
||||
for _, end := range idx.Source.Config.Include.FileEndsWith {
|
||||
if strings.HasSuffix(fullCombined, end) {
|
||||
shouldSkip = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if shouldSkip {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// check exclusions
|
||||
if isDir && slices.Contains(idx.Source.Config.Exclude.Folders, fullCombined) {
|
||||
return true
|
||||
}
|
||||
if !isDir && slices.Contains(idx.Source.Config.Exclude.Files, fullCombined) {
|
||||
return true
|
||||
}
|
||||
if idx.Source.Config.IgnoreHidden && isHidden {
|
||||
return true
|
||||
}
|
||||
|
||||
if !isDir && len(idx.Source.Config.Exclude.FileEndsWith) > 0 {
|
||||
shouldSkip := false
|
||||
for _, end := range idx.Source.Config.Exclude.FileEndsWith {
|
||||
if strings.HasSuffix(fullCombined, end) {
|
||||
shouldSkip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return shouldSkip
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"log"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
)
|
||||
|
||||
// schedule in minutes
|
||||
@@ -19,99 +19,108 @@ var scanSchedule = []time.Duration{
|
||||
4 * time.Hour, // 4 hours for quick scan & 20 hours for a full scan
|
||||
}
|
||||
|
||||
func (si *Index) newScanner(origin string) {
|
||||
func (idx *Index) newScanner(origin string) {
|
||||
fullScanAnchor := 3
|
||||
fullScanCounter := 0 // every 5th scan is a full scan
|
||||
for {
|
||||
// Determine sleep time with modifiers
|
||||
fullScanCounter++
|
||||
sleepTime := scanSchedule[si.currentSchedule] + si.SmartModifier
|
||||
if si.assessment == "simple" {
|
||||
sleepTime = scanSchedule[si.currentSchedule] - si.SmartModifier
|
||||
sleepTime := scanSchedule[idx.currentSchedule] + idx.SmartModifier
|
||||
if idx.assessment == "simple" {
|
||||
sleepTime = scanSchedule[idx.currentSchedule] - idx.SmartModifier
|
||||
}
|
||||
if settings.Config.Server.IndexingInterval > 0 {
|
||||
sleepTime = time.Duration(settings.Config.Server.IndexingInterval) * time.Minute
|
||||
if idx.Source.Config.IndexingInterval > 0 {
|
||||
sleepTime = time.Duration(idx.Source.Config.IndexingInterval) * time.Minute
|
||||
}
|
||||
|
||||
// Log and sleep before indexing
|
||||
log.Printf("Next scan in %v\n", sleepTime)
|
||||
logger.Debug(fmt.Sprintf("Next scan in %v", sleepTime))
|
||||
time.Sleep(sleepTime)
|
||||
|
||||
si.scannerMu.Lock()
|
||||
idx.scannerMu.Lock()
|
||||
if fullScanCounter == 5 {
|
||||
si.RunIndexing(origin, false) // Full scan
|
||||
idx.RunIndexing(origin, false) // Full scan
|
||||
fullScanCounter = 0
|
||||
} else {
|
||||
si.RunIndexing(origin, true) // Quick scan
|
||||
idx.RunIndexing(origin, true) // Quick scan
|
||||
}
|
||||
si.scannerMu.Unlock()
|
||||
idx.scannerMu.Unlock()
|
||||
|
||||
// Adjust schedule based on file changes
|
||||
if si.FilesChangedDuringIndexing {
|
||||
if idx.FilesChangedDuringIndexing {
|
||||
// Move to at least the full-scan anchor or reduce interval
|
||||
if si.currentSchedule > fullScanAnchor {
|
||||
si.currentSchedule = fullScanAnchor
|
||||
} else if si.currentSchedule > 0 {
|
||||
si.currentSchedule--
|
||||
if idx.currentSchedule > fullScanAnchor {
|
||||
idx.currentSchedule = fullScanAnchor
|
||||
} else if idx.currentSchedule > 0 {
|
||||
idx.currentSchedule--
|
||||
}
|
||||
} else {
|
||||
// Increment toward the longest interval if no changes
|
||||
if si.currentSchedule < len(scanSchedule)-1 {
|
||||
si.currentSchedule++
|
||||
if idx.currentSchedule < len(scanSchedule)-1 {
|
||||
idx.currentSchedule++
|
||||
}
|
||||
}
|
||||
if si.assessment == "simple" && si.currentSchedule > 3 {
|
||||
si.currentSchedule = 3
|
||||
if idx.assessment == "simple" && idx.currentSchedule > 3 {
|
||||
idx.currentSchedule = 3
|
||||
}
|
||||
// Ensure `currentSchedule` stays within bounds
|
||||
if si.currentSchedule < 0 {
|
||||
si.currentSchedule = 0
|
||||
} else if si.currentSchedule >= len(scanSchedule) {
|
||||
si.currentSchedule = len(scanSchedule) - 1
|
||||
if idx.currentSchedule < 0 {
|
||||
idx.currentSchedule = 0
|
||||
} else if idx.currentSchedule >= len(scanSchedule) {
|
||||
idx.currentSchedule = len(scanSchedule) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (si *Index) RunIndexing(origin string, quick bool) {
|
||||
prevNumDirs := si.NumDirs
|
||||
prevNumFiles := si.NumFiles
|
||||
func (idx *Index) RunIndexing(origin string, quick bool) {
|
||||
prevNumDirs := idx.NumDirs
|
||||
prevNumFiles := idx.NumFiles
|
||||
if quick {
|
||||
log.Println("Starting quick scan")
|
||||
logger.Debug(fmt.Sprintf("Starting quick scan for [%v]", idx.Source.Name))
|
||||
} else {
|
||||
log.Println("Starting full scan")
|
||||
si.NumDirs = 0
|
||||
si.NumFiles = 0
|
||||
logger.Debug(fmt.Sprintf("Starting full scan for [%v]", idx.Source.Name))
|
||||
idx.NumDirs = 0
|
||||
idx.NumFiles = 0
|
||||
}
|
||||
startTime := time.Now()
|
||||
si.FilesChangedDuringIndexing = false
|
||||
idx.FilesChangedDuringIndexing = false
|
||||
// Perform the indexing operation
|
||||
err := si.indexDirectory("/", quick, true)
|
||||
err := idx.indexDirectory("/", quick, true)
|
||||
if err != nil {
|
||||
log.Printf("Error during indexing: %v", err)
|
||||
logger.Error(fmt.Sprintf("Error during indexing: %v", err))
|
||||
}
|
||||
firstRun := idx.LastIndexed == time.Time{}
|
||||
// Update the LastIndexed time
|
||||
si.LastIndexed = time.Now()
|
||||
si.indexingTime = int(time.Since(startTime).Seconds())
|
||||
idx.LastIndexed = time.Now()
|
||||
idx.indexingTime = int(time.Since(startTime).Seconds())
|
||||
if !quick {
|
||||
// update smart indexing
|
||||
if si.indexingTime < 3 || si.NumDirs < 10000 {
|
||||
si.assessment = "simple"
|
||||
si.SmartModifier = 4 * time.Minute
|
||||
} else if si.indexingTime > 120 || si.NumDirs > 500000 {
|
||||
si.assessment = "complex"
|
||||
modifier := si.indexingTime / 10 // seconds
|
||||
si.SmartModifier = time.Duration(modifier) * time.Minute
|
||||
if idx.indexingTime < 3 || idx.NumDirs < 10000 {
|
||||
idx.assessment = "simple"
|
||||
idx.SmartModifier = 4 * time.Minute
|
||||
} else if idx.indexingTime > 120 || idx.NumDirs > 500000 {
|
||||
idx.assessment = "complex"
|
||||
modifier := idx.indexingTime / 10 // seconds
|
||||
idx.SmartModifier = time.Duration(modifier) * time.Minute
|
||||
} else {
|
||||
si.assessment = "normal"
|
||||
idx.assessment = "normal"
|
||||
}
|
||||
log.Printf("Index assessment : complexity=%v directories=%v files=%v \n", si.assessment, si.NumDirs, si.NumFiles)
|
||||
if si.NumDirs != prevNumDirs || si.NumFiles != prevNumFiles {
|
||||
si.FilesChangedDuringIndexing = true
|
||||
if firstRun {
|
||||
logger.Info(fmt.Sprintf("Index assessment : index=%v complexity=%v directories=%v files=%v", idx.Source.Name, idx.assessment, idx.NumDirs, idx.NumFiles))
|
||||
} else {
|
||||
logger.Debug(fmt.Sprintf("Index assessment : iindex=%v complexity=%v directories=%v files=%v", idx.Source.Name, idx.assessment, idx.NumDirs, idx.NumFiles))
|
||||
}
|
||||
if idx.NumDirs != prevNumDirs || idx.NumFiles != prevNumFiles {
|
||||
idx.FilesChangedDuringIndexing = true
|
||||
}
|
||||
}
|
||||
log.Printf("Time Spent Indexing : %v seconds\n", si.indexingTime)
|
||||
if firstRun {
|
||||
logger.Info(fmt.Sprintf("Time spent indexing [%v]: %v seconds", idx.Source.Name, idx.indexingTime))
|
||||
} else {
|
||||
logger.Debug(fmt.Sprintf("Time spent indexing [%v]: %v seconds", idx.Source.Name, idx.indexingTime))
|
||||
}
|
||||
}
|
||||
|
||||
func (si *Index) setupIndexingScanners() {
|
||||
go si.newScanner("/")
|
||||
func (idx *Index) setupIndexingScanners() {
|
||||
go idx.newScanner("/")
|
||||
}
|
||||
@@ -11,16 +11,19 @@ import (
|
||||
)
|
||||
|
||||
func BenchmarkFillIndex(b *testing.B) {
|
||||
InitializeIndex(false)
|
||||
si := GetIndex(settings.Config.Server.Root)
|
||||
Initialize(settings.Source{
|
||||
Name: "test",
|
||||
Path: "/srv",
|
||||
})
|
||||
idx := GetIndex("test")
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
si.createMockData(50, 3) // 1000 dirs, 3 files per dir
|
||||
idx.createMockData(50, 3) // 1000 dirs, 3 files per dir
|
||||
}
|
||||
}
|
||||
|
||||
func (si *Index) createMockData(numDirs, numFilesPerDir int) {
|
||||
func (idx *Index) createMockData(numDirs, numFilesPerDir int) {
|
||||
for i := 0; i < numDirs; i++ {
|
||||
dirPath := generateRandomPath(rand.Intn(3) + 1)
|
||||
files := []ItemInfo{} // Slice of FileInfo
|
||||
@@ -40,7 +43,7 @@ func (si *Index) createMockData(numDirs, numFilesPerDir int) {
|
||||
Files: files,
|
||||
}
|
||||
|
||||
si.UpdateMetadata(dirInfo)
|
||||
idx.UpdateMetadata(dirInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +81,7 @@ func TestMakeIndexPath(t *testing.T) {
|
||||
subPath string
|
||||
expected string
|
||||
}{
|
||||
// Linux
|
||||
{"Root path returns slash", "/", "/"},
|
||||
{"Dot-prefixed returns slash", ".", "/"},
|
||||
{"Double-dot prefix ignored", "./", "/"},
|
||||
@@ -91,11 +95,128 @@ func TestMakeIndexPath(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
si := &Index{Root: "/"}
|
||||
result := si.makeIndexPath(tt.subPath)
|
||||
idx := &Index{Source: settings.Source{Path: "/"}}
|
||||
result := idx.makeIndexPath(tt.subPath)
|
||||
if result != tt.expected {
|
||||
t.Errorf("makeIndexPath(%q) = %q; want %q", tt.name, result, tt.expected)
|
||||
t.Errorf("makeIndexPath(%q)\ngot %q\nwant %q", tt.name, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
tests = []struct {
|
||||
name string
|
||||
subPath string
|
||||
expected string
|
||||
}{
|
||||
// Windows
|
||||
{"Mixed slash", "/first\\second", "/first/second"},
|
||||
{"Windows slash", "\\first\\second", "/first/second"},
|
||||
{"Windows full path", "C:\\Users\\testfolder\\nestedfolder", "/testfolder/nestedfolder"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
idx := &Index{Source: settings.Source{Path: "C:\\Users"}}
|
||||
result := idx.makeIndexPath(tt.subPath)
|
||||
if result != tt.expected {
|
||||
t.Errorf("makeIndexPath(%q)\ngot %q\nwant %q", tt.name, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeIndexPathRoot(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
subPath string
|
||||
expected string
|
||||
}{
|
||||
// Linux
|
||||
{"Root path returns slash", "/rootpath", "/"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
idx := &Index{Source: settings.Source{Path: "/rootpath", Name: "default"}}
|
||||
result := idx.makeIndexPath(tt.subPath)
|
||||
if result != tt.expected {
|
||||
t.Errorf("makeIndexPath(%q)\ngot %q\nwant %q", tt.name, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCheckIndexExclude(b *testing.B) {
|
||||
tests := []struct {
|
||||
isDir bool
|
||||
isHidden bool
|
||||
fullPath string
|
||||
}{
|
||||
{false, false, "/test/.test"},
|
||||
{true, false, "/test/.test"},
|
||||
{true, true, "/test/.test"},
|
||||
{false, false, "/test/filepath"},
|
||||
{false, true, "/test/filepath"},
|
||||
{true, true, "/test/filepath"},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
idx := Index{
|
||||
Source: settings.Source{
|
||||
Name: "files",
|
||||
Config: settings.IndexConfig{
|
||||
IgnoreHidden: true,
|
||||
Exclude: settings.IndexFilter{
|
||||
Files: []string{"test", "filepath", ".test", ".filepath", "test", "filepath", ".test", ".filepath"},
|
||||
Folders: []string{"test", "filepath", ".test", ".filepath", "test", "filepath", ".test", ".filepath"},
|
||||
FileEndsWith: []string{".zip", ".tar", ".jpeg"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, v := range tests {
|
||||
idx.shouldSkip(v.isDir, v.isHidden, v.fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
func BenchmarkCheckIndexConditionsInclude(b *testing.B) {
|
||||
tests := []struct {
|
||||
isDir bool
|
||||
isHidden bool
|
||||
fullPath string
|
||||
}{
|
||||
{false, false, "/test/.test"},
|
||||
{true, false, "/test/.test"},
|
||||
{true, true, "/test/.test"},
|
||||
{false, false, "/test/filepath"},
|
||||
{false, true, "/test/filepath"},
|
||||
{true, true, "/test/filepath"},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
idx2 := Index{
|
||||
Source: settings.Source{
|
||||
Name: "files",
|
||||
Config: settings.IndexConfig{
|
||||
IgnoreHidden: true,
|
||||
Include: settings.IndexFilter{
|
||||
Files: []string{"test", "filepath", ".test", ".filepath", "test", "filepath", ".test", ".filepath"},
|
||||
Folders: []string{"test", "filepath", ".test", ".filepath", "test", "filepath", ".test", ".filepath"},
|
||||
FileEndsWith: []string{".zip", ".tar", ".jpeg"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, v := range tests {
|
||||
idx2.shouldSkip(v.isDir, v.isHidden, v.fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
package files
|
||||
|
||||
// This file contains code primarily sourced from::
|
||||
// github.com/kataras/iris
|
||||
|
||||
import (
|
||||
"mime"
|
||||
)
|
||||
|
||||
const (
|
||||
// ContentBinaryHeaderValue header value for binary data.
|
||||
ContentBinaryHeaderValue = "application/octet-stream"
|
||||
// ContentWebassemblyHeaderValue header value for web assembly files.
|
||||
ContentWebassemblyHeaderValue = "application/wasm"
|
||||
// ContentHTMLHeaderValue is the string of text/html response header's content type value.
|
||||
ContentHTMLHeaderValue = "text/html"
|
||||
// ContentJSONHeaderValue header value for JSON data.
|
||||
ContentJSONHeaderValue = "application/json"
|
||||
// ContentJSONProblemHeaderValue header value for JSON API problem error.
|
||||
// Read more at: https://tools.ietf.org/html/rfc7807
|
||||
ContentJSONProblemHeaderValue = "application/problem+json"
|
||||
// ContentXMLProblemHeaderValue header value for XML API problem error.
|
||||
// Read more at: https://tools.ietf.org/html/rfc7807
|
||||
ContentXMLProblemHeaderValue = "application/problem+xml"
|
||||
// ContentJavascriptHeaderValue header value for JSONP & Javascript data.
|
||||
ContentJavascriptHeaderValue = "text/javascript"
|
||||
// ContentTextHeaderValue header value for Text data.
|
||||
ContentTextHeaderValue = "text/plain"
|
||||
// ContentXMLHeaderValue header value for XML data.
|
||||
ContentXMLHeaderValue = "text/xml"
|
||||
// ContentXMLUnreadableHeaderValue obsolete header value for XML.
|
||||
ContentXMLUnreadableHeaderValue = "application/xml"
|
||||
// ContentMarkdownHeaderValue custom key/content type, the real is the text/html.
|
||||
ContentMarkdownHeaderValue = "text/markdown"
|
||||
// ContentYAMLHeaderValue header value for YAML data.
|
||||
ContentYAMLHeaderValue = "application/x-yaml"
|
||||
// ContentYAMLTextHeaderValue header value for YAML plain text.
|
||||
ContentYAMLTextHeaderValue = "text/yaml"
|
||||
// ContentProtobufHeaderValue header value for Protobuf messages data.
|
||||
ContentProtobufHeaderValue = "application/x-protobuf"
|
||||
// ContentMsgPackHeaderValue header value for MsgPack data.
|
||||
ContentMsgPackHeaderValue = "application/msgpack"
|
||||
// ContentMsgPack2HeaderValue alternative header value for MsgPack data.
|
||||
ContentMsgPack2HeaderValue = "application/x-msgpack"
|
||||
// ContentFormHeaderValue header value for post form data.
|
||||
ContentFormHeaderValue = "application/x-www-form-urlencoded"
|
||||
// ContentFormMultipartHeaderValue header value for post multipart form data.
|
||||
ContentFormMultipartHeaderValue = "multipart/form-data"
|
||||
// ContentMultipartRelatedHeaderValue header value for multipart related data.
|
||||
ContentMultipartRelatedHeaderValue = "multipart/related"
|
||||
// ContentGRPCHeaderValue Content-Type header value for gRPC.
|
||||
ContentGRPCHeaderValue = "application/grpc"
|
||||
)
|
||||
|
||||
var types = map[string]string{
|
||||
".3dm": "x-world/x-3dmf",
|
||||
".3dmf": "x-world/x-3dmf",
|
||||
".7z": "application/x-7z-compressed",
|
||||
".a": "application/octet-stream",
|
||||
".aab": "application/x-authorware-bin",
|
||||
".aam": "application/x-authorware-map",
|
||||
".aas": "application/x-authorware-seg",
|
||||
".abc": "text/vndabc",
|
||||
".ace": "application/x-ace-compressed",
|
||||
".acgi": "text/html",
|
||||
".afl": "video/animaflex",
|
||||
".ai": "application/postscript",
|
||||
".aif": "audio/aiff",
|
||||
".aifc": "audio/aiff",
|
||||
".aiff": "audio/aiff",
|
||||
".aim": "application/x-aim",
|
||||
".aip": "text/x-audiosoft-intra",
|
||||
".alz": "application/x-alz-compressed",
|
||||
".ani": "application/x-navi-animation",
|
||||
".aos": "application/x-nokia-9000-communicator-add-on-software",
|
||||
".aps": "application/mime",
|
||||
".apk": "application/vnd.android.package-archive",
|
||||
".arc": "application/x-arc-compressed",
|
||||
".arj": "application/arj",
|
||||
".art": "image/x-jg",
|
||||
".asf": "video/x-ms-asf",
|
||||
".asm": "text/x-asm",
|
||||
".asp": "text/asp",
|
||||
".asx": "application/x-mplayer2",
|
||||
".au": "audio/basic",
|
||||
".avi": "video/x-msvideo",
|
||||
".avs": "video/avs-video",
|
||||
".bcpio": "application/x-bcpio",
|
||||
".bin": "application/mac-binary",
|
||||
".bmp": "image/bmp",
|
||||
".boo": "application/book",
|
||||
".book": "application/book",
|
||||
".boz": "application/x-bzip2",
|
||||
".bsh": "application/x-bsh",
|
||||
".bz2": "application/x-bzip2",
|
||||
".bz": "application/x-bzip",
|
||||
".c++": ContentTextHeaderValue,
|
||||
".c": "text/x-c",
|
||||
".cab": "application/vnd.ms-cab-compressed",
|
||||
".cat": "application/vndms-pkiseccat",
|
||||
".cc": "text/x-c",
|
||||
".ccad": "application/clariscad",
|
||||
".cco": "application/x-cocoa",
|
||||
".cdf": "application/cdf",
|
||||
".cer": "application/pkix-cert",
|
||||
".cha": "application/x-chat",
|
||||
".chat": "application/x-chat",
|
||||
".chrt": "application/vnd.kde.kchart",
|
||||
".class": "application/java",
|
||||
".com": ContentTextHeaderValue,
|
||||
".conf": ContentTextHeaderValue,
|
||||
".cpio": "application/x-cpio",
|
||||
".cpp": "text/x-c",
|
||||
".cpt": "application/mac-compactpro",
|
||||
".crl": "application/pkcs-crl",
|
||||
".crt": "application/pkix-cert",
|
||||
".crx": "application/x-chrome-extension",
|
||||
".csh": "text/x-scriptcsh",
|
||||
".css": "text/css",
|
||||
".csv": "text/csv",
|
||||
".cxx": ContentTextHeaderValue,
|
||||
".dar": "application/x-dar",
|
||||
".dcr": "application/x-director",
|
||||
".deb": "application/x-debian-package",
|
||||
".deepv": "application/x-deepv",
|
||||
".def": ContentTextHeaderValue,
|
||||
".der": "application/x-x509-ca-cert",
|
||||
".dif": "video/x-dv",
|
||||
".dir": "application/x-director",
|
||||
".divx": "video/divx",
|
||||
".dl": "video/dl",
|
||||
".dmg": "application/x-apple-diskimage",
|
||||
".doc": "application/msword",
|
||||
".dot": "application/msword",
|
||||
".dp": "application/commonground",
|
||||
".drw": "application/drafting",
|
||||
".dump": "application/octet-stream",
|
||||
".dv": "video/x-dv",
|
||||
".dvi": "application/x-dvi",
|
||||
".dwf": "drawing/x-dwf=(old)",
|
||||
".dwg": "application/acad",
|
||||
".dxf": "application/dxf",
|
||||
".dxr": "application/x-director",
|
||||
".el": "text/x-scriptelisp",
|
||||
".elc": "application/x-bytecodeelisp=(compiled=elisp)",
|
||||
".eml": "message/rfc822",
|
||||
".env": "application/x-envoy",
|
||||
".eps": "application/postscript",
|
||||
".es": "application/x-esrehber",
|
||||
".etx": "text/x-setext",
|
||||
".evy": "application/envoy",
|
||||
".exe": "application/octet-stream",
|
||||
".f77": "text/x-fortran",
|
||||
".f90": "text/x-fortran",
|
||||
".f": "text/x-fortran",
|
||||
".fdf": "application/vndfdf",
|
||||
".fif": "application/fractals",
|
||||
".fli": "video/fli",
|
||||
".flo": "image/florian",
|
||||
".flv": "video/x-flv",
|
||||
".flx": "text/vndfmiflexstor",
|
||||
".fmf": "video/x-atomic3d-feature",
|
||||
".for": "text/x-fortran",
|
||||
".fpx": "image/vndfpx",
|
||||
".frl": "application/freeloader",
|
||||
".funk": "audio/make",
|
||||
".g3": "image/g3fax",
|
||||
".g": ContentTextHeaderValue,
|
||||
".gif": "image/gif",
|
||||
".gl": "video/gl",
|
||||
".gsd": "audio/x-gsm",
|
||||
".gsm": "audio/x-gsm",
|
||||
".gsp": "application/x-gsp",
|
||||
".gss": "application/x-gss",
|
||||
".gtar": "application/x-gtar",
|
||||
".gz": "application/x-compressed",
|
||||
".gzip": "application/x-gzip",
|
||||
".h": "text/x-h",
|
||||
".hdf": "application/x-hdf",
|
||||
".help": "application/x-helpfile",
|
||||
".hgl": "application/vndhp-hpgl",
|
||||
".hh": "text/x-h",
|
||||
".hlb": "text/x-script",
|
||||
".hlp": "application/hlp",
|
||||
".hpg": "application/vndhp-hpgl",
|
||||
".hpgl": "application/vndhp-hpgl",
|
||||
".hqx": "application/binhex",
|
||||
".hta": "application/hta",
|
||||
".htc": "text/x-component",
|
||||
".htm": "text/html",
|
||||
".html": "text/html",
|
||||
".htmls": "text/html",
|
||||
".htt": "text/webviewhtml",
|
||||
".htx": "text/html",
|
||||
".ice": "x-conference/x-cooltalk",
|
||||
".ico": "image/x-icon",
|
||||
".ics": "text/calendar",
|
||||
".icz": "text/calendar",
|
||||
".idc": ContentTextHeaderValue,
|
||||
".ief": "image/ief",
|
||||
".iefs": "image/ief",
|
||||
".iges": "application/iges",
|
||||
".igs": "application/iges",
|
||||
".ima": "application/x-ima",
|
||||
".imap": "application/x-httpd-imap",
|
||||
".inf": "application/inf",
|
||||
".ins": "application/x-internett-signup",
|
||||
".ip": "application/x-ip2",
|
||||
".isu": "video/x-isvideo",
|
||||
".it": "audio/it",
|
||||
".iv": "application/x-inventor",
|
||||
".ivr": "i-world/i-vrml",
|
||||
".ivy": "application/x-livescreen",
|
||||
".jam": "audio/x-jam",
|
||||
".jav": "text/x-java-source",
|
||||
".java": "text/x-java-source",
|
||||
".jcm": "application/x-java-commerce",
|
||||
".jfif-tbnl": "image/jpeg",
|
||||
".jfif": "image/jpeg",
|
||||
".jnlp": "application/x-java-jnlp-file",
|
||||
".jpe": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".jpg": "image/jpeg",
|
||||
".jps": "image/x-jps",
|
||||
".js": ContentJavascriptHeaderValue,
|
||||
".mjs": ContentJavascriptHeaderValue,
|
||||
".json": ContentJSONHeaderValue,
|
||||
".vue": ContentJavascriptHeaderValue,
|
||||
".jut": "image/jutvision",
|
||||
".kar": "audio/midi",
|
||||
".karbon": "application/vnd.kde.karbon",
|
||||
".kfo": "application/vnd.kde.kformula",
|
||||
".flw": "application/vnd.kde.kivio",
|
||||
".kml": "application/vnd.google-earth.kml+xml",
|
||||
".kmz": "application/vnd.google-earth.kmz",
|
||||
".kon": "application/vnd.kde.kontour",
|
||||
".kpr": "application/vnd.kde.kpresenter",
|
||||
".kpt": "application/vnd.kde.kpresenter",
|
||||
".ksp": "application/vnd.kde.kspread",
|
||||
".kwd": "application/vnd.kde.kword",
|
||||
".kwt": "application/vnd.kde.kword",
|
||||
".ksh": "text/x-scriptksh",
|
||||
".la": "audio/nspaudio",
|
||||
".lam": "audio/x-liveaudio",
|
||||
".latex": "application/x-latex",
|
||||
".lha": "application/lha",
|
||||
".lhx": "application/octet-stream",
|
||||
".list": ContentTextHeaderValue,
|
||||
".lma": "audio/nspaudio",
|
||||
".log": ContentTextHeaderValue,
|
||||
".lsp": "text/x-scriptlisp",
|
||||
".lst": ContentTextHeaderValue,
|
||||
".lsx": "text/x-la-asf",
|
||||
".ltx": "application/x-latex",
|
||||
".lzh": "application/octet-stream",
|
||||
".lzx": "application/lzx",
|
||||
".m1v": "video/mpeg",
|
||||
".m2a": "audio/mpeg",
|
||||
".m2v": "video/mpeg",
|
||||
".m3u": "audio/x-mpegurl",
|
||||
".m": "text/x-m",
|
||||
".man": "application/x-troff-man",
|
||||
".manifest": "text/cache-manifest",
|
||||
".map": "application/x-navimap",
|
||||
".mar": ContentTextHeaderValue,
|
||||
".mbd": "application/mbedlet",
|
||||
".mc$": "application/x-magic-cap-package-10",
|
||||
".mcd": "application/mcad",
|
||||
".mcf": "text/mcf",
|
||||
".mcp": "application/netmc",
|
||||
".me": "application/x-troff-me",
|
||||
".mht": "message/rfc822",
|
||||
".mhtml": "message/rfc822",
|
||||
".mid": "application/x-midi",
|
||||
".midi": "application/x-midi",
|
||||
".mif": "application/x-frame",
|
||||
".mime": "message/rfc822",
|
||||
".mjf": "audio/x-vndaudioexplosionmjuicemediafile",
|
||||
".mjpg": "video/x-motion-jpeg",
|
||||
".mm": "application/base64",
|
||||
".mme": "application/base64",
|
||||
".mod": "audio/mod",
|
||||
".moov": "video/quicktime",
|
||||
".mov": "video/quicktime",
|
||||
".movie": "video/x-sgi-movie",
|
||||
".mp2": "audio/mpeg",
|
||||
".mp3": "audio/mpeg",
|
||||
".mp4": "video/mp4",
|
||||
".mpa": "audio/mpeg",
|
||||
".mpc": "application/x-project",
|
||||
".mpe": "video/mpeg",
|
||||
".mpeg": "video/mpeg",
|
||||
".mpg": "video/mpeg",
|
||||
".mpga": "audio/mpeg",
|
||||
".mpp": "application/vndms-project",
|
||||
".mpt": "application/x-project",
|
||||
".mpv": "application/x-project",
|
||||
".mpx": "application/x-project",
|
||||
".mrc": "application/marc",
|
||||
".ms": "application/x-troff-ms",
|
||||
".mv": "video/x-sgi-movie",
|
||||
".my": "audio/make",
|
||||
".mzz": "application/x-vndaudioexplosionmzz",
|
||||
".nap": "image/naplps",
|
||||
".naplps": "image/naplps",
|
||||
".nc": "application/x-netcdf",
|
||||
".ncm": "application/vndnokiaconfiguration-message",
|
||||
".nif": "image/x-niff",
|
||||
".niff": "image/x-niff",
|
||||
".nix": "application/x-mix-transfer",
|
||||
".nsc": "application/x-conference",
|
||||
".nvd": "application/x-navidoc",
|
||||
".o": "application/octet-stream",
|
||||
".oda": "application/oda",
|
||||
".odb": "application/vnd.oasis.opendocument.database",
|
||||
".odc": "application/vnd.oasis.opendocument.chart",
|
||||
".odf": "application/vnd.oasis.opendocument.formula",
|
||||
".odg": "application/vnd.oasis.opendocument.graphics",
|
||||
".odi": "application/vnd.oasis.opendocument.image",
|
||||
".odm": "application/vnd.oasis.opendocument.text-master",
|
||||
".odp": "application/vnd.oasis.opendocument.presentation",
|
||||
".ods": "application/vnd.oasis.opendocument.spreadsheet",
|
||||
".odt": "application/vnd.oasis.opendocument.text",
|
||||
".oga": "audio/ogg",
|
||||
".ogg": "audio/ogg",
|
||||
".ogv": "video/ogg",
|
||||
".omc": "application/x-omc",
|
||||
".omcd": "application/x-omcdatamaker",
|
||||
".omcr": "application/x-omcregerator",
|
||||
".otc": "application/vnd.oasis.opendocument.chart-template",
|
||||
".otf": "application/vnd.oasis.opendocument.formula-template",
|
||||
".otg": "application/vnd.oasis.opendocument.graphics-template",
|
||||
".oth": "application/vnd.oasis.opendocument.text-web",
|
||||
".oti": "application/vnd.oasis.opendocument.image-template",
|
||||
".otm": "application/vnd.oasis.opendocument.text-master",
|
||||
".otp": "application/vnd.oasis.opendocument.presentation-template",
|
||||
".ots": "application/vnd.oasis.opendocument.spreadsheet-template",
|
||||
".ott": "application/vnd.oasis.opendocument.text-template",
|
||||
".p10": "application/pkcs10",
|
||||
".p12": "application/pkcs-12",
|
||||
".p7a": "application/x-pkcs7-signature",
|
||||
".p7c": "application/pkcs7-mime",
|
||||
".p7m": "application/pkcs7-mime",
|
||||
".p7r": "application/x-pkcs7-certreqresp",
|
||||
".p7s": "application/pkcs7-signature",
|
||||
".p": "text/x-pascal",
|
||||
".part": "application/pro_eng",
|
||||
".pas": "text/pascal",
|
||||
".pbm": "image/x-portable-bitmap",
|
||||
".pcl": "application/vndhp-pcl",
|
||||
".pct": "image/x-pict",
|
||||
".pcx": "image/x-pcx",
|
||||
".pdb": "chemical/x-pdb",
|
||||
".pdf": "application/pdf",
|
||||
".pfunk": "audio/make",
|
||||
".pgm": "image/x-portable-graymap",
|
||||
".pic": "image/pict",
|
||||
".pict": "image/pict",
|
||||
".pkg": "application/x-newton-compatible-pkg",
|
||||
".pko": "application/vndms-pkipko",
|
||||
".pl": "text/x-scriptperl",
|
||||
".plx": "application/x-pixclscript",
|
||||
".pm4": "application/x-pagemaker",
|
||||
".pm5": "application/x-pagemaker",
|
||||
".pm": "text/x-scriptperl-module",
|
||||
".png": "image/png",
|
||||
".pnm": "application/x-portable-anymap",
|
||||
".pot": "application/mspowerpoint",
|
||||
".pov": "model/x-pov",
|
||||
".ppa": "application/vndms-powerpoint",
|
||||
".ppm": "image/x-portable-pixmap",
|
||||
".pps": "application/mspowerpoint",
|
||||
".ppt": "application/mspowerpoint",
|
||||
".ppz": "application/mspowerpoint",
|
||||
".pre": "application/x-freelance",
|
||||
".prt": "application/pro_eng",
|
||||
".ps": "application/postscript",
|
||||
".psd": "application/octet-stream",
|
||||
".pvu": "paleovu/x-pv",
|
||||
".pwz": "application/vndms-powerpoint",
|
||||
".py": "text/x-scriptphyton",
|
||||
".pyc": "application/x-bytecodepython",
|
||||
".qcp": "audio/vndqcelp",
|
||||
".qd3": "x-world/x-3dmf",
|
||||
".qd3d": "x-world/x-3dmf",
|
||||
".qif": "image/x-quicktime",
|
||||
".qt": "video/quicktime",
|
||||
".qtc": "video/x-qtc",
|
||||
".qti": "image/x-quicktime",
|
||||
".qtif": "image/x-quicktime",
|
||||
".ra": "audio/x-pn-realaudio",
|
||||
".ram": "audio/x-pn-realaudio",
|
||||
".rar": "application/x-rar-compressed",
|
||||
".ras": "application/x-cmu-raster",
|
||||
".rast": "image/cmu-raster",
|
||||
".rexx": "text/x-scriptrexx",
|
||||
".rf": "image/vndrn-realflash",
|
||||
".rgb": "image/x-rgb",
|
||||
".rm": "application/vndrn-realmedia",
|
||||
".rmi": "audio/mid",
|
||||
".rmm": "audio/x-pn-realaudio",
|
||||
".rmp": "audio/x-pn-realaudio",
|
||||
".rng": "application/ringing-tones",
|
||||
".rnx": "application/vndrn-realplayer",
|
||||
".roff": "application/x-troff",
|
||||
".rp": "image/vndrn-realpix",
|
||||
".rpm": "audio/x-pn-realaudio-plugin",
|
||||
".rt": "text/vndrn-realtext",
|
||||
".rtf": "text/richtext",
|
||||
".rtx": "text/richtext",
|
||||
".rv": "video/vndrn-realvideo",
|
||||
".s": "text/x-asm",
|
||||
".s3m": "audio/s3m",
|
||||
".s7z": "application/x-7z-compressed",
|
||||
".saveme": "application/octet-stream",
|
||||
".sbk": "application/x-tbook",
|
||||
".scm": "text/x-scriptscheme",
|
||||
".sdml": ContentTextHeaderValue,
|
||||
".sdp": "application/sdp",
|
||||
".sdr": "application/sounder",
|
||||
".sea": "application/sea",
|
||||
".set": "application/set",
|
||||
".sgm": "text/x-sgml",
|
||||
".sgml": "text/x-sgml",
|
||||
".sh": "text/x-scriptsh",
|
||||
".shar": "application/x-bsh",
|
||||
".shtml": "text/x-server-parsed-html",
|
||||
".sid": "audio/x-psid",
|
||||
".skd": "application/x-koan",
|
||||
".skm": "application/x-koan",
|
||||
".skp": "application/x-koan",
|
||||
".skt": "application/x-koan",
|
||||
".sit": "application/x-stuffit",
|
||||
".sitx": "application/x-stuffitx",
|
||||
".sl": "application/x-seelogo",
|
||||
".smi": "application/smil",
|
||||
".smil": "application/smil",
|
||||
".snd": "audio/basic",
|
||||
".sol": "application/solids",
|
||||
".spc": "text/x-speech",
|
||||
".spl": "application/futuresplash",
|
||||
".spr": "application/x-sprite",
|
||||
".sprite": "application/x-sprite",
|
||||
".spx": "audio/ogg",
|
||||
".src": "application/x-wais-source",
|
||||
".srt": "text/plain",
|
||||
".sbv": "text/plain",
|
||||
".ssa": "text/plain",
|
||||
".ssi": "text/x-server-parsed-html",
|
||||
".ssm": "application/streamingmedia",
|
||||
".sst": "application/vndms-pkicertstore",
|
||||
".step": "application/step",
|
||||
".stl": "application/sla",
|
||||
".stp": "application/step",
|
||||
".sv4cpio": "application/x-sv4cpio",
|
||||
".sv4crc": "application/x-sv4crc",
|
||||
".svf": "image/vnddwg",
|
||||
".svg": "image/svg+xml",
|
||||
".svr": "application/x-world",
|
||||
".swf": "application/x-shockwave-flash",
|
||||
".t": "application/x-troff",
|
||||
".talk": "text/x-speech",
|
||||
".tar": "application/x-tar",
|
||||
".tbk": "application/toolbook",
|
||||
".tcl": "text/x-scripttcl",
|
||||
".tcsh": "text/x-scripttcsh",
|
||||
".tex": "application/x-tex",
|
||||
".texi": "application/x-texinfo",
|
||||
".texinfo": "application/x-texinfo",
|
||||
".text": ContentTextHeaderValue,
|
||||
".tgz": "application/gnutar",
|
||||
".tif": "image/tiff",
|
||||
".tiff": "image/tiff",
|
||||
".tr": "application/x-troff",
|
||||
".tsi": "audio/tsp-audio",
|
||||
".tsp": "application/dsptype",
|
||||
".tsv": "text/tab-separated-values",
|
||||
".turbot": "image/florian",
|
||||
".txt": ContentTextHeaderValue,
|
||||
".uil": "text/x-uil",
|
||||
".uni": "text/uri-list",
|
||||
".unis": "text/uri-list",
|
||||
".unv": "application/i-deas",
|
||||
".uri": "text/uri-list",
|
||||
".uris": "text/uri-list",
|
||||
".ustar": "application/x-ustar",
|
||||
".uu": "text/x-uuencode",
|
||||
".uue": "text/x-uuencode",
|
||||
".vcd": "application/x-cdlink",
|
||||
".vcf": "text/x-vcard",
|
||||
".vcard": "text/x-vcard",
|
||||
".vcs": "text/x-vcalendar",
|
||||
".vda": "application/vda",
|
||||
".vdo": "video/vdo",
|
||||
".vew": "application/groupwise",
|
||||
".viv": "video/vivo",
|
||||
".vivo": "video/vivo",
|
||||
".vmd": "application/vocaltec-media-desc",
|
||||
".vmf": "application/vocaltec-media-file",
|
||||
".voc": "audio/voc",
|
||||
".vos": "video/vosaic",
|
||||
".vox": "audio/voxware",
|
||||
".vqe": "audio/x-twinvq-plugin",
|
||||
".vqf": "audio/x-twinvq",
|
||||
".vql": "audio/x-twinvq-plugin",
|
||||
".vrml": "application/x-vrml",
|
||||
".vrt": "x-world/x-vrt",
|
||||
".vsd": "application/x-visio",
|
||||
".vst": "application/x-visio",
|
||||
".vsw": "application/x-visio",
|
||||
".w60": "application/wordperfect60",
|
||||
".w61": "application/wordperfect61",
|
||||
".w6w": "application/msword",
|
||||
".wav": "audio/wav",
|
||||
".wb1": "application/x-qpro",
|
||||
".wbmp": "image/vnd.wap.wbmp",
|
||||
".web": "application/vndxara",
|
||||
".wiz": "application/msword",
|
||||
".wk1": "application/x-123",
|
||||
".wmf": "windows/metafile",
|
||||
".wml": "text/vnd.wap.wml",
|
||||
".wmlc": "application/vnd.wap.wmlc",
|
||||
".wmls": "text/vnd.wap.wmlscript",
|
||||
".wmlsc": "application/vnd.wap.wmlscriptc",
|
||||
".word": "application/msword",
|
||||
".wp5": "application/wordperfect",
|
||||
".wp6": "application/wordperfect",
|
||||
".wp": "application/wordperfect",
|
||||
".wpd": "application/wordperfect",
|
||||
".wq1": "application/x-lotus",
|
||||
".wri": "application/mswrite",
|
||||
".wrl": "application/x-world",
|
||||
".wrz": "model/vrml",
|
||||
".wsc": "text/scriplet",
|
||||
".wsrc": "application/x-wais-source",
|
||||
".wtk": "application/x-wintalk",
|
||||
".x-png": "image/png",
|
||||
".xbm": "image/x-xbitmap",
|
||||
".xdr": "video/x-amt-demorun",
|
||||
".xgz": "xgl/drawing",
|
||||
".xif": "image/vndxiff",
|
||||
".xl": "application/excel",
|
||||
".xla": "application/excel",
|
||||
".xlb": "application/excel",
|
||||
".xlc": "application/excel",
|
||||
".xld": "application/excel",
|
||||
".xlk": "application/excel",
|
||||
".xll": "application/excel",
|
||||
".xlm": "application/excel",
|
||||
".xls": "application/excel",
|
||||
".xlt": "application/excel",
|
||||
".xlv": "application/excel",
|
||||
".xlw": "application/excel",
|
||||
".xm": "audio/xm",
|
||||
".xml": ContentXMLHeaderValue,
|
||||
".xmz": "xgl/movie",
|
||||
".xpix": "application/x-vndls-xpix",
|
||||
".xpm": "image/x-xpixmap",
|
||||
".xsr": "video/x-amt-showrun",
|
||||
".xwd": "image/x-xwd",
|
||||
".xyz": "chemical/x-pdb",
|
||||
".z": "application/x-compress",
|
||||
".zip": "application/zip",
|
||||
".zoo": "application/octet-stream",
|
||||
".zsh": "text/x-scriptzsh",
|
||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".docm": "application/vnd.ms-word.document.macroEnabled.12",
|
||||
".dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
|
||||
".dotm": "application/vnd.ms-word.template.macroEnabled.12",
|
||||
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
".xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12",
|
||||
".xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
||||
".xltm": "application/vnd.ms-excel.template.macroEnabled.12",
|
||||
".xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
|
||||
".xlam": "application/vnd.ms-excel.addin.macroEnabled.12",
|
||||
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
".pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
|
||||
".ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
|
||||
".ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
|
||||
".potx": "application/vnd.openxmlformats-officedocument.presentationml.template",
|
||||
".potm": "application/vnd.ms-powerpoint.template.macroEnabled.12",
|
||||
".ppam": "application/vnd.ms-powerpoint.addin.macroEnabled.12",
|
||||
".sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide",
|
||||
".sldm": "application/vnd.ms-powerpoint.slide.macroEnabled.12",
|
||||
".thmx": "application/vnd.ms-officetheme",
|
||||
".onetoc": "application/onenote",
|
||||
".onetoc2": "application/onenote",
|
||||
".onetmp": "application/onenote",
|
||||
".onepkg": "application/onenote",
|
||||
".xpi": "application/x-xpinstall",
|
||||
".wasm": "application/wasm",
|
||||
".m4a": "audio/mp4",
|
||||
".flac": "audio/x-flac",
|
||||
".amr": "audio/amr",
|
||||
".aac": "audio/aac",
|
||||
".opus": "video/ogg",
|
||||
".m4v": "video/mp4",
|
||||
".mkv": "video/x-matroska",
|
||||
".caf": "audio/x-caf",
|
||||
".m3u8": "application/x-mpegURL",
|
||||
".mpd": "application/dash+xml",
|
||||
".webp": "image/webp",
|
||||
".epub": "application/epub+zip",
|
||||
}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
for ext, typ := range types {
|
||||
// skip errors
|
||||
_ = mime.AddExtensionType(ext, typ)
|
||||
}
|
||||
}
|
||||
+17
-16
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/cache"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
@@ -20,21 +21,21 @@ type SearchResult struct {
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
func (si *Index) Search(search string, scope string, sourceSession string) []SearchResult {
|
||||
func (idx *Index) Search(search string, scope string, sourceSession string) []SearchResult {
|
||||
// Remove slashes
|
||||
scope = si.makeIndexPath(scope)
|
||||
runningHash := utils.GenerateRandomHash(4)
|
||||
scope = idx.makeIndexPath(scope)
|
||||
runningHash := utils.InsecureRandomIdentifier(4)
|
||||
sessionInProgress.Store(sourceSession, runningHash) // Store the value in the sync.Map
|
||||
searchOptions := ParseSearch(search)
|
||||
results := make(map[string]SearchResult, 0)
|
||||
count := 0
|
||||
var directories []string
|
||||
cachedDirs, ok := utils.SearchResultsCache.Get(si.Root + scope).([]string)
|
||||
cachedDirs, ok := cache.SearchResults.Get(idx.Source.Path + scope).([]string)
|
||||
if ok {
|
||||
directories = cachedDirs
|
||||
} else {
|
||||
directories = si.getDirsInScope(scope)
|
||||
utils.SearchResultsCache.Set(si.Root+scope, directories)
|
||||
directories = idx.getDirsInScope(scope)
|
||||
cache.SearchResults.Set(idx.Source.Path+scope, directories)
|
||||
}
|
||||
for _, searchTerm := range searchOptions.Terms {
|
||||
if searchTerm == "" {
|
||||
@@ -43,12 +44,12 @@ func (si *Index) Search(search string, scope string, sourceSession string) []Sea
|
||||
if count > maxSearchResults {
|
||||
break
|
||||
}
|
||||
si.mu.Lock()
|
||||
idx.mu.Lock()
|
||||
for _, dirName := range directories {
|
||||
scopedPath := strings.TrimPrefix(strings.TrimPrefix(dirName, scope), "/") + "/"
|
||||
si.mu.Unlock()
|
||||
dir, found := si.GetReducedMetadata(dirName, true)
|
||||
si.mu.Lock()
|
||||
idx.mu.Unlock()
|
||||
dir, found := idx.GetReducedMetadata(dirName, true)
|
||||
idx.mu.Lock()
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
@@ -74,7 +75,7 @@ func (si *Index) Search(search string, scope string, sourceSession string) []Sea
|
||||
}
|
||||
value, found := sessionInProgress.Load(sourceSession)
|
||||
if !found || value != runningHash {
|
||||
si.mu.Unlock()
|
||||
idx.mu.Unlock()
|
||||
return []SearchResult{}
|
||||
}
|
||||
if count > maxSearchResults {
|
||||
@@ -87,7 +88,7 @@ func (si *Index) Search(search string, scope string, sourceSession string) []Sea
|
||||
}
|
||||
}
|
||||
}
|
||||
si.mu.Unlock()
|
||||
idx.mu.Unlock()
|
||||
}
|
||||
|
||||
// Sort keys based on the number of elements in the path after splitting by "/"
|
||||
@@ -169,11 +170,11 @@ func (fi ItemInfo) containsSearchTerm(searchTerm string, options SearchOptions)
|
||||
return true
|
||||
}
|
||||
|
||||
func (si *Index) getDirsInScope(scope string) []string {
|
||||
func (idx *Index) getDirsInScope(scope string) []string {
|
||||
newList := []string{}
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
for k := range si.Directories {
|
||||
idx.mu.Lock()
|
||||
defer idx.mu.Unlock()
|
||||
for k := range idx.Directories {
|
||||
if strings.HasPrefix(k, scope) || scope == "" {
|
||||
newList = append(newList, k)
|
||||
}
|
||||
|
||||
@@ -4,14 +4,15 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func BenchmarkSearchAllIndexes(b *testing.B) {
|
||||
InitializeIndex(false)
|
||||
si := GetIndex(rootPath)
|
||||
Initialize(settings.Source{Name: "test", Path: "/srv"})
|
||||
idx := GetIndex("test")
|
||||
|
||||
si.createMockData(50, 3) // 50 dirs, 3 files per dir
|
||||
idx.createMockData(50, 3) // 50 dirs, 3 files per dir
|
||||
|
||||
// Generate 100 random search terms
|
||||
searchTerms := generateRandomSearchTerms(100)
|
||||
@@ -21,7 +22,7 @@ func BenchmarkSearchAllIndexes(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Execute the SearchAllIndexes function
|
||||
for _, term := range searchTerms {
|
||||
si.Search(term, "/", "test")
|
||||
idx.Search(term, "/", "test")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,14 +75,14 @@ func TestParseSearch(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSearchWhileIndexing(t *testing.T) {
|
||||
InitializeIndex(false)
|
||||
si := GetIndex(rootPath)
|
||||
Initialize(settings.Source{Name: "test", Path: "/srv"})
|
||||
idx := GetIndex("test")
|
||||
|
||||
searchTerms := generateRandomSearchTerms(10)
|
||||
for i := 0; i < 5; i++ {
|
||||
go si.createMockData(100, 100) // Creating mock data concurrently
|
||||
go idx.createMockData(100, 100) // Creating mock data concurrently
|
||||
for _, term := range searchTerms {
|
||||
go si.Search(term, "/", "test") // Search concurrently
|
||||
go idx.Search(term, "/", "test") // Search concurrently
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-39
@@ -7,22 +7,22 @@ import (
|
||||
)
|
||||
|
||||
// UpdateFileMetadata updates the FileInfo for the specified directory in the index.
|
||||
func (si *Index) UpdateMetadata(info *FileInfo) bool {
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
si.Directories[info.Path] = info
|
||||
func (idx *Index) UpdateMetadata(info *FileInfo) bool {
|
||||
idx.mu.Lock()
|
||||
defer idx.mu.Unlock()
|
||||
idx.Directories[info.Path] = info
|
||||
return true
|
||||
}
|
||||
|
||||
// GetMetadataInfo retrieves the FileInfo from the specified directory in the index.
|
||||
func (si *Index) GetReducedMetadata(target string, isDir bool) (*FileInfo, bool) {
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
checkDir := si.makeIndexPath(target)
|
||||
func (idx *Index) GetReducedMetadata(target string, isDir bool) (*FileInfo, bool) {
|
||||
idx.mu.Lock()
|
||||
defer idx.mu.Unlock()
|
||||
checkDir := idx.makeIndexPath(target)
|
||||
if !isDir {
|
||||
checkDir = si.makeIndexPath(filepath.Dir(target))
|
||||
checkDir = idx.makeIndexPath(filepath.Dir(target))
|
||||
}
|
||||
dir, exists := si.Directories[checkDir]
|
||||
dir, exists := idx.Directories[checkDir]
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
@@ -48,42 +48,32 @@ func (si *Index) GetReducedMetadata(target string, isDir bool) (*FileInfo, bool)
|
||||
}
|
||||
|
||||
// GetMetadataInfo retrieves the FileInfo from the specified directory in the index.
|
||||
func (si *Index) GetMetadataInfo(target string, isDir bool) (*FileInfo, bool) {
|
||||
si.mu.RLock()
|
||||
defer si.mu.RUnlock()
|
||||
checkDir := si.makeIndexPath(target)
|
||||
func (idx *Index) GetMetadataInfo(target string, isDir bool) (*FileInfo, bool) {
|
||||
idx.mu.RLock()
|
||||
defer idx.mu.RUnlock()
|
||||
checkDir := idx.makeIndexPath(target)
|
||||
if !isDir {
|
||||
checkDir = si.makeIndexPath(filepath.Dir(target))
|
||||
checkDir = idx.makeIndexPath(filepath.Dir(target))
|
||||
}
|
||||
dir, exists := si.Directories[checkDir]
|
||||
dir, exists := idx.Directories[checkDir]
|
||||
return dir, exists
|
||||
}
|
||||
|
||||
func (si *Index) RemoveDirectory(path string) {
|
||||
si.mu.Lock()
|
||||
defer si.mu.Unlock()
|
||||
si.NumDeleted++
|
||||
delete(si.Directories, path)
|
||||
func (idx *Index) RemoveDirectory(path string) {
|
||||
idx.mu.Lock()
|
||||
defer idx.mu.Unlock()
|
||||
idx.NumDeleted++
|
||||
delete(idx.Directories, path)
|
||||
}
|
||||
|
||||
func GetIndex(root string) *Index {
|
||||
for _, index := range indexes {
|
||||
if index.Root == root {
|
||||
return index
|
||||
func GetIndex(name string) *Index {
|
||||
indexesMutex.Lock()
|
||||
defer indexesMutex.Unlock()
|
||||
index, ok := indexes[name]
|
||||
if !ok {
|
||||
return &Index{
|
||||
Source: settings.Source{Name: "default", Path: "."},
|
||||
}
|
||||
}
|
||||
if settings.Config.Server.Root != "" {
|
||||
rootPath = settings.Config.Server.Root
|
||||
}
|
||||
newIndex := &Index{
|
||||
Root: rootPath,
|
||||
Directories: map[string]*FileInfo{},
|
||||
NumDirs: 0,
|
||||
NumFiles: 0,
|
||||
}
|
||||
newIndex.Directories["/"] = &FileInfo{}
|
||||
indexesMutex.Lock()
|
||||
indexes = append(indexes, newIndex)
|
||||
indexesMutex.Unlock()
|
||||
return newIndex
|
||||
return index
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package files
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -207,7 +208,10 @@ func TestRemoveDirectory(t *testing.T) {
|
||||
|
||||
func init() {
|
||||
testIndex = Index{
|
||||
Root: "/",
|
||||
Source: settings.Source{
|
||||
Path: "/",
|
||||
Name: "test",
|
||||
},
|
||||
NumFiles: 10,
|
||||
NumDirs: 5,
|
||||
Directories: map[string]*FileInfo{
|
||||
|
||||
@@ -26,9 +26,25 @@ func MoveFile(src, dst string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyFile copies a file from source to dest and returns
|
||||
// an error if any.
|
||||
// CopyFile copies a file or directory from source to dest and returns an error if any.
|
||||
func CopyFile(source, dest string) error {
|
||||
// Check if the source exists and whether it's a file or directory.
|
||||
info, err := os.Stat(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
// If the source is a directory, copy it recursively.
|
||||
return copyDirectory(source, dest)
|
||||
}
|
||||
|
||||
// If the source is a file, copy the file.
|
||||
return copySingleFile(source, dest)
|
||||
}
|
||||
|
||||
// copySingleFile handles copying a single file.
|
||||
func copySingleFile(source, dest string) error {
|
||||
// Open the source file.
|
||||
src, err := os.Open(source)
|
||||
if err != nil {
|
||||
@@ -36,7 +52,7 @@ func CopyFile(source, dest string) error {
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// Makes the directory needed to create the dst file.
|
||||
// Create the destination directory if needed.
|
||||
err = os.MkdirAll(filepath.Dir(dest), 0775) //nolint:gomnd
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -68,6 +84,43 @@ func CopyFile(source, dest string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyDirectory handles copying directories recursively.
|
||||
func copyDirectory(source, dest string) error {
|
||||
// Create the destination directory.
|
||||
err := os.MkdirAll(dest, 0775) //nolint:gomnd
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Read the contents of the source directory.
|
||||
entries, err := os.ReadDir(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Iterate over each entry in the directory.
|
||||
for _, entry := range entries {
|
||||
srcPath := filepath.Join(source, entry.Name())
|
||||
destPath := filepath.Join(dest, entry.Name())
|
||||
|
||||
if entry.IsDir() {
|
||||
// Recursively copy subdirectories.
|
||||
err = copyDirectory(srcPath, destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Copy files.
|
||||
err = copySingleFile(srcPath, destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommonPrefix returns the common directory path of provided files.
|
||||
func CommonPrefix(sep byte, paths ...string) string {
|
||||
// Handle special cases.
|
||||
|
||||
+13
-15
@@ -1,26 +1,26 @@
|
||||
module github.com/gtsteffaniak/filebrowser/backend
|
||||
|
||||
go 1.22.5
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.1
|
||||
|
||||
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/gabriel-vasile/mimetype v1.4.7
|
||||
github.com/goccy/go-yaml v1.15.7
|
||||
github.com/goccy/go-yaml v1.15.23
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
github.com/spf13/afero v1.11.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/swaggo/http-swagger v1.3.4
|
||||
github.com/swaggo/swag v1.16.4
|
||||
golang.org/x/crypto v0.30.0
|
||||
golang.org/x/image v0.23.0
|
||||
golang.org/x/text v0.21.0
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/image v0.24.0
|
||||
golang.org/x/sys v0.30.0
|
||||
golang.org/x/text v0.22.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -36,17 +36,15 @@ require (
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/swaggo/files v1.0.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.3.11 // indirect
|
||||
golang.org/x/net v0.32.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/tools v0.28.0 // indirect
|
||||
go.etcd.io/bbolt v1.4.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/tools v0.30.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
+24
-34
@@ -6,7 +6,6 @@ github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwv
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
|
||||
github.com/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.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=
|
||||
@@ -30,8 +29,6 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
@@ -49,8 +46,8 @@ github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9Z
|
||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/goccy/go-yaml v1.15.7 h1:L7XuKpd/A66X4w/dlk08lVfiIADdy79a1AzRoIefC98=
|
||||
github.com/goccy/go-yaml v1.15.7/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.15.23 h1:WS0GAX1uNPDLUvLkNU2vXq6oTnsmfVFocjQ/4qA48qo=
|
||||
github.com/goccy/go-yaml v1.15.23/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
@@ -67,8 +64,6 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
@@ -80,26 +75,21 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww=
|
||||
@@ -112,18 +102,18 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
|
||||
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.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
|
||||
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
|
||||
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
|
||||
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
|
||||
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -135,12 +125,12 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
|
||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.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=
|
||||
@@ -155,8 +145,8 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -165,13 +155,13 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
|
||||
+23
-15
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
libError "errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -12,11 +11,13 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
jwt "github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v4/request"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
@@ -45,9 +46,18 @@ func extractToken(r *http.Request) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
auth := r.URL.Query().Get("auth")
|
||||
if auth != "" {
|
||||
hasToken = true
|
||||
if strings.Count(auth, ".") == 2 {
|
||||
return auth, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Authorization header
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader != "" {
|
||||
|
||||
hasToken = true
|
||||
// Split the header to get "Bearer {token}"
|
||||
parts := strings.Split(authHeader, " ")
|
||||
@@ -57,14 +67,6 @@ func extractToken(r *http.Request) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
auth := r.URL.Query().Get("auth")
|
||||
if auth != "" {
|
||||
hasToken = true
|
||||
if strings.Count(auth, ".") == 2 {
|
||||
return auth, nil
|
||||
}
|
||||
}
|
||||
|
||||
if hasToken {
|
||||
return "", fmt.Errorf("invalid token provided")
|
||||
}
|
||||
@@ -73,8 +75,13 @@ func extractToken(r *http.Request) (string, error) {
|
||||
}
|
||||
|
||||
func loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !config.Auth.Methods.PasswordAuth.Enabled {
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
// currently only supports user/pass
|
||||
// Get the authentication method from the settings
|
||||
auther, err := store.Auth.Get(config.Auth.Method)
|
||||
auther, err := store.Auth.Get("password")
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -126,14 +133,14 @@ func signupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
user.Username = info.Username
|
||||
user.Password = info.Password
|
||||
|
||||
userHome, err := config.MakeUserDir(user.Username, user.Scope, config.Server.Root)
|
||||
userHome, err := config.MakeUserDir(user.Username, user.Scope, files.RootPaths["default"])
|
||||
if err != nil {
|
||||
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
||||
logger.Error(fmt.Sprintf("create user: failed to mkdir user home dir: [%s]", userHome))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
user.Scope = userHome
|
||||
log.Printf("new user: %s, home dir: [%s].", user.Username, userHome)
|
||||
logger.Debug(fmt.Sprintf("new user: %s, home dir: [%s].", user.Username, userHome))
|
||||
err = store.Users.Save(&user)
|
||||
if err == errors.ErrExist {
|
||||
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
|
||||
@@ -150,7 +157,8 @@ func renewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (in
|
||||
}
|
||||
|
||||
func printToken(w http.ResponseWriter, _ *http.Request, user *users.User) (int, error) {
|
||||
signed, err := makeSignedTokenAPI(user, "WEB_TOKEN_"+utils.GenerateRandomHash(4), time.Hour*2, user.Perm)
|
||||
|
||||
signed, err := makeSignedTokenAPI(user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), time.Hour*time.Duration(config.Auth.TokenExpirationHours), user.Perm)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "key already exists with same name") {
|
||||
return http.StatusConflict, err
|
||||
|
||||
+71
-32
@@ -4,14 +4,14 @@ import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
jwt "github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/runner"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
@@ -20,7 +20,8 @@ import (
|
||||
type requestContext struct {
|
||||
user *users.User
|
||||
*runner.Runner
|
||||
raw interface{}
|
||||
raw interface{}
|
||||
token string
|
||||
}
|
||||
|
||||
type HttpResponse struct {
|
||||
@@ -59,11 +60,10 @@ func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
|
||||
// Get file information with options
|
||||
file, err := FileInfoFasterFunc(files.FileOptions{
|
||||
Path: filepath.Join(user.Scope, link.Path+"/"+path),
|
||||
Modify: user.Perm.Modify,
|
||||
Expand: true,
|
||||
ReadHeader: config.Server.TypeDetectionByHeader,
|
||||
Checker: user, // Call your checker function here
|
||||
Path: filepath.Join(user.Scope, link.Path+"/"+path),
|
||||
Modify: user.Perm.Modify,
|
||||
Expand: true,
|
||||
Checker: user, // Call your checker function here
|
||||
})
|
||||
file.Token = link.Token
|
||||
if err != nil {
|
||||
@@ -94,15 +94,44 @@ func withAdminHelper(fn handleFunc) handleFunc {
|
||||
// Middleware to retrieve and authenticate user
|
||||
func withUserHelper(fn handleFunc) handleFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
|
||||
if settings.Config.Auth.Method == "noauth" {
|
||||
if config.Auth.Methods.NoAuth {
|
||||
var err error
|
||||
// Retrieve the user from the store and store it in the context
|
||||
data.user, err = store.Users.Get(config.Server.Root, "admin")
|
||||
data.user, err = store.Users.Get(files.RootPaths["default"], uint(1))
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("no auth: %v", err))
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
return fn(w, r, data)
|
||||
}
|
||||
proxyUser := r.Header.Get(config.Auth.Methods.ProxyAuth.Header)
|
||||
if config.Auth.Methods.ProxyAuth.Enabled && proxyUser != "" {
|
||||
var err error
|
||||
// Retrieve the user from the store and store it in the context
|
||||
data.user, err = store.Users.Get(files.RootPaths["default"], proxyUser)
|
||||
if err != nil {
|
||||
if err.Error() != "the resource does not exist" {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
if config.Auth.Methods.ProxyAuth.CreateUser {
|
||||
newUser := settings.ApplyUserDefaults(users.User{
|
||||
Username: proxyUser,
|
||||
})
|
||||
err := store.Users.Save(&newUser)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
data.user, err = store.Users.Get(files.RootPaths["default"], proxyUser)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
} else {
|
||||
return http.StatusUnauthorized, fmt.Errorf("proxy authentication failed - no user found")
|
||||
}
|
||||
}
|
||||
setUserInResponseWriter(w, data.user)
|
||||
return fn(w, r, data)
|
||||
}
|
||||
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
||||
return config.Auth.Key, nil
|
||||
}
|
||||
@@ -110,6 +139,7 @@ func withUserHelper(fn handleFunc) handleFunc {
|
||||
if err != nil {
|
||||
return http.StatusUnauthorized, err
|
||||
}
|
||||
data.token = tokenString
|
||||
|
||||
var tk users.AuthToken
|
||||
token, err := jwt.ParseWithClaims(tokenString, &tk, keyFunc)
|
||||
@@ -126,11 +156,15 @@ func withUserHelper(fn handleFunc) handleFunc {
|
||||
if tk.Expires < time.Now().Add(time.Hour).Unix() {
|
||||
w.Header().Add("X-Renew-Token", "true")
|
||||
}
|
||||
|
||||
// Retrieve the user from the store and store it in the context
|
||||
data.user, err = store.Users.Get(config.Server.Root, tk.BelongsTo)
|
||||
data.user, err = store.Users.Get(files.RootPaths["default"], tk.BelongsTo)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
setUserInResponseWriter(w, data.user)
|
||||
|
||||
// Call the handler function, passing in the context
|
||||
return fn(w, r, data)
|
||||
}
|
||||
@@ -172,14 +206,14 @@ func wrapHandler(fn handleFunc) http.HandlerFunc {
|
||||
// Marshal the error response to JSON
|
||||
errorBytes, marshalErr := json.Marshal(response)
|
||||
if marshalErr != nil {
|
||||
log.Printf("Error marshalling error response: %v", marshalErr)
|
||||
logger.Error(fmt.Sprintf("Error marshalling error response: %v", marshalErr))
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Write the JSON error response
|
||||
if _, writeErr := w.Write(errorBytes); writeErr != nil {
|
||||
log.Printf("Error writing error response: %v", writeErr)
|
||||
logger.Error(fmt.Sprintf("Error writing error response: %v", writeErr))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -233,6 +267,7 @@ type ResponseWriterWrapper struct {
|
||||
StatusCode int
|
||||
wroteHeader bool
|
||||
PayloadSize int
|
||||
User string
|
||||
}
|
||||
|
||||
// WriteHeader captures the status code and ensures it's only written once
|
||||
@@ -255,6 +290,16 @@ func (w *ResponseWriterWrapper) Write(b []byte) (int, error) {
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
// Helper function to set the user in the ResponseWriterWrapper
|
||||
func setUserInResponseWriter(w http.ResponseWriter, user *users.User) {
|
||||
// Wrap the response writer to set the user field
|
||||
if wrappedWriter, ok := w.(*ResponseWriterWrapper); ok {
|
||||
if user != nil {
|
||||
wrappedWriter.User = user.Username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LoggingMiddleware logs each request and its status code.
|
||||
func LoggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -267,30 +312,24 @@ func LoggingMiddleware(next http.Handler) http.Handler {
|
||||
// Call the next handler.
|
||||
next.ServeHTTP(wrappedWriter, r)
|
||||
|
||||
// Determine the color based on the status code.
|
||||
color := "\033[32m" // Default green color
|
||||
if wrappedWriter.StatusCode >= 300 && wrappedWriter.StatusCode < 500 {
|
||||
color = "\033[33m" // Yellow for client errors (4xx)
|
||||
} else if wrappedWriter.StatusCode >= 500 {
|
||||
color = "\033[31m" // Red for server errors (5xx)
|
||||
}
|
||||
|
||||
// Capture the full URL path including the query parameters.
|
||||
fullURL := r.URL.Path
|
||||
if r.URL.RawQuery != "" {
|
||||
fullURL += "?" + r.URL.RawQuery
|
||||
}
|
||||
|
||||
// Log the request, status code, and response size.
|
||||
log.Printf("%s%-7s | %3d | %-15s | %-12s | \"%s\"%s",
|
||||
color,
|
||||
r.Method,
|
||||
wrappedWriter.StatusCode, // Captured status code
|
||||
r.RemoteAddr,
|
||||
time.Since(start).String(),
|
||||
fullURL,
|
||||
"\033[0m", // Reset color
|
||||
)
|
||||
truncUser := wrappedWriter.User
|
||||
if len(truncUser) > 12 {
|
||||
truncUser = truncUser[:10] + ".."
|
||||
}
|
||||
duration := time.Since(start)
|
||||
logger.Api(
|
||||
fmt.Sprintf("%-7s | %3d | %-15s | %-12s | %-12s | \"%s\"",
|
||||
r.Method,
|
||||
wrappedWriter.StatusCode, // Captured status code
|
||||
r.RemoteAddr,
|
||||
truncUser,
|
||||
fmt.Sprintf("%vms", duration.Milliseconds()),
|
||||
fullURL), wrappedWriter.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
storm "github.com/asdine/storm/v3"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/diskcache"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/img"
|
||||
@@ -50,7 +50,7 @@ func mockFileInfoFaster(t *testing.T) {
|
||||
// Mock the function to skip execution
|
||||
FileInfoFasterFunc = func(opts files.FileOptions) (files.ExtendedFileInfo, error) {
|
||||
return files.ExtendedFileInfo{
|
||||
FileInfo: &files.FileInfo{
|
||||
FileInfo: files.FileInfo{
|
||||
Path: opts.Path,
|
||||
ItemInfo: files.ItemInfo{
|
||||
Name: "mocked_file",
|
||||
@@ -105,7 +105,7 @@ func TestWithAdminHelper(t *testing.T) {
|
||||
data := &requestContext{
|
||||
user: tc.user,
|
||||
}
|
||||
token, err := makeSignedTokenAPI(tc.user, "WEB_TOKEN_"+utils.GenerateRandomHash(4), time.Hour*2, tc.user.Perm)
|
||||
token, err := makeSignedTokenAPI(tc.user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), time.Hour*2, tc.user.Perm)
|
||||
if err != nil {
|
||||
t.Fatalf("Error making token for request: %v", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
jwt "github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/cache"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
const (
|
||||
onlyOfficeStatusDocumentClosedWithChanges = 2
|
||||
onlyOfficeStatusDocumentClosedWithNoChanges = 4
|
||||
onlyOfficeStatusForceSaveWhileDocumentStillOpen = 6
|
||||
)
|
||||
|
||||
type OnlyOfficeCallback struct {
|
||||
ChangesURL string `json:"changesurl,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Status int `json:"status,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Users []string `json:"users,omitempty"`
|
||||
UserData string `json:"userdata,omitempty"`
|
||||
}
|
||||
|
||||
func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
if settings.Config.Integrations.OnlyOffice.Url == "" {
|
||||
return http.StatusInternalServerError, errors.New("only-office integration must be configured in settings")
|
||||
}
|
||||
|
||||
if !d.user.Perm.Modify {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
encodedUrl := r.URL.Query().Get("url")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
}
|
||||
// Decode the URL-encoded path
|
||||
url, err := url.QueryUnescape(encodedUrl)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
|
||||
// get path from url
|
||||
pathParts := strings.Split(url, "/api/raw?files=/")
|
||||
path := pathParts[len(pathParts)-1]
|
||||
urlFirst := pathParts[0]
|
||||
if settings.Config.Server.InternalUrl != "" {
|
||||
urlFirst = strings.TrimSuffix(settings.Config.Server.InternalUrl, "/")
|
||||
replacement := strings.Split(url, "/api/raw")[0]
|
||||
url = strings.Replace(url, replacement, settings.Config.Server.InternalUrl, 1)
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: filepath.Join(d.user.Scope, path),
|
||||
Modify: d.user.Perm.Modify,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
Checker: d.user,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
id, err := getOnlyOfficeId(source, fileInfo.Path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
split := strings.Split(fileInfo.Name, ".")
|
||||
fileType := split[len(split)-1]
|
||||
|
||||
theme := "light"
|
||||
if d.user.DarkMode {
|
||||
theme = "dark"
|
||||
}
|
||||
|
||||
clientConfig := map[string]interface{}{
|
||||
"document": map[string]interface{}{
|
||||
"fileType": fileType,
|
||||
"key": id,
|
||||
"title": fileInfo.Name,
|
||||
"url": url + "&auth=" + d.token,
|
||||
"permissions": map[string]interface{}{
|
||||
"edit": d.user.Perm.Modify,
|
||||
"download": d.user.Perm.Download,
|
||||
"print": d.user.Perm.Download,
|
||||
},
|
||||
},
|
||||
"editorConfig": map[string]interface{}{
|
||||
"callbackUrl": fmt.Sprintf("%v/api/onlyoffice/callback?path=%v&auth=%v", urlFirst, path, d.token),
|
||||
"user": map[string]interface{}{
|
||||
"id": strconv.FormatUint(uint64(d.user.ID), 10),
|
||||
"name": d.user.Username,
|
||||
},
|
||||
"customization": map[string]interface{}{
|
||||
"autosave": true,
|
||||
"forcesave": true,
|
||||
"uiTheme": theme,
|
||||
},
|
||||
"lang": d.user.Locale,
|
||||
"mode": "edit",
|
||||
},
|
||||
}
|
||||
if settings.Config.Integrations.OnlyOffice.Secret != "" {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(clientConfig))
|
||||
signature, err := token.SignedString([]byte(settings.Config.Integrations.OnlyOffice.Secret))
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to sign JWT")
|
||||
}
|
||||
clientConfig["token"] = signature
|
||||
}
|
||||
return renderJSON(w, r, clientConfig)
|
||||
}
|
||||
|
||||
func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
var data OnlyOfficeCallback
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
}
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
if data.Status == onlyOfficeStatusDocumentClosedWithChanges ||
|
||||
data.Status == onlyOfficeStatusDocumentClosedWithNoChanges {
|
||||
// Refer to only-office documentation
|
||||
// - https://api.onlyoffice.com/editors/coedit
|
||||
// - https://api.onlyoffice.com/editors/callback
|
||||
//
|
||||
// When the document is fully closed by all editors,
|
||||
// then the document key should no longer be re-used.
|
||||
deleteOfficeId(source, path)
|
||||
}
|
||||
|
||||
if data.Status == onlyOfficeStatusDocumentClosedWithChanges ||
|
||||
data.Status == onlyOfficeStatusForceSaveWhileDocumentStillOpen {
|
||||
if !d.user.Perm.Modify {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
doc, err := http.Get(data.URL)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
defer doc.Body.Close()
|
||||
|
||||
err = d.Runner.RunHook(func() error {
|
||||
fileOpts := files.FileOptions{
|
||||
Path: path,
|
||||
Source: source,
|
||||
}
|
||||
writeErr := files.WriteFile(fileOpts, doc.Body)
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
return nil
|
||||
}, "save", path, "", d.user)
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
|
||||
resp := map[string]int{
|
||||
"error": 0,
|
||||
}
|
||||
return renderJSON(w, r, resp)
|
||||
}
|
||||
|
||||
func getOnlyOfficeId(source, path string) (string, error) {
|
||||
idx := files.GetIndex(source)
|
||||
realpath, _, _ := idx.GetRealPath(path)
|
||||
// error is intentionally ignored in order treat errors
|
||||
// the same as a cache-miss
|
||||
cachedDocumentKey, ok := cache.OnlyOffice.Get(realpath).(string)
|
||||
if ok {
|
||||
return cachedDocumentKey, nil
|
||||
}
|
||||
return "", fmt.Errorf("document key not found")
|
||||
}
|
||||
func deleteOfficeId(source, path string) {
|
||||
idx := files.GetIndex(source)
|
||||
realpath, _, _ := idx.GetRealPath(path)
|
||||
cache.OnlyOffice.Delete(realpath)
|
||||
}
|
||||
+20
-18
@@ -9,9 +9,11 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/img"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
)
|
||||
|
||||
type ImgService interface {
|
||||
@@ -42,20 +44,20 @@ type FileCache interface {
|
||||
// @Router /api/preview [get]
|
||||
func previewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
path := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
previewSize := r.URL.Query().Get("size")
|
||||
if previewSize != "small" {
|
||||
previewSize = "large"
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid request path")
|
||||
}
|
||||
response, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: filepath.Join(d.user.Scope, path),
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: true,
|
||||
ReadHeader: config.Server.TypeDetectionByHeader,
|
||||
Checker: d.user,
|
||||
Path: filepath.Join(d.user.Scope, path),
|
||||
Modify: d.user.Perm.Modify,
|
||||
Source: source,
|
||||
Expand: true,
|
||||
Checker: d.user,
|
||||
})
|
||||
fileInfo := response.FileInfo
|
||||
if err != nil {
|
||||
@@ -88,26 +90,25 @@ func previewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
cacheKey := previewCacheKey(fileInfo, previewSize)
|
||||
cacheKey := previewCacheKey(response.RealPath, previewSize, fileInfo.ModTime)
|
||||
resizedImage, ok, err := fileCache.Load(r.Context(), cacheKey)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
resizedImage, err = createPreview(imgSvc, fileCache, fileInfo, previewSize)
|
||||
resizedImage, err = createPreview(imgSvc, fileCache, response, previewSize)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
}
|
||||
w.Header().Set("Cache-Control", "private")
|
||||
http.ServeContent(w, r, fileInfo.RealPath(), fileInfo.ModTime, bytes.NewReader(resizedImage))
|
||||
|
||||
http.ServeContent(w, r, response.RealPath, fileInfo.ModTime, bytes.NewReader(resizedImage))
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func createPreview(imgSvc ImgService, fileCache FileCache, file *files.FileInfo, previewSize string) ([]byte, error) {
|
||||
fd, err := os.Open(file.RealPath())
|
||||
func createPreview(imgSvc ImgService, fileCache FileCache, file files.ExtendedFileInfo, previewSize string) ([]byte, error) {
|
||||
fd, err := os.Open(file.RealPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -138,9 +139,9 @@ func createPreview(imgSvc ImgService, fileCache FileCache, file *files.FileInfo,
|
||||
}
|
||||
|
||||
go func() {
|
||||
cacheKey := previewCacheKey(file, previewSize)
|
||||
cacheKey := previewCacheKey(file.RealPath, previewSize, file.FileInfo.ModTime)
|
||||
if err := fileCache.Store(context.Background(), cacheKey, buf.Bytes()); err != nil {
|
||||
fmt.Printf("failed to cache resized image: %v", err)
|
||||
logger.Error(fmt.Sprintf("failed to cache resized image: %v", err))
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -148,12 +149,13 @@ func createPreview(imgSvc ImgService, fileCache FileCache, file *files.FileInfo,
|
||||
}
|
||||
|
||||
// Generates a cache key for the preview image
|
||||
func previewCacheKey(f *files.FileInfo, previewSize string) string {
|
||||
return fmt.Sprintf("%x%x%x", f.RealPath(), f.ModTime.Unix(), previewSize)
|
||||
func previewCacheKey(realPath, previewSize string, modTime time.Time) string {
|
||||
return fmt.Sprintf("%x%x%x", realPath, modTime.Unix(), previewSize)
|
||||
}
|
||||
|
||||
func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo) (int, error) {
|
||||
realPath, _, _ := files.GetRealPath(file.Path)
|
||||
func rawFileHandler(w http.ResponseWriter, r *http.Request, file files.FileInfo) (int, error) {
|
||||
idx := files.GetIndex("default")
|
||||
realPath, _, _ := idx.GetRealPath(file.Path)
|
||||
fd, err := os.Open(realPath)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
|
||||
+2
-20
@@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
|
||||
_ "github.com/gtsteffaniak/filebrowser/backend/swagger/docs"
|
||||
@@ -16,9 +15,9 @@ import (
|
||||
func publicShareHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
file, ok := d.raw.(files.ExtendedFileInfo)
|
||||
if !ok {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type *files.FileInfo")
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type files.FileInfo")
|
||||
}
|
||||
file.Path = strings.TrimPrefix(file.Path, settings.Config.Server.Root)
|
||||
file.Path = strings.TrimPrefix(file.Path, files.RootPaths["default"])
|
||||
return renderJSON(w, r, file)
|
||||
}
|
||||
|
||||
@@ -29,23 +28,6 @@ func publicUserGetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(status), status)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func publicDlHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
file, ok := d.raw.(files.ExtendedFileInfo)
|
||||
if !ok {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type files.FileInfo")
|
||||
}
|
||||
if d.user == nil {
|
||||
return http.StatusUnauthorized, fmt.Errorf("failed to get user")
|
||||
}
|
||||
|
||||
if file.Type == "directory" {
|
||||
return rawFilesHandler(w, r, d, []string{file.Path})
|
||||
}
|
||||
|
||||
return rawFileHandler(w, r, file.FileInfo)
|
||||
}
|
||||
|
||||
// health godoc
|
||||
|
||||
+56
-16
@@ -7,7 +7,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -15,6 +14,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
)
|
||||
|
||||
func setContentDisposition(w http.ResponseWriter, r *http.Request, fileName string) {
|
||||
@@ -45,17 +45,28 @@ func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int,
|
||||
if !d.user.Perm.Download {
|
||||
return http.StatusAccepted, nil
|
||||
}
|
||||
|
||||
filePrefix := ""
|
||||
file, ok := d.raw.(files.ExtendedFileInfo)
|
||||
if ok {
|
||||
filePrefix = file.Path
|
||||
}
|
||||
encodedFiles := r.URL.Query().Get("files")
|
||||
// Decode the URL-encoded path
|
||||
files, err := url.QueryUnescape(encodedFiles)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
return rawFilesHandler(w, r, d, strings.Split(files, ","))
|
||||
fileList := strings.Split(files, ",|")
|
||||
for i, f := range fileList {
|
||||
fileList[i] = filepath.Join(filePrefix, f)
|
||||
}
|
||||
return rawFilesHandler(w, r, d, fileList)
|
||||
}
|
||||
|
||||
func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *zip.Writer) error {
|
||||
realPath, _, _ := files.GetRealPath(d.user.Scope, path)
|
||||
func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *zip.Writer, flatten bool) error {
|
||||
idx := files.GetIndex("default")
|
||||
realPath, _, _ := idx.GetRealPath(d.user.Scope, path)
|
||||
if !d.user.Check(realPath) {
|
||||
return nil
|
||||
}
|
||||
@@ -64,16 +75,36 @@ func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *z
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the base name of the top-level folder or file
|
||||
baseName := filepath.Base(realPath)
|
||||
|
||||
if info.IsDir() {
|
||||
// Walk through directory contents
|
||||
return filepath.Walk(realPath, func(filePath string, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relPath, err := filepath.Rel(filepath.Dir(realPath), filePath) // Relative path including base dir
|
||||
|
||||
// Calculate the relative path
|
||||
relPath, err := filepath.Rel(realPath, filePath) // Use realPath directly
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Normalize for tar: convert \ to /
|
||||
relPath = filepath.ToSlash(relPath)
|
||||
|
||||
// Skip adding `.` (current directory)
|
||||
if relPath == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prepend base folder name unless flatten is true
|
||||
if !flatten {
|
||||
relPath = filepath.Join(baseName, relPath)
|
||||
relPath = filepath.ToSlash(relPath) // Ensure normalized separators
|
||||
}
|
||||
|
||||
if fileInfo.IsDir() {
|
||||
if tarWriter != nil {
|
||||
header := &tar.Header{
|
||||
@@ -94,8 +125,8 @@ func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *z
|
||||
})
|
||||
}
|
||||
|
||||
// Add a single file if it's not a directory
|
||||
return addSingleFile(realPath, filepath.Base(realPath), zipWriter, tarWriter)
|
||||
// For a single file, use the base name as the archive path
|
||||
return addSingleFile(realPath, baseName, zipWriter, tarWriter)
|
||||
}
|
||||
|
||||
func addSingleFile(realPath, archivePath string, zipWriter *zip.Writer, tarWriter *tar.Writer) error {
|
||||
@@ -111,11 +142,12 @@ func addSingleFile(realPath, archivePath string, zipWriter *zip.Writer, tarWrite
|
||||
}
|
||||
|
||||
if tarWriter != nil {
|
||||
header, err := tar.FileInfoHeader(info, realPath)
|
||||
header, err := tar.FileInfoHeader(info, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = archivePath
|
||||
// Ensure correct relative path
|
||||
header.Name = filepath.ToSlash(archivePath)
|
||||
if err = tarWriter.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -143,7 +175,8 @@ func addSingleFile(realPath, archivePath string, zipWriter *zip.Writer, tarWrite
|
||||
func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext, fileList []string) (int, error) {
|
||||
filePath := fileList[0]
|
||||
fileName := filepath.Base(filePath)
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, filePath)
|
||||
idx := files.GetIndex("default")
|
||||
realPath, isDir, err := idx.GetRealPath(d.user.Scope, filePath)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -163,6 +196,7 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
// Set headers and serve the file
|
||||
setContentDisposition(w, r, fileName)
|
||||
w.Header().Set("Cache-Control", "private")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
|
||||
// Serve the content
|
||||
http.ServeContent(w, r, fileName, fileInfo.ModTime(), fd)
|
||||
@@ -179,12 +213,14 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
default:
|
||||
return http.StatusInternalServerError, errors.New("format not implemented")
|
||||
}
|
||||
|
||||
baseDirName := filepath.Base(filepath.Dir(realPath))
|
||||
if baseDirName == "" || baseDirName == "/" {
|
||||
baseDirName = "download"
|
||||
}
|
||||
downloadFileName := url.PathEscape(baseDirName + "." + algo)
|
||||
if len(fileList) == 1 && isDir {
|
||||
baseDirName = filepath.Base(realPath)
|
||||
}
|
||||
downloadFileName := url.PathEscape(baseDirName + extension)
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+downloadFileName)
|
||||
// Create the archive and stream it directly to the response
|
||||
if extension == ".zip" {
|
||||
@@ -204,10 +240,12 @@ func createZip(w io.Writer, d *requestContext, filenames ...string) error {
|
||||
zipWriter := zip.NewWriter(w)
|
||||
defer zipWriter.Close()
|
||||
|
||||
// Check if we have exactly one directory
|
||||
//flatten := len(filenames) == 1
|
||||
for _, fname := range filenames {
|
||||
err := addFile(fname, d, nil, zipWriter)
|
||||
err := addFile(fname, d, nil, zipWriter, false)
|
||||
if err != nil {
|
||||
log.Printf("Failed to add %s to ZIP: %v", fname, err)
|
||||
logger.Error(fmt.Sprintf("Failed to add %s to ZIP: %v", fname, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,10 +259,12 @@ func createTarGz(w io.Writer, d *requestContext, filenames ...string) error {
|
||||
tarWriter := tar.NewWriter(gzWriter)
|
||||
defer tarWriter.Close()
|
||||
|
||||
// Check if we have exactly one directory
|
||||
//flatten := len(filenames) == 1
|
||||
for _, fname := range filenames {
|
||||
err := addFile(fname, d, tarWriter, nil)
|
||||
err := addFile(fname, d, tarWriter, nil, false)
|
||||
if err != nil {
|
||||
log.Printf("Failed to add %s to TAR.GZ: %v", fname, err)
|
||||
logger.Error(fmt.Sprintf("Failed to add %s to TAR.GZ: %v", fname, err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+88
-76
@@ -13,9 +13,10 @@ import (
|
||||
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/cache"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
)
|
||||
|
||||
// resourceGetHandler retrieves information about a resource.
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param path query string true "Path to the resource"
|
||||
// @Param source query string false "Source name for the desired source, default is used if not provided"
|
||||
// @Param source query string false "Name for the desired source, default is used if not provided"
|
||||
// @Param content query string false "Include file content if true"
|
||||
// @Param checksum query string false "Optional checksum validation"
|
||||
@@ -33,20 +35,23 @@ import (
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/resources [get]
|
||||
func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
// TODO source := r.URL.Query().Get("source")
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
}
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: filepath.Join(d.user.Scope, path),
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: true,
|
||||
ReadHeader: config.Server.TypeDetectionByHeader,
|
||||
Checker: d.user,
|
||||
Content: r.URL.Query().Get("content") == "true",
|
||||
Path: filepath.Join(d.user.Scope, path),
|
||||
Modify: d.user.Perm.Modify,
|
||||
Source: source,
|
||||
Expand: true,
|
||||
Checker: d.user,
|
||||
Content: r.URL.Query().Get("content") == "true",
|
||||
})
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
@@ -55,7 +60,9 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
return renderJSON(w, r, fileInfo)
|
||||
}
|
||||
if algo := r.URL.Query().Get("checksum"); algo != "" {
|
||||
checksums, err := files.GetChecksum(fileInfo.Path, algo)
|
||||
idx := files.GetIndex(source)
|
||||
realPath, _, _ := idx.GetRealPath(d.user.Scope, path)
|
||||
checksums, err := files.GetChecksum(realPath, algo)
|
||||
if err == errors.ErrInvalidOption {
|
||||
return http.StatusBadRequest, nil
|
||||
} else if err != nil {
|
||||
@@ -74,6 +81,7 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param path query string true "Path to the resource"
|
||||
// @Param source query string false "Source name for the desired source, default is used if not provided"
|
||||
// @Param source query string false "Name for the desired source, default is used if not provided"
|
||||
// @Success 200 "Resource deleted successfully"
|
||||
// @Failure 403 {object} map[string]string "Forbidden"
|
||||
@@ -83,6 +91,10 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
// TODO source := r.URL.Query().Get("source")
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
}
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
if err != nil {
|
||||
@@ -91,17 +103,12 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon
|
||||
if path == "/" || !d.user.Perm.Delete {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
fileOpts := files.FileOptions{
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: config.Server.TypeDetectionByHeader,
|
||||
Checker: d.user,
|
||||
Path: filepath.Join(d.user.Scope, path),
|
||||
Source: source,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
Checker: d.user,
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(fileOpts)
|
||||
if err != nil {
|
||||
@@ -109,12 +116,9 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon
|
||||
}
|
||||
|
||||
// delete thumbnails
|
||||
err = delThumbs(r.Context(), fileCache, fileInfo.FileInfo)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
delThumbs(r.Context(), fileCache, fileInfo)
|
||||
|
||||
err = files.DeleteFiles(realPath, fileOpts)
|
||||
err = files.DeleteFiles(source, fileInfo.RealPath, filepath.Dir(fileInfo.RealPath))
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
@@ -129,6 +133,7 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param path query string true "Path to the resource"
|
||||
// @Param source query string false "Source name for the desired source, default is used if not provided"
|
||||
// @Param source query string false "Name for the desired source, default is used if not provided"
|
||||
// @Param override query bool false "Override existing file if true"
|
||||
// @Success 200 "Resource created successfully"
|
||||
@@ -138,8 +143,11 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/resources [post]
|
||||
func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
// TODO source := r.URL.Query().Get("source")
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
}
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
if err != nil {
|
||||
@@ -150,6 +158,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
}
|
||||
fileOpts := files.FileOptions{
|
||||
Path: filepath.Join(d.user.Scope, path),
|
||||
Source: source,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
Checker: d.user,
|
||||
@@ -173,10 +182,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
err = delThumbs(r.Context(), fileCache, fileInfo.FileInfo)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
delThumbs(r.Context(), fileCache, fileInfo)
|
||||
}
|
||||
err = files.WriteFile(fileOpts, r.Body)
|
||||
if err != nil {
|
||||
@@ -193,6 +199,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param path query string true "Path to the resource"
|
||||
// @Param source query string false "Source name for the desired source, default is used if not provided"
|
||||
// @Param source query string false "Name for the desired source, default is used if not provided"
|
||||
// @Success 200 "Resource updated successfully"
|
||||
// @Failure 403 {object} map[string]string "Forbidden"
|
||||
@@ -201,9 +208,13 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/resources [put]
|
||||
func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
// TODO source := r.URL.Query().Get("source")
|
||||
// TODO source := r.URL.Query().Get("source")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
}
|
||||
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
if err != nil {
|
||||
@@ -218,17 +229,12 @@ func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, path)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
fileOpts := files.FileOptions{
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: config.Server.TypeDetectionByHeader,
|
||||
Checker: d.user,
|
||||
Path: filepath.Join(d.user.Scope, path),
|
||||
Source: source,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
Checker: d.user,
|
||||
}
|
||||
err = files.WriteFile(fileOpts, r.Body)
|
||||
return errToStatus(err), err
|
||||
@@ -241,6 +247,7 @@ func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param from query string true "Path from resource"
|
||||
// @Param source query string false "Source name for the desired source, default is used if not provided"
|
||||
// @Param destination query string true "Destination path for the resource"
|
||||
// @Param action query string true "Action to perform (copy, rename)"
|
||||
// @Param overwrite query bool false "Overwrite if destination exists"
|
||||
@@ -254,6 +261,10 @@ func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
// TODO source := r.URL.Query().Get("source")
|
||||
action := r.URL.Query().Get("action")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
}
|
||||
encodedFrom := r.URL.Query().Get("from")
|
||||
// Decode the URL-encoded path
|
||||
src, err := url.QueryUnescape(encodedFrom)
|
||||
@@ -266,19 +277,20 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
return errToStatus(err), err
|
||||
}
|
||||
if !d.user.Check(src) || !d.user.Check(dst) {
|
||||
return http.StatusForbidden, nil
|
||||
return http.StatusForbidden, fmt.Errorf("forbidden: user rules deny access to source or destination")
|
||||
}
|
||||
if dst == "/" || src == "/" {
|
||||
return http.StatusForbidden, nil
|
||||
return http.StatusForbidden, fmt.Errorf("forbidden: source or destination is attempting to modify root")
|
||||
}
|
||||
|
||||
idx := files.GetIndex(source)
|
||||
// check target dir exists
|
||||
parentDir, _, err := files.GetRealPath(d.user.Scope, filepath.Dir(dst))
|
||||
parentDir, _, err := idx.GetRealPath(d.user.Scope, filepath.Dir(dst))
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
realDest := parentDir + "/" + filepath.Base(dst)
|
||||
realSrc, isSrcDir, err := files.GetRealPath(d.user.Scope, src)
|
||||
realSrc, isSrcDir, err := idx.GetRealPath(d.user.Scope, src)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
@@ -289,12 +301,14 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
}
|
||||
// Permission for overwriting the file
|
||||
if overwrite && !d.user.Perm.Modify {
|
||||
return http.StatusForbidden, nil
|
||||
return http.StatusForbidden, fmt.Errorf("forbidden: user does not have permission to overwrite file")
|
||||
}
|
||||
err = d.RunHook(func() error {
|
||||
return patchAction(r.Context(), action, realSrc, realDest, d, fileCache, isSrcDir)
|
||||
err = d.Runner.RunHook(func() error {
|
||||
return patchAction(r.Context(), action, realSrc, realDest, d, fileCache, isSrcDir, source)
|
||||
}, action, realSrc, realDest, d.user)
|
||||
|
||||
if err != nil {
|
||||
logger.Debug(fmt.Sprintf("Could not run patch action. src=%v dst=%v err=%v", realSrc, realDest, err))
|
||||
}
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
@@ -314,28 +328,28 @@ func addVersionSuffix(source string) string {
|
||||
return source
|
||||
}
|
||||
|
||||
func delThumbs(ctx context.Context, fileCache FileCache, file *files.FileInfo) error {
|
||||
if err := fileCache.Delete(ctx, previewCacheKey(file, "small")); err != nil {
|
||||
return err
|
||||
func delThumbs(ctx context.Context, fileCache FileCache, file files.ExtendedFileInfo) {
|
||||
err := fileCache.Delete(ctx, previewCacheKey(file.RealPath, "small", file.FileInfo.ModTime))
|
||||
if err != nil {
|
||||
logger.Debug(fmt.Sprintf("Could not delete small thumbnail: %v", err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func patchAction(ctx context.Context, action, src, dst string, d *requestContext, fileCache FileCache, isSrcDir bool) error {
|
||||
func patchAction(ctx context.Context, action, src, dst string, d *requestContext, fileCache FileCache, isSrcDir bool, index string) error {
|
||||
switch action {
|
||||
case "copy":
|
||||
if !d.user.Perm.Create {
|
||||
return errors.ErrPermissionDenied
|
||||
}
|
||||
err := files.CopyResource(src, dst, isSrcDir)
|
||||
err := files.CopyResource(index, src, dst, isSrcDir)
|
||||
return err
|
||||
case "rename", "move":
|
||||
|
||||
if !d.user.Perm.Rename {
|
||||
return errors.ErrPermissionDenied
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: src,
|
||||
Source: index,
|
||||
IsDir: isSrcDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
@@ -347,11 +361,8 @@ func patchAction(ctx context.Context, action, src, dst string, d *requestContext
|
||||
}
|
||||
|
||||
// delete thumbnails
|
||||
err = delThumbs(ctx, fileCache, fileInfo.FileInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return files.MoveResource(src, dst, isSrcDir)
|
||||
delThumbs(ctx, fileCache, fileInfo)
|
||||
return files.MoveResource(index, src, dst, isSrcDir)
|
||||
default:
|
||||
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
|
||||
}
|
||||
@@ -368,7 +379,7 @@ type DiskUsageResponse struct {
|
||||
// @Tags Resources
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param source query string false "Name for the desired source, default is used if not provided"
|
||||
// @Param source query string false "Source name for the desired source, default is used if not provided"
|
||||
// @Success 200 {object} DiskUsageResponse "Disk usage details"
|
||||
// @Failure 404 {object} map[string]string "Directory not found"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
@@ -376,22 +387,19 @@ type DiskUsageResponse struct {
|
||||
func diskUsage(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "/"
|
||||
source = "default"
|
||||
}
|
||||
|
||||
value, ok := utils.DiskUsageCache.Get(source).(DiskUsageResponse)
|
||||
value, ok := cache.DiskUsage.Get(source).(DiskUsageResponse)
|
||||
if ok {
|
||||
return renderJSON(w, r, &value)
|
||||
}
|
||||
|
||||
fPath, isDir, err := files.GetRealPath(d.user.Scope, source)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
rootPath, ok := files.RootPaths[source]
|
||||
if !ok {
|
||||
return 400, fmt.Errorf("bad source path provided: %v", source)
|
||||
}
|
||||
if !isDir {
|
||||
return http.StatusNotFound, fmt.Errorf("not a directory: %s", source)
|
||||
}
|
||||
usage, err := disk.UsageWithContext(r.Context(), fPath)
|
||||
|
||||
usage, err := disk.UsageWithContext(r.Context(), rootPath)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
@@ -399,17 +407,21 @@ func diskUsage(w http.ResponseWriter, r *http.Request, d *requestContext) (int,
|
||||
Total: usage.Total,
|
||||
Used: usage.Used,
|
||||
}
|
||||
utils.DiskUsageCache.Set(source, latestUsage)
|
||||
cache.DiskUsage.Set(source, latestUsage)
|
||||
return renderJSON(w, r, &latestUsage)
|
||||
}
|
||||
|
||||
func inspectIndex(w http.ResponseWriter, r *http.Request) {
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
}
|
||||
// Decode the URL-encoded path
|
||||
path, _ := url.QueryUnescape(encodedPath)
|
||||
isDir := r.URL.Query().Get("isDir") == "true"
|
||||
index := files.GetIndex(config.Server.Root)
|
||||
info, _ := index.GetReducedMetadata(path, isDir)
|
||||
isNotDir := r.URL.Query().Get("isDir") == "false" // default to isDir true
|
||||
index := files.GetIndex(source)
|
||||
info, _ := index.GetReducedMetadata(path, !isNotDir)
|
||||
renderJSON(w, r, info) // nolint:errcheck
|
||||
}
|
||||
|
||||
|
||||
+78
-50
@@ -1,15 +1,17 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/version"
|
||||
@@ -43,7 +45,7 @@ var (
|
||||
assetFs fs.FS
|
||||
)
|
||||
|
||||
func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
|
||||
func StartHttp(ctx context.Context, Service ImgService, storage *storage.Storage, cache FileCache, shutdownComplete chan struct{}) {
|
||||
|
||||
store = storage
|
||||
fileCache = cache
|
||||
@@ -56,7 +58,7 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
|
||||
// Embedded mode: Serve files from the embedded assets
|
||||
assetFs, err = fs.Sub(assets, "embed")
|
||||
if err != nil {
|
||||
log.Fatal("Could not embed frontend. Does dist exist?")
|
||||
logger.Fatal("Could not embed frontend. Does dist exist?")
|
||||
}
|
||||
} else {
|
||||
assetFs = dirFS{Dir: http.Dir("http/dist")}
|
||||
@@ -107,13 +109,16 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
|
||||
|
||||
// Public routes
|
||||
api.HandleFunc("GET /public/publicUser", publicUserGetHandler)
|
||||
api.HandleFunc("GET /public/dl", withHashFile(publicDlHandler))
|
||||
api.HandleFunc("GET /public/dl", withHashFile(rawHandler))
|
||||
api.HandleFunc("GET /public/share", withHashFile(publicShareHandler))
|
||||
|
||||
// Settings routes
|
||||
api.HandleFunc("GET /settings", withAdmin(settingsGetHandler))
|
||||
api.HandleFunc("PUT /settings", withAdmin(settingsPutHandler))
|
||||
|
||||
api.HandleFunc("GET /onlyoffice/config", withUser(onlyofficeClientConfigGetHandler))
|
||||
api.HandleFunc("POST /onlyoffice/callback", withUser(onlyofficeCallbackHandler))
|
||||
|
||||
api.HandleFunc("GET /search", withUser(searchHandler))
|
||||
apiPath := config.Server.BaseURL + "api"
|
||||
router.Handle(apiPath+"/", http.StripPrefix(apiPath, api))
|
||||
@@ -137,51 +142,74 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
|
||||
|
||||
var scheme string
|
||||
port := ""
|
||||
|
||||
// Determine whether to use HTTPS (TLS) or HTTP
|
||||
if config.Server.TLSCert != "" && config.Server.TLSKey != "" {
|
||||
// Load the TLS certificate and key
|
||||
cer, err := tls.LoadX509KeyPair(config.Server.TLSCert, config.Server.TLSKey)
|
||||
if err != nil {
|
||||
log.Fatalf("could not load certificate: %v", err)
|
||||
}
|
||||
|
||||
// Create a custom TLS listener
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
Certificates: []tls.Certificate{cer},
|
||||
}
|
||||
|
||||
// Set HTTPS scheme and default port for TLS
|
||||
scheme = "https"
|
||||
|
||||
// Listen on TCP and wrap with TLS
|
||||
listener, err := tls.Listen("tcp", fmt.Sprintf(":%v", config.Server.Port), tlsConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("could not start TLS server: %v", err)
|
||||
}
|
||||
if config.Server.Port != 443 {
|
||||
port = fmt.Sprintf(":%d", config.Server.Port)
|
||||
}
|
||||
// Build the full URL with host and port
|
||||
fullURL := fmt.Sprintf("%s://localhost%s%s", scheme, port, config.Server.BaseURL)
|
||||
log.Printf("Running at : %s", fullURL)
|
||||
err = http.Serve(listener, muxWithMiddleware(router))
|
||||
if err != nil {
|
||||
log.Fatalf("could not start server: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Set HTTP scheme and the default port for HTTP
|
||||
scheme = "http"
|
||||
if config.Server.Port != 80 {
|
||||
port = fmt.Sprintf(":%d", config.Server.Port)
|
||||
}
|
||||
// Build the full URL with host and port
|
||||
fullURL := fmt.Sprintf("%s://localhost%s%s", scheme, port, config.Server.BaseURL)
|
||||
log.Printf("Running at : %s", fullURL)
|
||||
err := http.ListenAndServe(fmt.Sprintf(":%v", config.Server.Port), muxWithMiddleware(router))
|
||||
if err != nil {
|
||||
log.Fatalf("could not start server: %v", err)
|
||||
}
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf(":%v", config.Server.Port),
|
||||
Handler: muxWithMiddleware(router),
|
||||
}
|
||||
go func() {
|
||||
// Determine whether to use HTTPS (TLS) or HTTP
|
||||
if config.Server.TLSCert != "" && config.Server.TLSKey != "" {
|
||||
// Load the TLS certificate and key
|
||||
cer, err := tls.LoadX509KeyPair(config.Server.TLSCert, config.Server.TLSKey)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("Could not load certificate: %v", err))
|
||||
}
|
||||
|
||||
// Create a custom TLS configuration
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
Certificates: []tls.Certificate{cer},
|
||||
}
|
||||
|
||||
// Set HTTPS scheme and default port for TLS
|
||||
scheme = "https"
|
||||
if config.Server.Port != 443 {
|
||||
port = fmt.Sprintf(":%d", config.Server.Port)
|
||||
}
|
||||
|
||||
// Build the full URL with host and port
|
||||
fullURL := fmt.Sprintf("%s://localhost%s%s", scheme, port, config.Server.BaseURL)
|
||||
logger.Info(fmt.Sprintf("Running at : %s", fullURL))
|
||||
|
||||
// Create a TLS listener and serve
|
||||
listener, err := tls.Listen("tcp", srv.Addr, tlsConfig)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("Could not start TLS server: %v", err))
|
||||
}
|
||||
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
logger.Fatal(fmt.Sprintf("Server error: %v", err))
|
||||
}
|
||||
} else {
|
||||
// Set HTTP scheme and the default port for HTTP
|
||||
scheme = "http"
|
||||
if config.Server.Port != 80 {
|
||||
port = fmt.Sprintf(":%d", config.Server.Port)
|
||||
}
|
||||
|
||||
// Build the full URL with host and port
|
||||
fullURL := fmt.Sprintf("%s://localhost%s%s", scheme, port, config.Server.BaseURL)
|
||||
logger.Info(fmt.Sprintf("Running at : %s", fullURL))
|
||||
|
||||
// Start HTTP server
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.Fatal(fmt.Sprintf("Server error: %v", err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for context cancellation to shut down the server
|
||||
<-ctx.Done()
|
||||
logger.Info("Shutting down HTTP server...")
|
||||
|
||||
// Graceful shutdown with a timeout - 30 seconds, in case downloads are happening
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
logger.Error(fmt.Sprintf("HTTP server forced to shut down: %v", err))
|
||||
} else {
|
||||
logger.Info("HTTP server shut down gracefully.")
|
||||
}
|
||||
|
||||
// Signal that shutdown is complete
|
||||
close(shutdownComplete)
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
// searchHandler handles search requests for files based on the provided query.
|
||||
@@ -54,11 +53,15 @@ import (
|
||||
// @Router /api/search [get]
|
||||
func searchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
query := r.URL.Query().Get("query")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
}
|
||||
searchScope := strings.TrimPrefix(r.URL.Query().Get("scope"), ".")
|
||||
searchScope = strings.TrimPrefix(searchScope, "/")
|
||||
// Retrieve the User-Agent and X-Auth headers from the request
|
||||
sessionId := r.Header.Get("SessionId")
|
||||
index := files.GetIndex(settings.Config.Server.Root)
|
||||
index := files.GetIndex(source)
|
||||
userScope := strings.TrimPrefix(d.user.Scope, ".")
|
||||
combinedScope := strings.TrimPrefix(userScope+"/"+searchScope, "/")
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ func shareGetsHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
if err == errors.ErrNotExist {
|
||||
return renderJSON(w, r, []*share.Link{})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("error getting share info from server")
|
||||
}
|
||||
|
||||
+11
-8
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
"text/template"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/version"
|
||||
)
|
||||
@@ -36,7 +36,7 @@ func (t *TemplateRenderer) Render(w http.ResponseWriter, name string, data inter
|
||||
func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentType string) {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
auther, err := store.Auth.Get(config.Auth.Method)
|
||||
auther, err := store.Auth.Get("password")
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -44,7 +44,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Name": config.Frontend.Name,
|
||||
"DisableExternal": config.Frontend.DisableExternal,
|
||||
"DisableExternal": config.Frontend.DisableDefaultLinks,
|
||||
"DisableUsedPercentage": config.Frontend.DisableUsedPercentage,
|
||||
"darkMode": settings.Config.UserDefaults.DarkMode,
|
||||
"Color": config.Frontend.Color,
|
||||
@@ -53,8 +53,8 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
|
||||
"CommitSHA": version.CommitSHA,
|
||||
"StaticURL": config.Server.BaseURL + "static",
|
||||
"Signup": settings.Config.Auth.Signup,
|
||||
"NoAuth": config.Auth.Method == "noauth",
|
||||
"AuthMethod": config.Auth.Method,
|
||||
"NoAuth": config.Auth.Methods.NoAuth,
|
||||
"PasswordAuth": config.Auth.Methods.PasswordAuth,
|
||||
"LoginPage": auther.LoginPage(),
|
||||
"CSS": false,
|
||||
"ReCaptcha": false,
|
||||
@@ -62,6 +62,9 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
|
||||
"ResizePreview": config.Server.ResizePreview,
|
||||
"EnableExec": config.Server.EnableExec,
|
||||
"ReCaptchaHost": config.Auth.Recaptcha.Host,
|
||||
"ExternalLinks": config.Frontend.ExternalLinks,
|
||||
"ExternalUrl": strings.TrimSuffix(config.Server.ExternalUrl, "/"),
|
||||
"OnlyOfficeUrl": settings.Config.Integrations.OnlyOffice.Url,
|
||||
}
|
||||
|
||||
if config.Frontend.Files != "" {
|
||||
@@ -69,7 +72,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
|
||||
_, err := os.Stat(fPath) //nolint:govet
|
||||
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
log.Printf("couldn't load custom styles: %v", err)
|
||||
logger.Error(fmt.Sprintf("couldn't load custom styles: %v", err))
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
@@ -77,8 +80,8 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
|
||||
}
|
||||
}
|
||||
|
||||
if config.Auth.Method == "password" {
|
||||
raw, err := store.Auth.Get(config.Auth.Method) //nolint:govet
|
||||
if config.Auth.Methods.PasswordAuth.Enabled {
|
||||
raw, err := store.Auth.Get("password") //nolint:govet
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
+18
-13
@@ -55,26 +55,29 @@ func userGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
|
||||
if givenUserIdString == "self" {
|
||||
givenUserId = d.user.ID
|
||||
} else if givenUserIdString == "" {
|
||||
if !d.user.Perm.Admin {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
users, err := store.Users.Gets(config.Server.Root)
|
||||
|
||||
userList, err := store.Users.Gets(files.RootPaths["default"])
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
selfUserList := []*users.User{}
|
||||
for _, u := range userList {
|
||||
u.Password = ""
|
||||
}
|
||||
for _, u := range users {
|
||||
u.ApiKeys = nil
|
||||
if u.ID == d.user.ID {
|
||||
selfUserList = append(selfUserList, u)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(users, func(i, j int) bool {
|
||||
return users[i].ID < users[j].ID
|
||||
sort.Slice(userList, func(i, j int) bool {
|
||||
return userList[i].ID < userList[j].ID
|
||||
})
|
||||
|
||||
return renderJSON(w, r, users)
|
||||
if !d.user.Perm.Admin {
|
||||
userList = selfUserList
|
||||
}
|
||||
return renderJSON(w, r, userList)
|
||||
} else {
|
||||
num, _ := strconv.ParseUint(givenUserIdString, 10, 32)
|
||||
givenUserId = uint(num)
|
||||
@@ -85,7 +88,7 @@ func userGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
|
||||
}
|
||||
|
||||
// Fetch the user details
|
||||
u, err := store.Users.Get(config.Server.Root, givenUserId)
|
||||
u, err := store.Users.Get(files.RootPaths["default"], givenUserId)
|
||||
if err == errors.ErrNotExist {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
@@ -156,7 +159,8 @@ func usersPostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
}
|
||||
|
||||
// Validate the user's scope
|
||||
_, _, err := files.GetRealPath(config.Server.Root, d.user.Scope)
|
||||
idx := files.GetIndex("default")
|
||||
_, _, err := idx.GetRealPath(d.user.Scope)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
@@ -214,7 +218,8 @@ func userPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
|
||||
}
|
||||
|
||||
// Validate the user's scope
|
||||
_, _, err := files.GetRealPath(config.Server.Root, d.user.Scope)
|
||||
idx := files.GetIndex("default")
|
||||
_, _, err := idx.GetRealPath(d.user.Scope)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"io"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/dsoprea/go-exif/v3"
|
||||
exif "github.com/dsoprea/go-exif/v3"
|
||||
|
||||
exifcommon "github.com/dsoprea/go-exif/v3/common"
|
||||
)
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../frontend/tests/playwright-files/myfolder/testdata/
|
||||
@@ -0,0 +1,141 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Logger wraps the standard log.Logger with log level functionality
|
||||
type Logger struct {
|
||||
logger *log.Logger
|
||||
levels []LogLevel
|
||||
apiLevels []LogLevel
|
||||
stdout bool
|
||||
disabled bool
|
||||
debugEnabled bool
|
||||
disabledAPI bool
|
||||
colors bool
|
||||
}
|
||||
|
||||
var stdOutLoggerExists bool
|
||||
|
||||
// NewLogger creates a new Logger instance with separate file and stdout loggers
|
||||
func NewLogger(filepath string, levels, apiLevels []LogLevel, noColors bool) (*Logger, error) {
|
||||
var fileWriter io.Writer = io.Discard
|
||||
stdout := filepath == ""
|
||||
// Configure file logging
|
||||
if !stdout {
|
||||
file, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open log file: %v", err)
|
||||
}
|
||||
fileWriter = file
|
||||
}
|
||||
var flags int
|
||||
if slices.Contains(levels, DEBUG) {
|
||||
flags |= log.Lshortfile
|
||||
}
|
||||
logger := log.New(os.Stdout, "", flags)
|
||||
if filepath != "" {
|
||||
logger = log.New(fileWriter, "", flags)
|
||||
}
|
||||
if stdout {
|
||||
stdOutLoggerExists = true
|
||||
}
|
||||
return &Logger{
|
||||
logger: logger,
|
||||
levels: levels,
|
||||
apiLevels: apiLevels,
|
||||
disabled: slices.Contains(levels, DISABLED),
|
||||
debugEnabled: slices.Contains(levels, DEBUG),
|
||||
disabledAPI: slices.Contains(apiLevels, DISABLED),
|
||||
colors: !noColors,
|
||||
stdout: stdout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetupLogger configures the logger with file and stdout options and their respective log levels
|
||||
func SetupLogger(output, levels, apiLevels string, noColors bool) error {
|
||||
upperLevels := []LogLevel{}
|
||||
for _, level := range SplitByMultiple(levels) {
|
||||
if level == "" {
|
||||
break
|
||||
}
|
||||
upperLevel := strings.ToUpper(level)
|
||||
if upperLevel == "WARNING" || upperLevel == "WARN" {
|
||||
upperLevel = "WARN "
|
||||
}
|
||||
if upperLevel == "INFO" {
|
||||
upperLevel = "INFO "
|
||||
}
|
||||
// Convert level strings to LogLevel
|
||||
level, ok := stringToLevel[upperLevel]
|
||||
if !ok {
|
||||
loggers = []*Logger{}
|
||||
return fmt.Errorf("invalid file log level: %s", upperLevel)
|
||||
}
|
||||
upperLevels = append(upperLevels, level)
|
||||
}
|
||||
if len(upperLevels) == 0 {
|
||||
upperLevels = []LogLevel{INFO, ERROR, WARNING}
|
||||
}
|
||||
upperApiLevels := []LogLevel{}
|
||||
for _, level := range SplitByMultiple(apiLevels) {
|
||||
if level == "" {
|
||||
break
|
||||
}
|
||||
upperLevel := strings.ToUpper(level)
|
||||
if upperLevel == "WARNING" || upperLevel == "WARN" {
|
||||
upperLevel = "WARN "
|
||||
}
|
||||
if upperLevel == "INFO" {
|
||||
upperLevel = "INFO "
|
||||
}
|
||||
// Convert level strings to LogLevel
|
||||
level, ok := stringToLevel[strings.ToUpper(upperLevel)]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid api log level: %s", upperLevel)
|
||||
}
|
||||
upperApiLevels = append(upperApiLevels, level)
|
||||
}
|
||||
if len(upperApiLevels) == 0 {
|
||||
upperApiLevels = []LogLevel{INFO, ERROR, WARNING}
|
||||
}
|
||||
if slices.Contains(upperLevels, DISABLED) && slices.Contains(upperApiLevels, DISABLED) {
|
||||
// both disabled, not creating a logger
|
||||
loggers = []*Logger{}
|
||||
return nil
|
||||
}
|
||||
outputStdout := strings.ToUpper(output)
|
||||
if outputStdout == "STDOUT" {
|
||||
output = ""
|
||||
}
|
||||
if output == "" && stdOutLoggerExists {
|
||||
// stdout logger already exists... don't create another
|
||||
return fmt.Errorf("stdout logger already exists, could not set config levels=[%v] apiLevels=[%v] noColors=[%v]", levels, apiLevels, noColors)
|
||||
}
|
||||
// Create the logger
|
||||
logger, err := NewLogger(output, upperLevels, upperApiLevels, noColors)
|
||||
if err != nil {
|
||||
loggers = []*Logger{}
|
||||
return err
|
||||
}
|
||||
loggers = append(loggers, logger)
|
||||
return nil
|
||||
}
|
||||
|
||||
func SplitByMultiple(str string) []string {
|
||||
delimiters := []rune{'|', ',', ' '}
|
||||
return strings.FieldsFunc(str, func(r rune) bool {
|
||||
for _, d := range delimiters {
|
||||
if r == d {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LogLevel int
|
||||
|
||||
const (
|
||||
DISABLED LogLevel = 0
|
||||
ERROR LogLevel = 1
|
||||
FATAL LogLevel = 1
|
||||
WARNING LogLevel = 2
|
||||
INFO LogLevel = 3
|
||||
DEBUG LogLevel = 4
|
||||
API LogLevel = 10
|
||||
// COLORS
|
||||
RED = "\033[31m"
|
||||
GREEN = "\033[32m"
|
||||
YELLOW = "\033[33m"
|
||||
GRAY = "\033[37m"
|
||||
)
|
||||
|
||||
var (
|
||||
loggers []*Logger
|
||||
)
|
||||
|
||||
type levelConsts struct {
|
||||
INFO string
|
||||
FATAL string
|
||||
ERROR string
|
||||
WARNING string
|
||||
DEBUG string
|
||||
API string
|
||||
DISABLED string
|
||||
}
|
||||
|
||||
var levels = levelConsts{
|
||||
INFO: "INFO ", // with consistent space padding
|
||||
FATAL: "FATAL",
|
||||
ERROR: "ERROR",
|
||||
WARNING: "WARN ", // with consistent space padding
|
||||
DEBUG: "DEBUG",
|
||||
DISABLED: "DISABLED",
|
||||
API: "API",
|
||||
}
|
||||
|
||||
// stringToLevel maps string representation to LogLevel
|
||||
var stringToLevel = map[string]LogLevel{
|
||||
"DEBUG": DEBUG,
|
||||
"INFO ": INFO, // with consistent space padding
|
||||
"ERROR": ERROR,
|
||||
"DISABLED": DISABLED,
|
||||
"WARN ": WARNING, // with consistent space padding
|
||||
"FATAL": FATAL,
|
||||
"API": API,
|
||||
}
|
||||
|
||||
// Log prints a log message if its level is greater than or equal to the logger's levels
|
||||
func Log(level string, msg string, prefix, api bool, color string) {
|
||||
LEVEL := stringToLevel[level]
|
||||
for _, logger := range loggers {
|
||||
if api {
|
||||
if logger.disabledAPI || !slices.Contains(logger.apiLevels, LEVEL) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if logger.disabled || !slices.Contains(logger.levels, LEVEL) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if logger.stdout && level == "FATAL" {
|
||||
continue
|
||||
}
|
||||
writeOut := msg
|
||||
formattedTime := time.Now().Format("2006/01/02 15:04:05")
|
||||
if logger.colors && color != "" {
|
||||
formattedTime = formattedTime + color
|
||||
}
|
||||
if prefix || logger.debugEnabled {
|
||||
logger.logger.SetPrefix(fmt.Sprintf("%s [%s] ", formattedTime, level))
|
||||
} else {
|
||||
logger.logger.SetPrefix(formattedTime + " ")
|
||||
}
|
||||
if logger.colors && color != "" {
|
||||
writeOut = writeOut + "\033[0m"
|
||||
}
|
||||
err := logger.logger.Output(3, writeOut) // 3 skips this function for correct file:line
|
||||
if err != nil {
|
||||
log.Printf("failed to log message '%v' with error `%v`", msg, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Api(msg string, statusCode int) {
|
||||
if statusCode >= 300 && statusCode < 500 {
|
||||
Log(levels.WARNING, msg, false, true, YELLOW)
|
||||
} else if statusCode >= 500 {
|
||||
Log(levels.ERROR, msg, false, true, RED)
|
||||
} else {
|
||||
Log(levels.INFO, msg, false, true, GREEN)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods for specific log levels
|
||||
func Debug(msg string) {
|
||||
if len(loggers) > 0 {
|
||||
Log(levels.DEBUG, msg, true, false, GRAY)
|
||||
} else {
|
||||
log.Println("[DEBUG] ", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func Info(msg string) {
|
||||
if len(loggers) > 0 {
|
||||
Log(levels.INFO, msg, false, false, "")
|
||||
} else {
|
||||
log.Println(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func Warning(msg string) {
|
||||
if len(loggers) > 0 {
|
||||
Log(levels.WARNING, msg, true, false, YELLOW)
|
||||
} else {
|
||||
log.Println("[WARN ] ", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func Error(msg string) {
|
||||
if len(loggers) > 0 {
|
||||
Log(levels.ERROR, msg, true, false, RED)
|
||||
} else {
|
||||
log.Println("[ERROR] ", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func Fatal(msg string) {
|
||||
if len(loggers) > 0 {
|
||||
Log(levels.FATAL, msg, true, false, RED)
|
||||
}
|
||||
log.Fatal("[FATAL] ", msg)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/sh
|
||||
## TEST file used by docker testing containers
|
||||
checkExit() {
|
||||
if [ "$?" -ne 0 ];then
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
if command -v go &> /dev/null
|
||||
then
|
||||
printf "\n == Running tests == \n"
|
||||
go test -race -parallel -v ./...
|
||||
checkExit
|
||||
else
|
||||
echo "ERROR: unable to perform tests"
|
||||
exit 1
|
||||
fi
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"runtime"
|
||||
"unicode"
|
||||
|
||||
"github.com/flynn/go-shlex"
|
||||
shlex "github.com/flynn/go-shlex"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -20,11 +20,12 @@ 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, _, _ = files.GetRealPath(user.Scope, path)
|
||||
dst, _, _ = files.GetRealPath(user.Scope, dst)
|
||||
idx := files.GetIndex("default")
|
||||
path, _, _ = idx.GetRealPath(user.Scope, path)
|
||||
dst, _, _ = idx.GetRealPath(user.Scope, dst)
|
||||
|
||||
if r.Enabled {
|
||||
if val, ok := r.Commands["before_"+evt]; ok {
|
||||
if val, ok := r.Settings.Commands["before_"+evt]; ok {
|
||||
for _, command := range val {
|
||||
err := r.exec(command, "before_"+evt, path, dst, user)
|
||||
if err != nil {
|
||||
@@ -40,7 +41,7 @@ func (r *Runner) RunHook(fn func() error, evt, path, dst string, user *users.Use
|
||||
}
|
||||
|
||||
if r.Enabled {
|
||||
if val, ok := r.Commands["after_"+evt]; ok {
|
||||
if val, ok := r.Settings.Commands["after_"+evt]; ok {
|
||||
for _, command := range val {
|
||||
err := r.exec(command, "after_"+evt, path, dst, user)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
if ! [ -x "$(command -v standard-version)" ]; then
|
||||
echo "standard-version is not installed. please run 'npm i -g standard-version'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
standard-version --dry-run --skip
|
||||
read -p "Continue (y/n)? " -n 1 -r
|
||||
echo ;
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
standard-version -s ;
|
||||
fi
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
if ! [ -x "$(command -v commitlint)" ]; then
|
||||
echo "commitlint is not installed. please run 'npm i -g commitlint'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for commit_hash in $(git log --pretty=format:%H origin/master..HEAD); do
|
||||
commitlint -f ${commit_hash}~1 -t ${commit_hash}
|
||||
done
|
||||
+107
-35
@@ -1,63 +1,121 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/version"
|
||||
)
|
||||
|
||||
var Config Settings
|
||||
|
||||
func Initialize(configFile string) {
|
||||
yamlData := loadConfigFile(configFile)
|
||||
Config = setDefaults()
|
||||
err := yaml.Unmarshal(yamlData, &Config)
|
||||
yamlData, err := loadConfigFile(configFile)
|
||||
if err != nil && configFile != "config.yaml" {
|
||||
logger.Fatal("Could not load specified config file: " + err.Error())
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
||||
logger.Warning(fmt.Sprintf("Could not load config file '%v', using default settings: %v", configFile, err))
|
||||
}
|
||||
Config = setDefaults()
|
||||
err = yaml.Unmarshal(yamlData, &Config)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error unmarshaling YAML data: %v", err))
|
||||
}
|
||||
if len(Config.Server.Logging) == 0 {
|
||||
Config.Server.Logging = []LogConfig{
|
||||
{
|
||||
Output: "stdout",
|
||||
},
|
||||
}
|
||||
}
|
||||
for _, logConfig := range Config.Server.Logging {
|
||||
err = logger.SetupLogger(
|
||||
logConfig.Output,
|
||||
logConfig.Levels,
|
||||
logConfig.ApiLevels,
|
||||
logConfig.NoColors,
|
||||
)
|
||||
if err != nil {
|
||||
log.Println("[ERROR] Failed to set up logger:", err)
|
||||
}
|
||||
}
|
||||
if Config.Auth.Method != "" {
|
||||
logger.Warning("The `auth.method` setting is deprecated and will be removed in a future version. Please use `auth.methods` instead.")
|
||||
}
|
||||
Config.UserDefaults.Perm = Config.UserDefaults.Permissions
|
||||
// Convert relative path to absolute path
|
||||
realRoot, err := filepath.Abs(Config.Server.Root)
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting root path: %v", err)
|
||||
if len(Config.Server.Sources) > 0 {
|
||||
if Config.Server.Root != "" {
|
||||
logger.Warning("`server.root` is configured but will be ignored in favor of `server.sources`")
|
||||
}
|
||||
// TODO allow multiple sources not named default
|
||||
for _, source := range Config.Server.Sources {
|
||||
realPath, err2 := filepath.Abs(source.Path)
|
||||
if err2 != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error getting source path: %v", err2))
|
||||
}
|
||||
source.Path = realPath
|
||||
source.Name = "default"
|
||||
Config.Server.Sources = []Source{source} // temporary set only one source
|
||||
}
|
||||
} else {
|
||||
realPath, err2 := filepath.Abs(Config.Server.Root)
|
||||
if err2 != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error getting source path: %v", err2))
|
||||
}
|
||||
Config.Server.Sources = []Source{
|
||||
{
|
||||
Name: "default",
|
||||
Path: realPath,
|
||||
},
|
||||
}
|
||||
}
|
||||
_, err = os.Stat(realRoot)
|
||||
if err != nil {
|
||||
log.Fatalf("ERROR: Configured Root Path does not exist! %v", err)
|
||||
}
|
||||
Config.Server.Root = realRoot
|
||||
|
||||
baseurl := strings.Trim(Config.Server.BaseURL, "/")
|
||||
if baseurl == "" {
|
||||
Config.Server.BaseURL = "/"
|
||||
} else {
|
||||
Config.Server.BaseURL = "/" + baseurl + "/"
|
||||
}
|
||||
if !Config.Frontend.DisableDefaultLinks {
|
||||
Config.Frontend.ExternalLinks = append(Config.Frontend.ExternalLinks, ExternalLink{
|
||||
Text: fmt.Sprintf("(%v)", version.Version),
|
||||
Title: version.CommitSHA,
|
||||
Url: "https://github.com/gtsteffaniak/filebrowser/releases/",
|
||||
})
|
||||
Config.Frontend.ExternalLinks = append(Config.Frontend.ExternalLinks, ExternalLink{
|
||||
Text: "Help",
|
||||
Url: "https://github.com/gtsteffaniak/filebrowser/wiki",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfigFile(configFile string) []byte {
|
||||
func loadConfigFile(configFile string) ([]byte, error) {
|
||||
// Open and read the YAML file
|
||||
yamlFile, err := os.Open(configFile)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
os.Exit(1)
|
||||
return nil, err
|
||||
}
|
||||
defer yamlFile.Close()
|
||||
|
||||
stat, err := yamlFile.Stat()
|
||||
if err != nil {
|
||||
log.Fatalf("error getting file information: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
yamlData := make([]byte, stat.Size())
|
||||
_, err = yamlFile.Read(yamlData)
|
||||
if err != nil {
|
||||
log.Fatalf("Error reading YAML data: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return yamlData
|
||||
return yamlData, nil
|
||||
}
|
||||
|
||||
func setDefaults() Settings {
|
||||
@@ -70,29 +128,43 @@ func setDefaults() Settings {
|
||||
NumImageProcessors: 4,
|
||||
BaseURL: "",
|
||||
Database: "database.db",
|
||||
Log: "stdout",
|
||||
Root: "/srv",
|
||||
Indexing: true,
|
||||
Root: ".",
|
||||
},
|
||||
Auth: Auth{
|
||||
TokenExpirationTime: "2h",
|
||||
AdminUsername: "admin",
|
||||
AdminPassword: "admin",
|
||||
Method: "password",
|
||||
Signup: false,
|
||||
AdminUsername: "admin",
|
||||
AdminPassword: "admin",
|
||||
TokenExpirationHours: 2,
|
||||
Signup: false,
|
||||
Recaptcha: Recaptcha{
|
||||
Host: "",
|
||||
},
|
||||
Methods: LoginMethods{
|
||||
ProxyAuth: ProxyAuthConfig{
|
||||
Enabled: false,
|
||||
CreateUser: false,
|
||||
Header: "",
|
||||
},
|
||||
NoAuth: false,
|
||||
PasswordAuth: PasswordAuthConfig{
|
||||
Enabled: true,
|
||||
MinLength: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
Frontend: Frontend{
|
||||
Name: "FileBrowser Quantum",
|
||||
},
|
||||
UserDefaults: UserDefaults{
|
||||
StickySidebar: true,
|
||||
Scope: ".",
|
||||
LockPassword: false,
|
||||
HideDotfiles: true,
|
||||
DarkMode: false,
|
||||
DisableSettings: false,
|
||||
ViewMode: "normal",
|
||||
Locale: "en",
|
||||
DisableOnlyOfficeExt: ".txt .csv .html",
|
||||
StickySidebar: true,
|
||||
Scope: ".",
|
||||
LockPassword: false,
|
||||
ShowHidden: false,
|
||||
DarkMode: true,
|
||||
DisableSettings: false,
|
||||
ViewMode: "normal",
|
||||
Locale: "en",
|
||||
GallerySize: 3,
|
||||
Permissions: users.Permissions{
|
||||
Create: false,
|
||||
Rename: false,
|
||||
|
||||
@@ -35,7 +35,7 @@ func Test_loadConfigFile(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := loadConfigFile(tt.args.configFile); !reflect.DeepEqual(got, tt.want) {
|
||||
if got, _ := loadConfigFile(tt.args.configFile); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("loadConfigFile() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,12 +3,13 @@ package settings
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -22,7 +23,7 @@ func (s *Settings) MakeUserDir(username, userScope, serverRoot string) (string,
|
||||
if userScope == "" && s.Server.CreateUserDir {
|
||||
username = cleanUsername(username)
|
||||
if username == "" || username == "-" || username == "." {
|
||||
log.Printf("create user: invalid user for home dir creation: [%s]", username)
|
||||
logger.Error(fmt.Sprintf("create user: invalid user for home dir creation: [%s]", username))
|
||||
return "", errors.New("invalid user for home dir creation")
|
||||
}
|
||||
userScope = path.Join(s.Server.UserHomeBasePath, username)
|
||||
|
||||
@@ -52,7 +52,7 @@ func ApplyUserDefaults(u users.User) users.User {
|
||||
u.Perm = Config.UserDefaults.Perm
|
||||
u.Sorting = Config.UserDefaults.Sorting
|
||||
u.Commands = Config.UserDefaults.Commands
|
||||
u.HideDotfiles = Config.UserDefaults.HideDotfiles
|
||||
u.ShowHidden = Config.UserDefaults.ShowHidden
|
||||
u.DateFormat = Config.UserDefaults.DateFormat
|
||||
return u
|
||||
}
|
||||
@@ -1,22 +1,23 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"log"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
)
|
||||
|
||||
func TestConfigLoadChanged(t *testing.T) {
|
||||
yamlData := loadConfigFile("./testingConfig.yaml")
|
||||
yamlData, _ := loadConfigFile("./testingConfig.yaml")
|
||||
// Marshal the YAML data to a more human-readable format
|
||||
newConfig := setDefaults()
|
||||
Config := setDefaults()
|
||||
|
||||
err := yaml.Unmarshal(yamlData, &newConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
||||
logger.Fatal(fmt.Sprintf("Error unmarshaling YAML data: %v", err))
|
||||
}
|
||||
// Use go-cmp to compare the two structs
|
||||
if diff := cmp.Diff(newConfig, Config); diff == "" {
|
||||
@@ -25,30 +26,26 @@ func TestConfigLoadChanged(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigLoadSpecificValues(t *testing.T) {
|
||||
yamlData := loadConfigFile("./testingConfig.yaml")
|
||||
yamlData, _ := loadConfigFile("./testingConfig.yaml")
|
||||
// Marshal the YAML data to a more human-readable format
|
||||
newConfig := setDefaults()
|
||||
Config := setDefaults()
|
||||
|
||||
err := yaml.Unmarshal(yamlData, &newConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
||||
logger.Fatal(fmt.Sprintf("Error unmarshaling YAML data: %v", err))
|
||||
}
|
||||
testCases := []struct {
|
||||
fieldName string
|
||||
globalVal interface{}
|
||||
newVal interface{}
|
||||
}{
|
||||
{"Auth.Method", Config.Auth.Method, newConfig.Auth.Method},
|
||||
{"Auth.Method", Config.Auth.Method, newConfig.Auth.Method},
|
||||
{"Frontend.disableExternal", Config.Frontend.DisableExternal, newConfig.Frontend.DisableExternal},
|
||||
{"UserDefaults.HideDotfiles", Config.UserDefaults.HideDotfiles, newConfig.UserDefaults.HideDotfiles},
|
||||
{"Server.Database", Config.Server.Database, newConfig.Server.Database},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
if tc.globalVal == tc.newVal {
|
||||
t.Errorf("Differences should have been found:\n\tConfig.%s: %v \n\tSetConfig: %v \n", tc.fieldName, tc.globalVal, tc.newVal)
|
||||
t.Errorf("Differences should have been found:\nConfig.%s: %v \nSetConfig: %v \n", tc.fieldName, tc.globalVal, tc.newVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
-39
@@ -4,6 +4,14 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
type AllowedMethods string
|
||||
|
||||
const (
|
||||
ProxyAuth AllowedMethods = "proxyAuth"
|
||||
NoAuth AllowedMethods = "noAuth"
|
||||
PasswordAuth AllowedMethods = "passwordAuth"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
Commands map[string][]string `json:"commands"`
|
||||
Shell []string `json:"shell"`
|
||||
@@ -13,19 +21,37 @@ type Settings struct {
|
||||
Frontend Frontend `json:"frontend"`
|
||||
Users []UserDefaults `json:"users,omitempty"`
|
||||
UserDefaults UserDefaults `json:"userDefaults"`
|
||||
Integrations Integrations `json:"integrations"`
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
TokenExpirationTime string `json:"tokenExpirationTime"`
|
||||
Recaptcha Recaptcha `json:"recaptcha"`
|
||||
Header string `json:"header"`
|
||||
Method string `json:"method"`
|
||||
Command string `json:"command"`
|
||||
Signup bool `json:"signup"`
|
||||
Shell string `json:"shell"`
|
||||
AdminUsername string `json:"adminUsername"`
|
||||
AdminPassword string `json:"adminPassword"`
|
||||
Key []byte `json:"key"`
|
||||
TokenExpirationHours int `json:"tokenExpirationHours"`
|
||||
Recaptcha Recaptcha `json:"recaptcha"`
|
||||
Methods LoginMethods `json:"methods"`
|
||||
Command string `json:"command"`
|
||||
Signup bool `json:"signup"`
|
||||
Method string `json:"method"`
|
||||
Shell string `json:"shell"`
|
||||
Key []byte `json:"key"`
|
||||
AdminUsername string `json:"adminUsername"`
|
||||
AdminPassword string `json:"adminPassword"`
|
||||
}
|
||||
|
||||
type LoginMethods struct {
|
||||
ProxyAuth ProxyAuthConfig `json:"proxy"`
|
||||
NoAuth bool `json:"noauth"`
|
||||
PasswordAuth PasswordAuthConfig `json:"password"`
|
||||
}
|
||||
|
||||
type PasswordAuthConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
MinLength int `json:"minLength"`
|
||||
}
|
||||
|
||||
type ProxyAuthConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
CreateUser bool `json:"createUser"`
|
||||
Header string `json:"header"`
|
||||
}
|
||||
|
||||
type Recaptcha struct {
|
||||
@@ -35,33 +61,82 @@ type Recaptcha struct {
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
IndexingInterval uint32 `json:"indexingInterval"`
|
||||
NumImageProcessors int `json:"numImageProcessors"`
|
||||
Socket string `json:"socket"`
|
||||
TLSKey string `json:"tlsKey"`
|
||||
TLSCert string `json:"tlsCert"`
|
||||
EnableThumbnails bool `json:"enableThumbnails"`
|
||||
ResizePreview bool `json:"resizePreview"`
|
||||
EnableExec bool `json:"enableExec"`
|
||||
TypeDetectionByHeader bool `json:"typeDetectionByHeader"`
|
||||
AuthHook string `json:"authHook"`
|
||||
Port int `json:"port"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
Address string `json:"address"`
|
||||
Log string `json:"log"`
|
||||
Database string `json:"database"`
|
||||
Root string `json:"root"`
|
||||
UserHomeBasePath string `json:"userHomeBasePath"`
|
||||
CreateUserDir bool `json:"createUserDir"`
|
||||
Indexing bool `json:"indexing"`
|
||||
NumImageProcessors int `json:"numImageProcessors"`
|
||||
Socket string `json:"socket"`
|
||||
TLSKey string `json:"tlsKey"`
|
||||
TLSCert string `json:"tlsCert"`
|
||||
EnableThumbnails bool `json:"enableThumbnails"`
|
||||
ResizePreview bool `json:"resizePreview"`
|
||||
EnableExec bool `json:"enableExec"`
|
||||
AuthHook string `json:"authHook"`
|
||||
Port int `json:"port"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
Logging []LogConfig `json:"logging"`
|
||||
Database string `json:"database"`
|
||||
Root string `json:"root"`
|
||||
UserHomeBasePath string `json:"userHomeBasePath"`
|
||||
CreateUserDir bool `json:"createUserDir"`
|
||||
Sources []Source `json:"sources"`
|
||||
ExternalUrl string `json:"externalUrl"`
|
||||
InternalUrl string `json:"internalUrl"` // used by integrations
|
||||
CacheDir string `json:"cacheDir"`
|
||||
}
|
||||
|
||||
type Integrations struct {
|
||||
OnlyOffice OnlyOffice `json:"office"`
|
||||
}
|
||||
|
||||
// onlyoffice secret is stored in the local.json file
|
||||
// docker exec <containerID> /var/www/onlyoffice/documentserver/npm/json -f /etc/onlyoffice/documentserver/local.json 'services.CoAuthoring.secret.session.string'
|
||||
type OnlyOffice struct {
|
||||
Url string `json:"url"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Levels string `json:"levels"`
|
||||
ApiLevels string `json:"apiLevels"`
|
||||
Output string `json:"output"`
|
||||
NoColors bool `json:"noColors"`
|
||||
Json bool `json:"json"`
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Config IndexConfig `json:"config"`
|
||||
}
|
||||
|
||||
type IndexConfig struct {
|
||||
IndexingInterval uint32 `json:"indexingInterval"`
|
||||
Disabled bool `json:"disabled"`
|
||||
MaxWatchers int `json:"maxWatchers"`
|
||||
NeverWatch []string `json:"neverWatchPaths"`
|
||||
IgnoreHidden bool `json:"ignoreHidden"`
|
||||
IgnoreZeroSizeFolders bool `json:"ignoreZeroSizeFolders"`
|
||||
Exclude IndexFilter `json:"exclude"`
|
||||
Include IndexFilter `json:"include"`
|
||||
}
|
||||
|
||||
type IndexFilter struct {
|
||||
Files []string `json:"files"`
|
||||
Folders []string `json:"folders"`
|
||||
FileEndsWith []string `json:"fileEndsWith"`
|
||||
}
|
||||
|
||||
type Frontend struct {
|
||||
Name string `json:"name"`
|
||||
DisableExternal bool `json:"disableExternal"`
|
||||
DisableUsedPercentage bool `json:"disableUsedPercentage"`
|
||||
Files string `json:"files"`
|
||||
Color string `json:"color"`
|
||||
Name string `json:"name"`
|
||||
DisableDefaultLinks bool `json:"disableDefaultLinks"`
|
||||
DisableUsedPercentage bool `json:"disableUsedPercentage"`
|
||||
Files string `json:"files"`
|
||||
Color string `json:"color"`
|
||||
ExternalLinks []ExternalLink `json:"externalLinks"`
|
||||
}
|
||||
|
||||
type ExternalLink struct {
|
||||
Text string `json:"text"`
|
||||
Title string `json:"title"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
// UserDefaults is a type that holds the default values
|
||||
@@ -81,9 +156,12 @@ type UserDefaults 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 users.Permissions `json:"perm"`
|
||||
Permissions users.Permissions `json:"permissions"`
|
||||
Commands []string `json:"commands,omitempty"`
|
||||
ShowHidden bool `json:"showHidden"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
ThemeColor string `json:"themeColor"`
|
||||
QuickDownload bool `json:"quickDownload"`
|
||||
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"`
|
||||
}
|
||||
@@ -6,20 +6,16 @@ server:
|
||||
tlsCert: ""
|
||||
enableThumbnails: false
|
||||
resizePreview: true
|
||||
typeDetectionByHeader: true
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
address: ""
|
||||
log: "stdout"
|
||||
database: "mydb.db"
|
||||
root: "/srv"
|
||||
root: "."
|
||||
auth:
|
||||
recaptcha:
|
||||
host: ""
|
||||
key: ""
|
||||
secret: ""
|
||||
header: ""
|
||||
method: json
|
||||
command: ""
|
||||
signup: false
|
||||
shell: ""
|
||||
@@ -48,5 +44,4 @@ userDefaults:
|
||||
delete: true
|
||||
share: true
|
||||
download: true
|
||||
hideDotfiles: false
|
||||
dateFormat: false
|
||||
@@ -1,7 +1,7 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/asdine/storm/v3"
|
||||
storm "github.com/asdine/storm/v3"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/asdine/storm/v3"
|
||||
storm "github.com/asdine/storm/v3"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
@@ -14,6 +14,9 @@ func NewStorage(db *storm.DB) (*auth.Storage, *users.Storage, *share.Storage, *s
|
||||
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)
|
||||
authStore, err := auth.NewStorage(authBackend{db: db}, userStore)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
return authStore, userStore, shareStore, settingsStore, nil
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/asdine/storm/v3"
|
||||
storm "github.com/asdine/storm/v3"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/asdine/storm/v3"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
storm "github.com/asdine/storm/v3"
|
||||
"github.com/asdine/storm/v3/q"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/share"
|
||||
)
|
||||
|
||||
@@ -59,7 +63,20 @@ func (s shareBackend) Gets(path string, id uint) ([]*share.Link, error) {
|
||||
return v, errors.ErrNotExist
|
||||
}
|
||||
|
||||
return v, err
|
||||
filteredList := []*share.Link{}
|
||||
// automatically delete and clear expired shares
|
||||
for i := range v {
|
||||
if v[i].Expire < time.Now().Unix() {
|
||||
err = s.Delete(v[i].PasswordHash)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("expired share could not be deleted: %v", err.Error()))
|
||||
}
|
||||
} else {
|
||||
filteredList = append(filteredList, v[i])
|
||||
}
|
||||
}
|
||||
|
||||
return filteredList, err
|
||||
}
|
||||
|
||||
func (s shareBackend) Save(l *share.Link) error {
|
||||
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
storm "github.com/asdine/storm/v3"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
@@ -19,16 +20,21 @@ func (st usersBackend) GetBy(i interface{}) (user *users.User, err error) {
|
||||
user = &users.User{}
|
||||
|
||||
var arg string
|
||||
switch i.(type) {
|
||||
var val interface{}
|
||||
switch i := i.(type) {
|
||||
case uint:
|
||||
val = i
|
||||
arg = "ID"
|
||||
case int:
|
||||
val = uint(i)
|
||||
case string:
|
||||
arg = "Username"
|
||||
val = i
|
||||
default:
|
||||
return nil, errors.ErrInvalidDataType
|
||||
}
|
||||
|
||||
err = st.db.One(arg, i, user)
|
||||
err = st.db.One(arg, val, user)
|
||||
|
||||
if err != nil {
|
||||
if err == storm.ErrNotFound {
|
||||
@@ -80,6 +86,9 @@ func (st usersBackend) Update(user *users.User, fields ...string) error {
|
||||
}
|
||||
|
||||
func (st usersBackend) Save(user *users.User) error {
|
||||
if len(user.Password) < settings.Config.Auth.Methods.PasswordAuth.MinLength {
|
||||
return fmt.Errorf("password must be at least %d characters long", settings.Config.Auth.Methods.PasswordAuth.MinLength)
|
||||
}
|
||||
pass, err := users.HashPwd(user.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/asdine/storm/v3"
|
||||
storm "github.com/asdine/storm/v3"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
)
|
||||
|
||||
+13
-15
@@ -2,14 +2,14 @@ package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
storm "github.com/asdine/storm/v3"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage/bolt"
|
||||
@@ -80,14 +80,6 @@ func dbExists(path string) (bool, error) {
|
||||
|
||||
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)
|
||||
@@ -116,16 +108,22 @@ func CreateUser(userInfo users.User, asAdmin bool) error {
|
||||
newUser.Perm = settings.AdminPerms()
|
||||
}
|
||||
// create new home directory
|
||||
userHome, err := settings.Config.MakeUserDir(newUser.Username, newUser.Scope, settings.Config.Server.Root)
|
||||
userHome, err := settings.Config.MakeUserDir(newUser.Username, newUser.Scope, files.RootPaths["default"])
|
||||
if err != nil {
|
||||
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
||||
logger.Error(fmt.Sprintf("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)
|
||||
logger.Debug(fmt.Sprintf("user: %s, home dir: [%s].", newUser.Username, userHome))
|
||||
|
||||
// todo: fix this, requries index path to be set
|
||||
idx := files.GetIndex("default")
|
||||
if idx == nil {
|
||||
idx = files.GetIndex("default")
|
||||
}
|
||||
_, _, err = idx.GetRealPath(newUser.Scope)
|
||||
if err != nil {
|
||||
log.Println("user path is not valid", newUser.Scope)
|
||||
logger.Error(fmt.Sprintf("user path is not valid: %v", newUser.Scope))
|
||||
return nil
|
||||
}
|
||||
err = store.Users.Save(&newUser)
|
||||
|
||||
@@ -199,6 +199,12 @@ const docTemplate = `{
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name for the desired source, default is used if not provided",
|
||||
"name": "source",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name for the desired source, default is used if not provided",
|
||||
@@ -265,6 +271,12 @@ const docTemplate = `{
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name for the desired source, default is used if not provided",
|
||||
"name": "source",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name for the desired source, default is used if not provided",
|
||||
@@ -334,6 +346,12 @@ const docTemplate = `{
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name for the desired source, default is used if not provided",
|
||||
"name": "source",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name for the desired source, default is used if not provided",
|
||||
@@ -409,6 +427,12 @@ const docTemplate = `{
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name for the desired source, default is used if not provided",
|
||||
"name": "source",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name for the desired source, default is used if not provided",
|
||||
@@ -469,6 +493,12 @@ const docTemplate = `{
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name for the desired source, default is used if not provided",
|
||||
"name": "source",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Destination path for the resource",
|
||||
@@ -857,7 +887,7 @@ const docTemplate = `{
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name for the desired source, default is used if not provided",
|
||||
"description": "Source name for the desired source, default is used if not provided",
|
||||
"name": "source",
|
||||
"in": "query"
|
||||
}
|
||||
@@ -1141,30 +1171,41 @@ const docTemplate = `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"files": {
|
||||
"description": "files in the directory",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/files.ItemInfo"
|
||||
}
|
||||
},
|
||||
"folders": {
|
||||
"description": "folders in the directory",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/files.ItemInfo"
|
||||
}
|
||||
},
|
||||
"hidden": {
|
||||
"description": "whether the file is hidden",
|
||||
"type": "boolean"
|
||||
},
|
||||
"modified": {
|
||||
"description": "modification time",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the file",
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"description": "path scoped to the associated index",
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"description": "length in bytes for regular files",
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"description": "type of the file, either \"directory\" or a file mimetype",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -1172,16 +1213,24 @@ const docTemplate = `{
|
||||
"files.ItemInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hidden": {
|
||||
"description": "whether the file is hidden",
|
||||
"type": "boolean"
|
||||
},
|
||||
"modified": {
|
||||
"description": "modification time",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the file",
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"description": "length in bytes for regular files",
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"description": "type of the file, either \"directory\" or a file mimetype",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -1260,18 +1309,38 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.ExternalLink": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.Frontend": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableExternal": {
|
||||
"disableDefaultLinks": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableUsedPercentage": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"externalLinks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/settings.ExternalLink"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1295,15 +1364,15 @@ const docTemplate = `{
|
||||
"dateFormat": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableOnlyOfficeExt": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableSettings": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"gallerySize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"hideDotfiles": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"locale": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1316,6 +1385,9 @@ const docTemplate = `{
|
||||
"permissions": {
|
||||
"$ref": "#/definitions/users.Permissions"
|
||||
},
|
||||
"quickDownload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -1325,6 +1397,9 @@ const docTemplate = `{
|
||||
"scope": {
|
||||
"type": "string"
|
||||
},
|
||||
"showHidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"singleClick": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1342,6 +1417,9 @@ const docTemplate = `{
|
||||
"stickySidebar": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"viewMode": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -1500,15 +1578,15 @@ const docTemplate = `{
|
||||
"dateFormat": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableOnlyOfficeExt": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableSettings": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"gallerySize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"hideDotfiles": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -1524,6 +1602,9 @@ const docTemplate = `{
|
||||
"perm": {
|
||||
"$ref": "#/definitions/users.Permissions"
|
||||
},
|
||||
"quickDownload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -1533,6 +1614,9 @@ const docTemplate = `{
|
||||
"scope": {
|
||||
"type": "string"
|
||||
},
|
||||
"showHidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"singleClick": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1542,6 +1626,9 @@ const docTemplate = `{
|
||||
"stickySidebar": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -188,6 +188,12 @@
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name for the desired source, default is used if not provided",
|
||||
"name": "source",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name for the desired source, default is used if not provided",
|
||||
@@ -254,6 +260,12 @@
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name for the desired source, default is used if not provided",
|
||||
"name": "source",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name for the desired source, default is used if not provided",
|
||||
@@ -323,6 +335,12 @@
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name for the desired source, default is used if not provided",
|
||||
"name": "source",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name for the desired source, default is used if not provided",
|
||||
@@ -398,6 +416,12 @@
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name for the desired source, default is used if not provided",
|
||||
"name": "source",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name for the desired source, default is used if not provided",
|
||||
@@ -458,6 +482,12 @@
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name for the desired source, default is used if not provided",
|
||||
"name": "source",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Destination path for the resource",
|
||||
@@ -846,7 +876,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name for the desired source, default is used if not provided",
|
||||
"description": "Source name for the desired source, default is used if not provided",
|
||||
"name": "source",
|
||||
"in": "query"
|
||||
}
|
||||
@@ -1130,30 +1160,41 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"files": {
|
||||
"description": "files in the directory",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/files.ItemInfo"
|
||||
}
|
||||
},
|
||||
"folders": {
|
||||
"description": "folders in the directory",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/files.ItemInfo"
|
||||
}
|
||||
},
|
||||
"hidden": {
|
||||
"description": "whether the file is hidden",
|
||||
"type": "boolean"
|
||||
},
|
||||
"modified": {
|
||||
"description": "modification time",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the file",
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"description": "path scoped to the associated index",
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"description": "length in bytes for regular files",
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"description": "type of the file, either \"directory\" or a file mimetype",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -1161,16 +1202,24 @@
|
||||
"files.ItemInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hidden": {
|
||||
"description": "whether the file is hidden",
|
||||
"type": "boolean"
|
||||
},
|
||||
"modified": {
|
||||
"description": "modification time",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the file",
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"description": "length in bytes for regular files",
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"description": "type of the file, either \"directory\" or a file mimetype",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -1249,18 +1298,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.ExternalLink": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.Frontend": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableExternal": {
|
||||
"disableDefaultLinks": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableUsedPercentage": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"externalLinks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/settings.ExternalLink"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1284,15 +1353,15 @@
|
||||
"dateFormat": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableOnlyOfficeExt": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableSettings": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"gallerySize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"hideDotfiles": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"locale": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1305,6 +1374,9 @@
|
||||
"permissions": {
|
||||
"$ref": "#/definitions/users.Permissions"
|
||||
},
|
||||
"quickDownload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -1314,6 +1386,9 @@
|
||||
"scope": {
|
||||
"type": "string"
|
||||
},
|
||||
"showHidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"singleClick": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1331,6 +1406,9 @@
|
||||
"stickySidebar": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"viewMode": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -1489,15 +1567,15 @@
|
||||
"dateFormat": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableOnlyOfficeExt": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableSettings": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"gallerySize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"hideDotfiles": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -1513,6 +1591,9 @@
|
||||
"perm": {
|
||||
"$ref": "#/definitions/users.Permissions"
|
||||
},
|
||||
"quickDownload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -1522,6 +1603,9 @@
|
||||
"scope": {
|
||||
"type": "string"
|
||||
},
|
||||
"showHidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"singleClick": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1531,6 +1615,9 @@
|
||||
"stickySidebar": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -2,33 +2,50 @@ definitions:
|
||||
files.FileInfo:
|
||||
properties:
|
||||
files:
|
||||
description: files in the directory
|
||||
items:
|
||||
$ref: '#/definitions/files.ItemInfo'
|
||||
type: array
|
||||
folders:
|
||||
description: folders in the directory
|
||||
items:
|
||||
$ref: '#/definitions/files.ItemInfo'
|
||||
type: array
|
||||
hidden:
|
||||
description: whether the file is hidden
|
||||
type: boolean
|
||||
modified:
|
||||
description: modification time
|
||||
type: string
|
||||
name:
|
||||
description: name of the file
|
||||
type: string
|
||||
path:
|
||||
description: path scoped to the associated index
|
||||
type: string
|
||||
size:
|
||||
description: length in bytes for regular files
|
||||
type: integer
|
||||
type:
|
||||
description: type of the file, either "directory" or a file mimetype
|
||||
type: string
|
||||
type: object
|
||||
files.ItemInfo:
|
||||
properties:
|
||||
hidden:
|
||||
description: whether the file is hidden
|
||||
type: boolean
|
||||
modified:
|
||||
description: modification time
|
||||
type: string
|
||||
name:
|
||||
description: name of the file
|
||||
type: string
|
||||
size:
|
||||
description: length in bytes for regular files
|
||||
type: integer
|
||||
type:
|
||||
description: type of the file, either "directory" or a file mimetype
|
||||
type: string
|
||||
type: object
|
||||
files.SearchResult:
|
||||
@@ -79,14 +96,27 @@ definitions:
|
||||
userHomeBasePath:
|
||||
type: string
|
||||
type: object
|
||||
settings.ExternalLink:
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
settings.Frontend:
|
||||
properties:
|
||||
color:
|
||||
type: string
|
||||
disableExternal:
|
||||
disableDefaultLinks:
|
||||
type: boolean
|
||||
disableUsedPercentage:
|
||||
type: boolean
|
||||
externalLinks:
|
||||
items:
|
||||
$ref: '#/definitions/settings.ExternalLink'
|
||||
type: array
|
||||
files:
|
||||
type: string
|
||||
name:
|
||||
@@ -102,12 +132,12 @@ definitions:
|
||||
type: boolean
|
||||
dateFormat:
|
||||
type: boolean
|
||||
disableOnlyOfficeExt:
|
||||
type: string
|
||||
disableSettings:
|
||||
type: boolean
|
||||
gallerySize:
|
||||
type: integer
|
||||
hideDotfiles:
|
||||
type: boolean
|
||||
locale:
|
||||
type: string
|
||||
lockPassword:
|
||||
@@ -116,12 +146,16 @@ definitions:
|
||||
$ref: '#/definitions/users.Permissions'
|
||||
permissions:
|
||||
$ref: '#/definitions/users.Permissions'
|
||||
quickDownload:
|
||||
type: boolean
|
||||
rules:
|
||||
items:
|
||||
$ref: '#/definitions/users.Rule'
|
||||
type: array
|
||||
scope:
|
||||
type: string
|
||||
showHidden:
|
||||
type: boolean
|
||||
singleClick:
|
||||
type: boolean
|
||||
sorting:
|
||||
@@ -133,6 +167,8 @@ definitions:
|
||||
type: object
|
||||
stickySidebar:
|
||||
type: boolean
|
||||
themeColor:
|
||||
type: string
|
||||
viewMode:
|
||||
type: string
|
||||
type: object
|
||||
@@ -239,12 +275,12 @@ definitions:
|
||||
type: boolean
|
||||
dateFormat:
|
||||
type: boolean
|
||||
disableOnlyOfficeExt:
|
||||
type: string
|
||||
disableSettings:
|
||||
type: boolean
|
||||
gallerySize:
|
||||
type: integer
|
||||
hideDotfiles:
|
||||
type: boolean
|
||||
id:
|
||||
type: integer
|
||||
locale:
|
||||
@@ -255,18 +291,24 @@ definitions:
|
||||
type: string
|
||||
perm:
|
||||
$ref: '#/definitions/users.Permissions'
|
||||
quickDownload:
|
||||
type: boolean
|
||||
rules:
|
||||
items:
|
||||
$ref: '#/definitions/users.Rule'
|
||||
type: array
|
||||
scope:
|
||||
type: string
|
||||
showHidden:
|
||||
type: boolean
|
||||
singleClick:
|
||||
type: boolean
|
||||
sorting:
|
||||
$ref: '#/definitions/users.Sorting'
|
||||
stickySidebar:
|
||||
type: boolean
|
||||
themeColor:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
viewMode:
|
||||
@@ -398,6 +440,10 @@ paths:
|
||||
name: path
|
||||
required: true
|
||||
type: string
|
||||
- description: Source name for the desired source, default is used if not provided
|
||||
in: query
|
||||
name: source
|
||||
type: string
|
||||
- description: Name for the desired source, default is used if not provided
|
||||
in: query
|
||||
name: source
|
||||
@@ -439,6 +485,10 @@ paths:
|
||||
name: path
|
||||
required: true
|
||||
type: string
|
||||
- description: Source name for the desired source, default is used if not provided
|
||||
in: query
|
||||
name: source
|
||||
type: string
|
||||
- description: Name for the desired source, default is used if not provided
|
||||
in: query
|
||||
name: source
|
||||
@@ -483,6 +533,10 @@ paths:
|
||||
name: from
|
||||
required: true
|
||||
type: string
|
||||
- description: Source name for the desired source, default is used if not provided
|
||||
in: query
|
||||
name: source
|
||||
type: string
|
||||
- description: Destination path for the resource
|
||||
in: query
|
||||
name: destination
|
||||
@@ -544,6 +598,10 @@ paths:
|
||||
name: path
|
||||
required: true
|
||||
type: string
|
||||
- description: Source name for the desired source, default is used if not provided
|
||||
in: query
|
||||
name: source
|
||||
type: string
|
||||
- description: Name for the desired source, default is used if not provided
|
||||
in: query
|
||||
name: source
|
||||
@@ -594,6 +652,10 @@ paths:
|
||||
name: path
|
||||
required: true
|
||||
type: string
|
||||
- description: Source name for the desired source, default is used if not provided
|
||||
in: query
|
||||
name: source
|
||||
type: string
|
||||
- description: Name for the desired source, default is used if not provided
|
||||
in: query
|
||||
name: source
|
||||
@@ -842,7 +904,7 @@ paths:
|
||||
- application/json
|
||||
description: Returns the total and used disk space for a specified directory.
|
||||
parameters:
|
||||
- description: Name for the desired source, default is used if not provided
|
||||
- description: Source name for the desired source, default is used if not provided
|
||||
in: query
|
||||
name: source
|
||||
type: string
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
|
||||
== Running tests ==
|
||||
/usr/local/go/bin/go
|
||||
? github.com/gtsteffaniak/filebrowser [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/auth [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/cmd [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/errors [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/files [no test files]
|
||||
=== RUN TestFileCache
|
||||
--- PASS: TestFileCache (0.00s)
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/diskcache (cached)
|
||||
=== RUN TestCommonPrefix
|
||||
=== RUN TestCommonPrefix/sub_folder
|
||||
=== RUN TestCommonPrefix/relative_path
|
||||
=== RUN TestCommonPrefix/no_common_path
|
||||
=== RUN TestCommonPrefix/same_lvl
|
||||
--- PASS: TestCommonPrefix (0.00s)
|
||||
--- PASS: TestCommonPrefix/sub_folder (0.00s)
|
||||
--- PASS: TestCommonPrefix/relative_path (0.00s)
|
||||
--- PASS: TestCommonPrefix/no_common_path (0.00s)
|
||||
--- PASS: TestCommonPrefix/same_lvl (0.00s)
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/fileutils (cached)
|
||||
? github.com/gtsteffaniak/filebrowser/settings [no test files]
|
||||
? 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]
|
||||
2023/09/02 13:07:17 Error opening YAML file: open filebrowser.yaml: no such file or directory
|
||||
FAIL github.com/gtsteffaniak/filebrowser/http 0.008s
|
||||
=== RUN TestService_Resize
|
||||
=== RUN TestService_Resize/convert_to_png
|
||||
=== RUN TestService_Resize/convert_to_tiff
|
||||
=== RUN TestService_Resize/resize_bmp
|
||||
=== RUN TestService_Resize/resize_with_medium_quality
|
||||
=== RUN TestService_Resize/resize_with_low_quality
|
||||
=== RUN TestService_Resize/get_thumbnail_from_file_with_APP0_JFIF
|
||||
=== RUN TestService_Resize/fill_upscale
|
||||
=== RUN TestService_Resize/fit_upscale
|
||||
=== RUN TestService_Resize/convert_to_gif
|
||||
=== RUN TestService_Resize/convert_to_bmp
|
||||
=== RUN TestService_Resize/resize_tiff
|
||||
=== RUN TestService_Resize/resize_with_high_quality
|
||||
=== RUN TestService_Resize/fill_downscale
|
||||
=== RUN TestService_Resize/keep_original_format
|
||||
=== RUN TestService_Resize/convert_to_unknown
|
||||
=== RUN TestService_Resize/get_thumbnail_from_file_without_APP0_JFIF
|
||||
=== RUN TestService_Resize/resize_for_higher_quality_levels
|
||||
=== RUN TestService_Resize/broken_file
|
||||
=== RUN TestService_Resize/fit_downscale
|
||||
=== RUN TestService_Resize/convert_to_jpeg
|
||||
=== RUN TestService_Resize/resize_png
|
||||
=== RUN TestService_Resize/resize_gif
|
||||
=== RUN TestService_Resize/resize_with_unknown_quality
|
||||
=== RUN TestService_Resize/resize_from_file_without_IFD1_thumbnail
|
||||
--- PASS: TestService_Resize (1.36s)
|
||||
--- PASS: TestService_Resize/convert_to_png (0.01s)
|
||||
--- PASS: TestService_Resize/convert_to_tiff (0.01s)
|
||||
--- PASS: TestService_Resize/resize_bmp (0.01s)
|
||||
--- PASS: TestService_Resize/resize_with_medium_quality (0.01s)
|
||||
--- PASS: TestService_Resize/resize_with_low_quality (0.01s)
|
||||
--- PASS: TestService_Resize/get_thumbnail_from_file_with_APP0_JFIF (0.02s)
|
||||
--- PASS: TestService_Resize/fill_upscale (0.01s)
|
||||
--- PASS: TestService_Resize/fit_upscale (0.00s)
|
||||
--- PASS: TestService_Resize/convert_to_gif (0.01s)
|
||||
--- PASS: TestService_Resize/convert_to_bmp (0.01s)
|
||||
--- PASS: TestService_Resize/resize_tiff (0.01s)
|
||||
--- PASS: TestService_Resize/resize_with_high_quality (0.01s)
|
||||
--- PASS: TestService_Resize/fill_downscale (0.01s)
|
||||
--- PASS: TestService_Resize/keep_original_format (0.01s)
|
||||
--- PASS: TestService_Resize/convert_to_unknown (0.01s)
|
||||
--- PASS: TestService_Resize/get_thumbnail_from_file_without_APP0_JFIF (0.03s)
|
||||
--- PASS: TestService_Resize/resize_for_higher_quality_levels (0.03s)
|
||||
--- PASS: TestService_Resize/broken_file (0.00s)
|
||||
--- PASS: TestService_Resize/fit_downscale (0.01s)
|
||||
--- PASS: TestService_Resize/convert_to_jpeg (0.01s)
|
||||
--- PASS: TestService_Resize/resize_png (0.02s)
|
||||
--- PASS: TestService_Resize/resize_gif (0.02s)
|
||||
--- PASS: TestService_Resize/resize_with_unknown_quality (0.01s)
|
||||
--- PASS: TestService_Resize/resize_from_file_without_IFD1_thumbnail (1.09s)
|
||||
=== RUN TestService_FormatFromExtension
|
||||
=== RUN TestService_FormatFromExtension/gif
|
||||
=== RUN TestService_FormatFromExtension/tiff
|
||||
=== RUN TestService_FormatFromExtension/bmp
|
||||
=== RUN TestService_FormatFromExtension/unknown
|
||||
=== RUN TestService_FormatFromExtension/jpg
|
||||
=== RUN TestService_FormatFromExtension/jpeg
|
||||
=== RUN TestService_FormatFromExtension/png
|
||||
--- PASS: TestService_FormatFromExtension (0.00s)
|
||||
--- PASS: TestService_FormatFromExtension/gif (0.00s)
|
||||
--- PASS: TestService_FormatFromExtension/tiff (0.00s)
|
||||
--- PASS: TestService_FormatFromExtension/bmp (0.00s)
|
||||
--- PASS: TestService_FormatFromExtension/unknown (0.00s)
|
||||
--- PASS: TestService_FormatFromExtension/jpg (0.00s)
|
||||
--- PASS: TestService_FormatFromExtension/jpeg (0.00s)
|
||||
--- PASS: TestService_FormatFromExtension/png (0.00s)
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/img (cached)
|
||||
=== RUN TestMatchHidden
|
||||
--- PASS: TestMatchHidden (0.00s)
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/rules (cached)
|
||||
2023/09/02 13:07:17 Error opening YAML file: open filebrowser.yaml: no such file or directory
|
||||
FAIL github.com/gtsteffaniak/filebrowser/runner 0.007s
|
||||
=== RUN TestParseSearch
|
||||
--- PASS: TestParseSearch (0.00s)
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/search (cached)
|
||||
? github.com/gtsteffaniak/filebrowser/version [no test files]
|
||||
testing: warning: no tests to run
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/users (cached) [no tests to run]
|
||||
FAIL
|
||||
+24
-21
@@ -3,7 +3,7 @@ package users
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
jwt "github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
type AuthToken struct {
|
||||
@@ -36,31 +36,34 @@ type Sorting struct {
|
||||
|
||||
// User describes a user.
|
||||
type User struct {
|
||||
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,omitempty"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Sorting Sorting `json:"sorting"`
|
||||
Perm Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
Rules []Rule `json:"rules"`
|
||||
ApiKeys map[string]AuthToken `json:"apiKeys,omitempty"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
GallerySize int `json:"gallerySize"`
|
||||
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,omitempty"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Sorting Sorting `json:"sorting"`
|
||||
Perm Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
Rules []Rule `json:"rules"`
|
||||
ApiKeys map[string]AuthToken `json:"apiKeys,omitempty"`
|
||||
ShowHidden bool `json:"showHidden"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
GallerySize int `json:"gallerySize"`
|
||||
ThemeColor string `json:"themeColor"`
|
||||
QuickDownload bool `json:"quickDownload"`
|
||||
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"`
|
||||
}
|
||||
|
||||
var PublicUser = User{
|
||||
Username: "publicUser", // temp user not registered
|
||||
Password: "publicUser", // temp user not registered
|
||||
Scope: "./",
|
||||
Scope: "/does/not/exist",
|
||||
ViewMode: "normal",
|
||||
LockPassword: true,
|
||||
Perm: Permissions{
|
||||
|
||||
+13
-5
@@ -2,17 +2,20 @@ package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
math "math/rand"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
)
|
||||
|
||||
func CheckErr(source string, err error) {
|
||||
if err != nil {
|
||||
log.Fatalf("%s: %v", source, err)
|
||||
logger.Fatal(fmt.Sprintf("%s: %v", source, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +36,7 @@ func CapitalizeFirst(s string) string {
|
||||
return strings.ToUpper(string(s[0])) + s[1:]
|
||||
}
|
||||
|
||||
func GenerateRandomHash(length int) string {
|
||||
func InsecureRandomIdentifier(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
math.New(math.NewSource(time.Now().UnixNano()))
|
||||
result := make([]byte, length)
|
||||
@@ -49,7 +52,7 @@ func PrintStructFields(v interface{}) {
|
||||
|
||||
// Ensure the input is a struct
|
||||
if val.Kind() != reflect.Struct {
|
||||
fmt.Println("Provided value is not a struct")
|
||||
logger.Debug("Provided value is not a struct")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -66,7 +69,7 @@ func PrintStructFields(v interface{}) {
|
||||
fieldValue = fieldValue[:100] + "..."
|
||||
}
|
||||
|
||||
fmt.Printf("Field: %s, %s\n", fieldType.Name, fieldValue)
|
||||
logger.Debug(fmt.Sprintf("Field: %s, %s\n", fieldType.Name, fieldValue))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,3 +87,8 @@ func GetParentDirectoryPath(path string) string {
|
||||
}
|
||||
return path[:lastSlash]
|
||||
}
|
||||
|
||||
func HashSHA256(data string) string {
|
||||
bytes := sha256.Sum256([]byte(data))
|
||||
return hex.EncodeToString(bytes[:])
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
# Configuration Help
|
||||
|
||||
This document covers the available configuration options, their defaults, and how they affect the functionality of filebrowser.
|
||||
|
||||
## All possible configurations
|
||||
|
||||
Here is an expanded config file which includes all possible configurations:
|
||||
|
||||
```
|
||||
server:
|
||||
CreateUserDir: false
|
||||
UserHomeBasePath: ""
|
||||
indexing: true
|
||||
numImageProcessors: 4
|
||||
socket: ""
|
||||
tlsKey: ""
|
||||
tlsCert: ""
|
||||
enableThumbnails: false
|
||||
resizePreview: true
|
||||
typeDetectionByHeader: true
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
address: ""
|
||||
log: "stdout"
|
||||
database: "/database/database.db"
|
||||
root: "/srv"
|
||||
auth:
|
||||
adminUsername: admin
|
||||
adminPassword: admin
|
||||
recaptcha:
|
||||
host: ""
|
||||
key: ""
|
||||
secret: ""
|
||||
tokenExpirationTime: 2h
|
||||
header: ""
|
||||
method: json
|
||||
signup: false
|
||||
frontend:
|
||||
name: ""
|
||||
disableExternal: false
|
||||
disableUsedPercentage: true
|
||||
files: ""
|
||||
color: ""
|
||||
userDefaults:
|
||||
settingsAllowed: true
|
||||
darkMode: false
|
||||
scope: ""
|
||||
locale: "en"
|
||||
viewMode: ""
|
||||
singleClick: true
|
||||
sorting:
|
||||
by: ""
|
||||
asc: true
|
||||
permissions:
|
||||
admin: true
|
||||
execute: true
|
||||
create: true
|
||||
rename: true
|
||||
modify: true
|
||||
delete: true
|
||||
share: true
|
||||
download: true
|
||||
hideDotfiles: false
|
||||
dateFormat: false
|
||||
```
|
||||
|
||||
Here are the defaults if nothing is set:
|
||||
|
||||
```
|
||||
server:
|
||||
enableThumbnails: true
|
||||
enableExec: false
|
||||
port: 80
|
||||
numImageProcessors: 4
|
||||
baseURL: ""
|
||||
database: database.db
|
||||
log: stdout
|
||||
root: /srv
|
||||
auth:
|
||||
adminUsername: admin
|
||||
adminPassword: admin
|
||||
method: password
|
||||
recaptcha:
|
||||
host: ""
|
||||
userDefaults:
|
||||
settingsAllowed: true
|
||||
darkMode: false
|
||||
scope: ""
|
||||
locale: "en"
|
||||
scope: "."
|
||||
lockPassword: false
|
||||
hideDotfiles: true
|
||||
permissions:
|
||||
create: true
|
||||
rename: true
|
||||
modify: true
|
||||
delete: true
|
||||
share: true
|
||||
download: true
|
||||
```
|
||||
|
||||
## About each configuration
|
||||
|
||||
# Configuration Settings Documentation
|
||||
|
||||
## About each configuration
|
||||
|
||||
### Server configuration settings
|
||||
|
||||
- `indexingInterval`: This optional paramter disables smart indexing and specifies a time in minutes the system waits before checking for filesystem changes. See [indexing readme](indexing.md) for more information.
|
||||
|
||||
- `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.
|
||||
|
||||
- `tlsKey`: This is the TLS key configuration.
|
||||
|
||||
- `tlsCert`: This is the TLS certificate configuration.
|
||||
|
||||
- `enableThumbnails`: This boolean value determines whether thumbnails are enabled on ui. Default: `true`
|
||||
|
||||
- `resizePreview`: This boolean value determines whether preview resizing is enabled. Default: `false`
|
||||
|
||||
- `typeDetectionByHeader`: This boolean value determines whether type detection is based on headers.
|
||||
|
||||
- `port`: This is the port number on which the server is running. Default: `80`
|
||||
|
||||
- `baseURL`: This is the base URL for the server. Default: `""`
|
||||
|
||||
- `address`: This is the server address configuration. Default: `0.0.0.0`
|
||||
|
||||
- `log`: This specifies the log destination. Default: `stdout`
|
||||
|
||||
- `database`: This is the database file path + filename that will be created if it does not already exist. If it exists, it will use the existing file. Default `database.db`
|
||||
|
||||
- `root`: This is the root directory path. Default: `/srv`
|
||||
|
||||
- `CreateUserDir`: Boolean to create user directory on user creation. Default: `false`
|
||||
|
||||
- `UserHomeBasePath`: String to define user home directory base path. Default: `""`
|
||||
|
||||
### Auth configuration settings
|
||||
|
||||
- `recaptcha`:
|
||||
|
||||
- `host`: This is the host for reCAPTCHA.
|
||||
|
||||
- `key`: This is the reCAPTCHA key.
|
||||
|
||||
- `secret`: This is the reCAPTCHA secret.
|
||||
|
||||
- `header`: This is the authentication header.
|
||||
|
||||
- `method`: This is the authentication method used (e.g., "json"). Possible values:
|
||||
- `password` - username and password
|
||||
- `hook` - hook authentication
|
||||
- `proxy` - proxy authentication
|
||||
- `oath` - oath authentication
|
||||
- `noauth` - no authentication/login required.
|
||||
|
||||
- `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`
|
||||
|
||||
- `AdminPassword`: This is the password of the admin user. Default: `admin`
|
||||
|
||||
### Frontend configuration settings
|
||||
|
||||
- `name`: This is the name of the frontend.
|
||||
|
||||
- `disableExternal`: This boolean value determines whether external access is disabled.
|
||||
|
||||
- `disableUsedPercentage`: This boolean value determines whether used percentage is disabled.
|
||||
|
||||
- `files`: This is the files configuration.
|
||||
|
||||
- `theme`: This is the theme configuration.
|
||||
|
||||
- `color`: This is the color configuration.
|
||||
|
||||
### UserDefaults configuration settings
|
||||
|
||||
- `darkMode`: Determines whether dark mode is enabled for the user (`true` or `false`)
|
||||
|
||||
- `settingsAllowed`: Determines whether settings page is enabled for the user (`true` or `false`)
|
||||
|
||||
- `scope`: This is a scope of the permissions, "." or "./" means all directories, "./downloads" would mean only the downloads folder.
|
||||
|
||||
- `locale`: String locale configuration. Default: `en`
|
||||
|
||||
- `viewMode`: This is the view mode configuration. Possible values: `normal`, `compact`, `list`, and `gallery`. default: `normal`
|
||||
|
||||
- `singleClick`: This boolean value determines whether single-click is enabled. (`true` or `false`)
|
||||
|
||||
- `sorting`:
|
||||
|
||||
- `by`: This is the sorting method used (e.g., "asc").
|
||||
|
||||
- `asc`: This boolean value determines the sorting order is ascending or descending. (`true` or `false`)
|
||||
|
||||
- `permissions`:
|
||||
|
||||
- `admin`: This boolean value determines whether admin permissions are granted.
|
||||
|
||||
- `execute`: This boolean value determines whether execute permissions are granted.
|
||||
|
||||
- `create`: This boolean value determines whether create permissions are granted.
|
||||
|
||||
- `rename`: This boolean value determines whether rename permissions are granted.
|
||||
|
||||
- `modify`: This boolean value determines whether modify permissions are granted.
|
||||
|
||||
- `delete`: This boolean value determines whether delete permissions are granted.
|
||||
|
||||
- `share`: This boolean value determines whether share permissions are granted.
|
||||
|
||||
- `download`: This boolean value determines whether download permissions are granted.
|
||||
|
||||
- `api`: Ability to create and manage API keys.
|
||||
|
||||
- `hideDotfiles`: This boolean value determines whether dotfiles are hidden. (`true` or `false`)
|
||||
|
||||
- `dateFormat`: This boolean value determines whether date formatting is enabled. (`true` or `false`)
|
||||
Loaded 100 of 233 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user