Compare commits
97
Commits
v0.3.5
...
v0.7.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8d6043c99 | ||
|
|
afbf4ca4c5 | ||
|
|
8136731f81 | ||
|
|
384a4a301b | ||
|
|
40c4cb925e | ||
|
|
58c89cf561 | ||
|
|
3bb68760a9 | ||
|
|
f19ec9a221 | ||
|
|
492b477b7d | ||
|
|
43cbba7d89 | ||
|
|
e66cdfde43 | ||
|
|
a0130f6ae4 | ||
|
|
69e410e992 | ||
|
|
2f66dd3442 | ||
|
|
a07853763d | ||
|
|
2e2fe5200c | ||
|
|
d3390ee585 | ||
|
|
813762caba | ||
|
|
16793c3851 | ||
|
|
6715479758 | ||
|
|
7ae2a8c851 | ||
|
|
5ef01021a4 | ||
|
|
acbd976a5c | ||
|
|
0c7d35d8cd | ||
|
|
f249e38dd9 | ||
|
|
b6ebd91775 | ||
|
|
db816c1fe8 | ||
|
|
ceb8eda39f | ||
|
|
99198fbe19 | ||
|
|
418b187acf | ||
|
|
2972772dda | ||
|
|
36e547bb56 | ||
|
|
cb6ad047f8 | ||
|
|
a5559ec3e2 | ||
|
|
698a3e126c | ||
|
|
f6702414b2 | ||
|
|
5ea3813d81 | ||
|
|
86e73abfab | ||
|
|
9230169d0f | ||
|
|
147615e627 | ||
|
|
8d82792618 | ||
|
|
06cb471fe3 | ||
|
|
40e76c2a14 | ||
|
|
a83f39d41f | ||
|
|
b531100208 | ||
|
|
2de5dbaf68 | ||
|
|
3161ded372 | ||
|
|
c750b6c4e2 | ||
|
|
db77552bd8 | ||
|
|
2c26b6fc45 | ||
|
|
948add54fc | ||
|
|
9daf88c5dd | ||
|
|
cecdf7ae09 | ||
|
|
d4e5b52dc2 | ||
|
|
85266f09d0 | ||
|
|
f56fa85fa4 | ||
|
|
bafdff14d4 | ||
|
|
23db04bacb | ||
|
|
dfe024ab62 | ||
|
|
ce292c7be1 | ||
|
|
e39cf2ddf9 | ||
|
|
6909df0071 | ||
|
|
8460ddbc13 | ||
|
|
be374d75f5 | ||
|
|
c153a39543 | ||
|
|
3d51a5af7a | ||
|
|
e6850feddc | ||
|
|
a6f7883e64 | ||
|
|
f28115afc3 | ||
|
|
c4ccfd48f5 | ||
|
|
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
@@ -1,4 +1,5 @@
|
||||
backend/**.db
|
||||
backend/tmp
|
||||
backend/vendor/**
|
||||
frontend/dist/**
|
||||
frontend/node_modules/**
|
||||
@@ -1,20 +1,10 @@
|
||||
**Description**
|
||||
<!--
|
||||
Please explain the changes you made here.
|
||||
If the feature changes current behaviour, explain why your solution is better.
|
||||
-->
|
||||
|
||||
:rotating_light: Before submitting your PR, please read [community](https://github.com/filebrowser/community), and indicate which issues (in any of the repos) are either fixed or closed by this PR. See [GitHub Help: Closing issues using keywords](https://help.github.com/articles/closing-issues-via-commit-messages/).
|
||||
According to the [contributing guide](https://github.com/gtsteffaniak/filebrowser/wiki/Contributing#contributing-as-an-unofficial-contributor), A PR should contain:
|
||||
|
||||
- [ ] DO make sure you are requesting to **pull a topic/feature/bugfix branch** (right side). Don't request your master!
|
||||
- [ ] DO make sure you are making a pull request against the **master branch** (left side). Also you should start *your branch* off *our master*.
|
||||
- [ ] DO make sure that File Browser can be successfully built. See [builds](https://github.com/filebrowser/community/blob/master/builds.md) and [development](https://github.com/filebrowser/community/blob/master/development.md).
|
||||
- [ ] DO make sure that related issues are opened in other repositories. I.e., the frontend, caddy plugins or the web page need to be updated accordingly.
|
||||
- [ ] AVOID breaking the continuous integration build.
|
||||
- [ ] A clear description of why it was opened.
|
||||
- [ ] A short title that best describes the change.
|
||||
- [ ] Must pass unit and integration tests, which can be run checked locally prior to opening a PR.
|
||||
- [ ] Any additional details for functionality not covered by tests.
|
||||
|
||||
**Further comments**
|
||||
<!--
|
||||
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did, what alternatives you considered, etc.
|
||||
|
||||
:heart: Thank you!
|
||||
-->
|
||||
**Additional Details**
|
||||
@@ -0,0 +1,58 @@
|
||||
name: Create Dev Version
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to create (format: 0.0.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
create-dev-branch:
|
||||
name: Create Dev Branch
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }} # Uses the Personal Access Token
|
||||
|
||||
- name: Validate version format
|
||||
id: validate_version
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "❌ ERROR: Version must be in '0.0.0' format."
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Version format validated: $VERSION"
|
||||
|
||||
- name: Check if branch already exists
|
||||
id: check_branch
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
DEV_BRANCH="dev/v$VERSION"
|
||||
|
||||
if git ls-remote --exit-code origin "$DEV_BRANCH"; then
|
||||
echo "❌ ERROR: Branch '$DEV_BRANCH' already exists!"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Branch '$DEV_BRANCH' does not exist."
|
||||
|
||||
- name: Create and push new dev branch
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
DEV_BRANCH="dev/v$VERSION"
|
||||
|
||||
# Fetch latest changes
|
||||
git fetch origin main
|
||||
|
||||
# Create new branch from main
|
||||
git checkout -b "$DEV_BRANCH" origin/main
|
||||
|
||||
# Push branch to remote
|
||||
git push origin "$DEV_BRANCH"
|
||||
|
||||
echo "✅ Successfully created branch '$DEV_BRANCH'!"
|
||||
+13
-27
@@ -6,41 +6,27 @@ on:
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
push_latest_to_registry:
|
||||
name: Push latest
|
||||
test_playwright:
|
||||
name: Test Playwright
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Find latest tag
|
||||
run: |
|
||||
echo "LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV
|
||||
echo "latest tag is $LATEST_TAG"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i && npm run build
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: gtstef/filebrowser
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go build -o filebrowser .
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
build-args: |
|
||||
VERSION=${{ env.LATEST_TAG }}
|
||||
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: 'gtstef/filebrowser:latest'
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
file: ./_docker/Dockerfile.playwright-general
|
||||
push: false
|
||||
+78
-21
@@ -1,28 +1,85 @@
|
||||
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_playwright:
|
||||
name: Test Playwright - general
|
||||
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-general
|
||||
push: false
|
||||
test_playwright_proxy:
|
||||
name: Test Playwright - proxy
|
||||
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-proxy
|
||||
push: false
|
||||
test_playwright_noauth:
|
||||
name: Test Playwright - noauth
|
||||
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-noauth
|
||||
push: false
|
||||
push_pr_to_registry:
|
||||
name: Push PR
|
||||
runs-on: ubuntu-latest
|
||||
@@ -47,8 +104,8 @@ jobs:
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
file: ./_docker/Dockerfile
|
||||
push: false # Do not push the image for now
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
name: Promote Beta to Stable
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to promote (format: 0.0.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
promote-beta-to-stable:
|
||||
name: Promote Beta to Stable
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }} # Uses Personal Access Token for branch operations
|
||||
|
||||
- name: Validate version format
|
||||
id: validate_version
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "❌ ERROR: Version must be in '0.0.0' format."
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Version format validated: $VERSION"
|
||||
|
||||
- name: Check if beta branch exists
|
||||
id: check_beta_branch
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
BETA_BRANCH="beta/v$VERSION"
|
||||
|
||||
if ! git ls-remote --exit-code origin "$BETA_BRANCH"; then
|
||||
echo "❌ ERROR: Beta branch '$BETA_BRANCH' does not exist!"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Beta branch '$BETA_BRANCH' exists."
|
||||
|
||||
- name: Check if stable branch already exists
|
||||
id: check_stable_branch
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
STABLE_BRANCH="stable/v$VERSION"
|
||||
|
||||
if git ls-remote --exit-code origin "$STABLE_BRANCH"; then
|
||||
echo "❌ ERROR: Stable branch '$STABLE_BRANCH' already exists!"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Stable branch '$STABLE_BRANCH' does not exist."
|
||||
|
||||
- name: Create and push new stable branch
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
BETA_BRANCH="beta/v$VERSION"
|
||||
STABLE_BRANCH="stable/v$VERSION"
|
||||
|
||||
# Fetch latest changes
|
||||
git fetch origin "$BETA_BRANCH"
|
||||
|
||||
# Create new stable branch from beta branch
|
||||
git checkout -b "$STABLE_BRANCH" origin/"$BETA_BRANCH"
|
||||
|
||||
# Push new stable branch to remote
|
||||
git push origin "$STABLE_BRANCH"
|
||||
|
||||
echo "✅ Successfully created stable branch '$STABLE_BRANCH' from '$BETA_BRANCH'."
|
||||
|
||||
- name: Merge beta into stable
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
BETA_BRANCH="beta/v$VERSION"
|
||||
STABLE_BRANCH="stable/v$VERSION"
|
||||
|
||||
# Checkout stable branch
|
||||
git checkout "$STABLE_BRANCH"
|
||||
|
||||
# Merge beta into stable
|
||||
git merge --no-ff "origin/$BETA_BRANCH" -m "Merge $BETA_BRANCH into $STABLE_BRANCH"
|
||||
|
||||
# Push merge changes
|
||||
git push origin "$STABLE_BRANCH"
|
||||
|
||||
echo "✅ Successfully merged '$BETA_BRANCH' into '$STABLE_BRANCH'."
|
||||
|
||||
# - name: Delete beta branch after successful merge
|
||||
# run: |
|
||||
# VERSION="${{ github.event.inputs.version }}"
|
||||
# BETA_BRANCH="beta/v$VERSION"
|
||||
#
|
||||
# # Delete beta branch locally and remotely
|
||||
# git branch -d "$BETA_BRANCH"
|
||||
# git push origin --delete "$BETA_BRANCH"
|
||||
#
|
||||
# echo "✅ Successfully deleted '$BETA_BRANCH'."
|
||||
|
||||
- name: Create Pull Request to Main
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
commit-message: "Promote stable/v${{ github.event.inputs.version }} to main"
|
||||
title: "Promote stable/v${{ github.event.inputs.version }} to main"
|
||||
body: "This PR promotes stable/v${{ github.event.inputs.version }} to the main branch."
|
||||
base: main
|
||||
branch: stable/v${{ github.event.inputs.version }}
|
||||
delete-branch: false
|
||||
@@ -0,0 +1,99 @@
|
||||
name: Promote Dev to Beta
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to promote (format: 0.0.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
promote-dev-to-beta:
|
||||
name: Promote Dev to Beta
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }} # Uses the Personal Access Token
|
||||
|
||||
- name: Validate version format
|
||||
id: validate_version
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "❌ ERROR: Version must be in 'v0.0.0' format."
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Version format validated: $VERSION"
|
||||
|
||||
- name: Check if dev branch exists
|
||||
id: check_dev_branch
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
DEV_BRANCH="dev/v$VERSION"
|
||||
|
||||
if ! git ls-remote --exit-code origin "$DEV_BRANCH"; then
|
||||
echo "❌ ERROR: Dev branch '$DEV_BRANCH' does not exist!"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Dev branch '$DEV_BRANCH' exists."
|
||||
|
||||
- name: Check if beta branch already exists
|
||||
id: check_beta_branch
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
BETA_BRANCH="beta/v$VERSION"
|
||||
|
||||
if git ls-remote --exit-code origin "$BETA_BRANCH"; then
|
||||
echo "❌ ERROR: Beta branch '$BETA_BRANCH' already exists!"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Beta branch '$BETA_BRANCH' does not exist."
|
||||
|
||||
- name: Create and push new beta branch
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
DEV_BRANCH="dev/v$VERSION"
|
||||
BETA_BRANCH="beta/v$VERSION"
|
||||
|
||||
# Fetch latest changes
|
||||
git fetch origin "$DEV_BRANCH"
|
||||
|
||||
# Create new beta branch from dev branch
|
||||
git checkout -b "$BETA_BRANCH" origin/"$DEV_BRANCH"
|
||||
|
||||
# Push new beta branch to remote
|
||||
git push origin "$BETA_BRANCH"
|
||||
|
||||
echo "✅ Successfully created beta branch '$BETA_BRANCH' from '$DEV_BRANCH'."
|
||||
|
||||
- name: Merge dev into beta
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
DEV_BRANCH="dev/v$VERSION"
|
||||
BETA_BRANCH="beta/v$VERSION"
|
||||
|
||||
# Checkout beta branch
|
||||
git checkout "$BETA_BRANCH"
|
||||
|
||||
# Merge dev into beta
|
||||
git merge --no-ff "origin/$DEV_BRANCH" -m "Merge $DEV_BRANCH into $BETA_BRANCH"
|
||||
|
||||
# Push merge changes
|
||||
git push origin "$BETA_BRANCH"
|
||||
|
||||
echo "✅ Successfully merged '$DEV_BRANCH' into '$BETA_BRANCH'."
|
||||
|
||||
# - name: Delete dev branch after successful merge
|
||||
# run: |
|
||||
# VERSION="${{ github.event.inputs.version }}"
|
||||
# DEV_BRANCH="dev/v$VERSION"
|
||||
#
|
||||
# # Delete dev branch locally and remotely
|
||||
# git branch -d "$DEV_BRANCH"
|
||||
# git push origin --delete "$DEV_BRANCH"
|
||||
#
|
||||
# echo "✅ Successfully deleted '$DEV_BRANCH'."
|
||||
@@ -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
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
name: version release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create_release:
|
||||
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: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
|
||||
id: extract_branch
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22.2
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
target_commitish: ${{ steps.extract_branch.outputs.branch }}
|
||||
token: ${{ secrets.PAT }}
|
||||
tag_name: ${{ steps.extract_branch.outputs.branch }}
|
||||
prerelease: false
|
||||
draft: false
|
||||
generate_release_notes: true
|
||||
name: ${{ steps.extract_branch.outputs.branch }}
|
||||
|
||||
push_release_to_registry:
|
||||
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:v/filebrowser:/')
|
||||
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: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -0,0 +1,66 @@
|
||||
name: beta release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "beta/v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
test_playwright:
|
||||
name: Test Playwright - regular
|
||||
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-general
|
||||
push: false
|
||||
create_release_tag:
|
||||
needs: [ test_playwright ]
|
||||
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 }}
|
||||
@@ -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
|
||||
@@ -24,18 +24,30 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{github.actor}}
|
||||
password: ${{ secrets.PAT }}
|
||||
- name: Extract metadata (tags, labels) for Docker and GHCR
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: gtstef/filebrowser
|
||||
- name: Strip v from version number
|
||||
images: |
|
||||
gtstef/filebrowser
|
||||
ghcr.io/gtsteffaniak/filebrowser
|
||||
- name: Modify version names
|
||||
id: modify-json
|
||||
run: |
|
||||
JSON="${{ steps.meta.outputs.tags }}"
|
||||
# Use jq to remove 'v' from the version field
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
|
||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||
TAGS="${{ steps.meta.outputs.tags }}"
|
||||
# Apply modifications to both docker.io and ghcr.io tags
|
||||
CLEANED_TAGS=$(echo "$TAGS" | sed 's/filebrowser:dev-v/filebrowser:/g' | sed -E 's/(filebrowser:[0-9]+\.[0-9]+\.[0-9]+)/\1-dev/g')
|
||||
# Add dev tag to both registries
|
||||
CLEANED_TAGS="$CLEANED_TAGS,gtstef/filebrowser:dev,ghcr.io/gtsteffaniak/filebrowser:dev"
|
||||
echo "cleaned_tag<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CLEANED_TAGS" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
@@ -44,7 +56,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 }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -0,0 +1,65 @@
|
||||
name: stable release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "stable/v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
test_playwright:
|
||||
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-general
|
||||
push: false
|
||||
create_release_tag:
|
||||
needs: [ test_playwright ]
|
||||
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]+')-stable
|
||||
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
|
||||
draft: false
|
||||
generate_release_notes: true
|
||||
name: ${{ steps.extract_branch.outputs.tag_name }}
|
||||
@@ -1,9 +1,9 @@
|
||||
name: tag
|
||||
name: tag update
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
- "v[0-9]+.[0-9]+.[0-9]+*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -29,3 +29,70 @@ jobs:
|
||||
workdir: backend
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
push_release_to_registry:
|
||||
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: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{github.actor}}
|
||||
password: ${{ secrets.PAT }}
|
||||
- name: Extract metadata (tags, labels) for Docker and GHCR
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
gtstef/filebrowser
|
||||
ghcr.io/gtsteffaniak/filebrowser
|
||||
- name: Modify tags (strip 'v' and add 'beta'/'stable' if needed)
|
||||
id: modify-json
|
||||
run: |
|
||||
JSON='${{ steps.meta.outputs.json }}'
|
||||
MODIFIED_JSON=$(echo "$JSON" | jq '
|
||||
.tags |= map(sub(":v"; ":")) |
|
||||
if any(.tags[]; test("beta")) then
|
||||
.tags += [
|
||||
"gtstef/filebrowser:beta",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:beta",
|
||||
"gtstef/filebrowser:latest",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:latest"
|
||||
]
|
||||
else . end |
|
||||
if any(.tags[]; test("stable")) then
|
||||
.tags += [
|
||||
"gtstef/filebrowser:stable",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:stable",
|
||||
"gtstef/filebrowser:latest",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:latest"
|
||||
]
|
||||
else . end
|
||||
')
|
||||
TAGS_CSV=$(echo "$MODIFIED_JSON" | jq -r '.tags | join(",")')
|
||||
echo "$TAGS_CSV" > modified_tags.txt # Optional debug output
|
||||
echo "cleaned_tag=$TAGS_CSV" >> $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 }}
|
||||
+11
-2
@@ -1,19 +1,27 @@
|
||||
*.db
|
||||
*.bak
|
||||
*.log
|
||||
*.mjs
|
||||
_old
|
||||
rice-box.go
|
||||
.idea/
|
||||
/filebrowser
|
||||
/filebrowser.exe
|
||||
/backend/backend
|
||||
/backend/filebrowser
|
||||
/backend/filebrowser.exe
|
||||
/backend/backend.exe
|
||||
/backend/tmp
|
||||
/frontend/dist
|
||||
/frontend/pkg
|
||||
/frontend/test-results
|
||||
/frontend/tests/playwright-files/demo*
|
||||
/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
|
||||
@@ -37,3 +45,4 @@ yarn-error.log*
|
||||
*.sw*
|
||||
bin/
|
||||
build/
|
||||
*__debug*
|
||||
+373
-1
@@ -2,12 +2,384 @@
|
||||
|
||||
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.7.0-beta
|
||||
|
||||
**New Features**:
|
||||
- New authentication method: OIDC (OpenID Connect)
|
||||
- UI refresh
|
||||
- Refreshed icons and styles to provide more contrast https://github.com/gtsteffaniak/filebrowser/issues/493
|
||||
- New scrollbar which includes information about the listing https://github.com/gtsteffaniak/filebrowser/issues/304
|
||||
- User-configurable popup previewer and user can control preview size of images.
|
||||
- Enhanced user settings page with more toggle options.
|
||||
- Replaced checkboxes with toggles switches https://github.com/gtsteffaniak/filebrowser/issues/461
|
||||
- Refreshed Breadcrumbs style.
|
||||
- Main navbar icon is multipurpose menu, close, back and animates
|
||||
- Enhanced source info on the UI
|
||||
- User must have permission `realtime: true` property to get realtime events.
|
||||
- Sources shows status of the directory `ready`, `indexing`, and `unavailable`
|
||||
- Top-right overflow menu for deleting / editing files in peview https://github.com/gtsteffaniak/filebrowser/issues/456
|
||||
- Helpful UI animation for drag and drop files, to get feedback where the drop target is.
|
||||
- More consistent theme color https://github.com/gtsteffaniak/filebrowser/issues/538
|
||||
- New file preview types:
|
||||
- Video thumbnails available via new media integration (see configuration wiki for help) https://github.com/gtsteffaniak/filebrowser/issues/351
|
||||
- Office file previews if you have office integration enabled. https://github.com/gtsteffaniak/filebrowser/issues/460
|
||||
|
||||
**Notes**:
|
||||
- sesssionId is now unique per window. Previously it was shared accross browser tabs.
|
||||
- DisableUsedPercentage is a backend property now, so users can't "hack" the information to be shown.
|
||||
- Updated documentation for resources api https://github.com/gtsteffaniak/filebrowser/issues/560
|
||||
- Updated placeholder for scopes https://github.com/gtsteffaniak/filebrowser/issues/475
|
||||
- When user's API permissions are removed, any api keys the user had will be revoked.
|
||||
- `server.enableThumbnails` moved to `server.disablePreviews` defaulting to false.
|
||||
- `server.resizePreview` moved to `server.resizePreviews` (with an "s" at the end)
|
||||
|
||||
**Bug Fixes**:
|
||||
- Nil pointer error when source media is disconnected while running.
|
||||
- Source selection buggy https://github.com/gtsteffaniak/filebrowser/issues/537
|
||||
- Upload folder structure https://github.com/gtsteffaniak/filebrowser/issues/539
|
||||
- Editing files on multiple sources https://github.com/gtsteffaniak/filebrowser/issues/535
|
||||
- Prevent the user from changing the password https://github.com/gtsteffaniak/filebrowser/issues/550
|
||||
- Links in setting page does not navigate to correct location https://github.com/gtsteffaniak/filebrowser/issues/474
|
||||
- Url encoding issue https://github.com/gtsteffaniak/filebrowser/issues/530
|
||||
- Certain file types being treated as folders https://github.com/gtsteffaniak/filebrowser/issues/555
|
||||
- Source name with special characters https://github.com/gtsteffaniak/filebrowser/issues/557
|
||||
- Onlyoffice support on proxy auth https://github.com/gtsteffaniak/filebrowser/issues/559
|
||||
- Downloading with user scope https://github.com/gtsteffaniak/filebrowser/issues/564
|
||||
- User disableSettings property to be respected.
|
||||
- Non admin users updating admin settings.
|
||||
- Right click context issue on safari desktop.
|
||||
- office save file issue.
|
||||
|
||||
## v0.6.8-beta
|
||||
|
||||
**New Features**
|
||||
- environment variables are available for certain secrets.
|
||||
- see wiki https://github.com/gtsteffaniak/filebrowser/wiki/Environment-Variables
|
||||
- thanks @aaronkyriesenbach https://github.com/gtsteffaniak/filebrowser/pull/511
|
||||
|
||||
**Notes**:
|
||||
- config validation (see https://github.com/gtsteffaniak/filebrowser/wiki/Full-Config-Example)
|
||||
- fails when config file contains unknown fields (helps spot typos)
|
||||
- some light value validation on certain fields
|
||||
- removed recaptcha -- was disabled and not used before.
|
||||
- moved `recaptcha` and `signup` configs to `auth.methods.password`
|
||||
|
||||
**BugFixes**:
|
||||
- fix scope reset on restart https://github.com/gtsteffaniak/filebrowser/issues/515
|
||||
- Clicking empty space to deselect https://github.com/gtsteffaniak/filebrowser/issues/492
|
||||
|
||||
## v0.6.7-beta
|
||||
|
||||
**Notes**:
|
||||
- added full tests for single source example.
|
||||
- adds descriptive error if temp dir can't be created on fatal startup
|
||||
- clears temp directory on shutdown.
|
||||
- removed put settings api (unused)
|
||||
- removed more unused config properties.
|
||||
|
||||
**BugFixes**:
|
||||
- fix url encoding issue for search links when theres only one source https://github.com/gtsteffaniak/filebrowser/issues/501
|
||||
- files with # could have problems, double encoded.
|
||||
|
||||
## v0.6.6-beta
|
||||
|
||||
**New Feature**:
|
||||
- limit tar size creation to limit server burden. For example, don't let customers try to download the entire filesystem as a zip. see `server.maxArchiveSize` on config wiki.
|
||||
|
||||
**Notes**:
|
||||
- disableUsedPercentage also hides text and source bar.
|
||||
- share errors show up in logs in more verbose way.
|
||||
- archive creation occurs on disk rather than in memory, use `server.cacheDir` to determine where temp files are stored.
|
||||
- automatically ensures leading slash for scope
|
||||
- https://github.com/gtsteffaniak/filebrowser/issues/472
|
||||
- https://github.com/gtsteffaniak/filebrowser/issues/476
|
||||
|
||||
**BugFixes**:
|
||||
- fix proxy user creation issue https://github.com/gtsteffaniak/filebrowser/issues/478
|
||||
- externalUrl prefix issue fixed for shares. https://github.com/gtsteffaniak/filebrowser/issues/465
|
||||
- fix File Opens Instead of Just Downloading https://github.com/gtsteffaniak/filebrowser/issues/480
|
||||
- fix Download file name https://github.com/gtsteffaniak/filebrowser/issues/481
|
||||
|
||||
## v0.6.5-beta
|
||||
|
||||
**Notes**:
|
||||
- added more share and download tests
|
||||
|
||||
**BugFixes**:
|
||||
- fix share download issue https://github.com/gtsteffaniak/filebrowser/issues/465
|
||||
- fix content length size calculation issue when downloading multiple files.
|
||||
|
||||
## v0.6.4-beta
|
||||
|
||||
**BugFixes**:
|
||||
- fix preview arow issue. https://github.com/gtsteffaniak/filebrowser/issues/457
|
||||
- fix password change issue.
|
||||
- apply user defaults to publi user on startup https://github.com/gtsteffaniak/filebrowser/issues/451
|
||||
|
||||
## v0.6.3-beta
|
||||
|
||||
**Notes**:
|
||||
- windows directories get better naming, root directories like "D:\ get named "D", otherwise base filepath is the name when unselected "D:\path\to\folder" gets named "folder" (just like linux)
|
||||
- `.pdf` files added to default onlyoffice exclusion list.
|
||||
|
||||
**BugFixes**:
|
||||
- windows would not refresh file info automatically when viewing because of path issue.
|
||||
- windows paths without name for "D:\" would cause issues.
|
||||
- share path error https://github.com/gtsteffaniak/filebrowser/issues/429
|
||||
- fix bug where resource content flag would load entire file into memory.
|
||||
|
||||
## v0.6.2-beta
|
||||
|
||||
**Notes**:
|
||||
- Added playwright tests for bugfixes for permantent fix for stability.
|
||||
(except onlyoffice since it requires integrations)
|
||||
|
||||
**BugFixes**:
|
||||
- Context menu should only be available inside the folder/files container https://github.com/gtsteffaniak/filebrowser/issues/430
|
||||
- drag and drop files from desktop to browser is fixed.
|
||||
- replace prompt cancel button didn't work.
|
||||
- key events on listing page not working (like delete key)
|
||||
- fixed share viewing issue https://github.com/gtsteffaniak/filebrowser/issues/429
|
||||
- disableUsedPercentage hides entire source https://github.com/gtsteffaniak/filebrowser/issues/438
|
||||
- createUserDir fix for proxy users and new users https://github.com/gtsteffaniak/filebrowser/issues/440
|
||||
|
||||
## v0.6.1-beta
|
||||
|
||||
**New Feature**:
|
||||
- download size information is added, including when downloding multiple files in zip/tar.gz. The browser will see the XMB of X GB and will show browser native progress.
|
||||
|
||||
**BugFixes**:
|
||||
- fixed onlyoffice bug https://github.com/gtsteffaniak/filebrowser/issues/418
|
||||
- fixed breadcrumbs bug https://github.com/gtsteffaniak/filebrowser/issues/419
|
||||
- fixed search context bug https://github.com/gtsteffaniak/filebrowser/issues/417
|
||||
- fixed sessionID for search
|
||||
|
||||
## v0.6.0-beta
|
||||
|
||||
> [!WARNING]
|
||||
> This release includes several config changes that could cause issues. Please backup your database file before upgrading.
|
||||
|
||||
This release has several changes that should work without issues... however, still backup your database file first and proceed with caution. User permissions and source config changes have been updated -- and the `server.root` paramter is no longer used.
|
||||
|
||||
This is a significant step towards a stable release. There shouldn't be any major breaking config changes after this.
|
||||
|
||||
**New Features**:
|
||||
- multiple sources support https://github.com/gtsteffaniak/filebrowser/issues/360
|
||||
- listing view keeps them independant, you switch between the two and the url address will have a prefix `/files/<sourcename>/path/to/file` when there is more than 1 source.
|
||||
- search also happens independantly, with a selection toggle per source. searching current source searches the current scope in the listing view, if you toggle to an alternative source it will search from the source root.
|
||||
- copy/moving is currently only supported within the same source -- that will come in a future release.
|
||||
- `FILEBROWSER_CONFIG` environment variable is respected if no CLI config parameter is provided. https://github.com/gtsteffaniak/filebrowser/issues/413
|
||||
|
||||
**Notes**:
|
||||
- downloads no longer open new window.
|
||||
- swagger updated with auth api help for things like api token.
|
||||
- GET api keys now uses `name` query instead of `key`. eg `GET /api/auth/tokens?name=apikeyname`
|
||||
- user permissions simplified to four permission groups (no config change required):
|
||||
- **removed** : create, rename, delete, download
|
||||
- **remaining**: admin, modify, share, api
|
||||
- `scope` is deprecated, but still supported, applies to default source. if using multiple sources, set `defaultUserScope` at the [source config](https://github.com/gtsteffaniak/filebrowser/wiki/Configuration#default-source-configuration) instead.
|
||||
- **removed** user rules and commands.
|
||||
- commands feature has never been enabled so just removing the references.
|
||||
- rules will come back in a different form (not applied to the user).
|
||||
- `server.root` is completely removed in favor of `server.sources`
|
||||
|
||||
**BugFixes**:
|
||||
- fix conflict resolution issue https://github.com/gtsteffaniak/filebrowser/issues/384
|
||||
- many user creation page bugfixes.
|
||||
- fix share delete issue https://github.com/gtsteffaniak/filebrowser/issues/408
|
||||
|
||||
## v0.5.4-beta
|
||||
|
||||
**BugFixes**:
|
||||
- default scope share issue. @theryecatcher https://github.com/gtsteffaniak/filebrowser/pull/387
|
||||
- drag and drop on empty folders https://github.com/gtsteffaniak/filebrowser/issues/361
|
||||
- preview navigation issue https://github.com/gtsteffaniak/filebrowser/issues/372
|
||||
- auth proxy password length error https://github.com/gtsteffaniak/filebrowser/issues/375
|
||||
|
||||
<img width="294" alt="image" src="https://github.com/user-attachments/assets/669bca75-98d4-47c1-838b-1ffee2967d7d" />
|
||||
|
||||
## 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,204 +1,104 @@
|
||||
<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://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>
|
||||
The best free self-hosted web-based file manager.
|
||||
<br/><br/>
|
||||
<img width="800" src="https://github.com/user-attachments/assets/95af22fa-7760-4f7b-9c20-51fdcfe8f4ea" 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 for 2025.
|
||||
|
||||
FileBrowser Quantum is a fork of the file browser opensource project with the following changes:
|
||||
FileBrowser Quantum is a massive 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!
|
||||
- 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.
|
||||
- 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
|
||||
- Ability to create long-live API Tokens.
|
||||
- Helpful Swagger page available at `/swagger` endpoint.
|
||||
1. ✅ Multiple sources support
|
||||
2. ✅ Login support for OIDC, password, and proxy.
|
||||
3. ✅ Revamped UI
|
||||
4. ✅ Simplified configuration via `config.yaml` config file.
|
||||
5. ✅ Ultra-efficient [indexing](https://github.com/gtsteffaniak/filebrowser/wiki/Indexing) and real-time updates
|
||||
- Real-time search results as you type.
|
||||
- Real-time monitoring and updates in the UI.
|
||||
- Search supports file and folder sizes, along with various filters.
|
||||
6. ✅ Better listing browsing
|
||||
- More file type previews, such as **office** and **video** file previews
|
||||
- Instantly switches view modes and sort order without reloading data.
|
||||
- Folder sizes are displayed.
|
||||
- Navigating remembers the last scroll position.
|
||||
7. ✅ Developer API support
|
||||
- Ability to create long-lived API Tokens.
|
||||
- A helpful Swagger page is available at `/swagger` endpoint for API enabled users.
|
||||
|
||||
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 are not supported yet (planned).
|
||||
- ❌ 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.
|
||||
FileBrowser Quantum differs significantly from the original version. Many of these changes required a significant overhaul. Creating a fork was a necessary process to make the program better. There have been many growing pains, but a stable release is planned and coming soon.
|
||||
|
||||
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 version is called "Quantum" because it packs tons of advanced features in a tiny executable file. Unlike the majority of alternative options, FileBrowser Quantum is simple to install and easy to configure.
|
||||
|
||||
FileBrowser Quantum differs significantly from the original.
|
||||
There are hundreds of thousands of lines changed and they are generally
|
||||
no longer compatible with each other. This has been intentional -- the
|
||||
focus of this fork is on a few key principles:
|
||||
- Simplicity and improved user experience
|
||||
- Improving performance and faster feedback when making changes.
|
||||
- Minimize external dependencies and standard library usage.
|
||||
- Of course -- adding much-needed features.
|
||||
The goal for this repo is to become the best open-source self-hosted file browsing application that exists -- **all for free**.
|
||||
|
||||
For more questions, see the [Q&A Readme](./docs/questions.md)
|
||||
This repo will always be free and open-source.
|
||||
|
||||
For more, see the [Q&A Wiki](https://github.com/gtsteffaniak/filebrowser/wiki/Q&A)
|
||||
|
||||
## Look
|
||||
|
||||
The UI has a simple three-component navigation system :
|
||||
The UI has a simple three-component navigation system:
|
||||
|
||||
1. (Left) The slide-out action panel button
|
||||
1. (Left) Multi-action button with slide-out panel.
|
||||
2. (Middle) The powerful search bar.
|
||||
3. (Right) The view change toggle.
|
||||
|
||||
All other functions are moved either into the action menu or popup menus.
|
||||
All other functions are moved either into the action menu or pop-up menus.
|
||||
If the action does not depend on context, it will exist in the slide-out
|
||||
action panel. If the action is available based on context, it will show up as
|
||||
a popup menu.
|
||||
a pop-up 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="1000" src="https://github.com/user-attachments/assets/aa32b05c-f917-47bb-b07f-857edc5e47f7" title="Search GIF">
|
||||
</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,45 +108,41 @@ Self hostable | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
Has Stable Release? | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
S3 support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
webdav support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
ftp support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
FTP support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
Dedicated docs site? | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
Multiple sources at once | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
Multiple sources at once | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
Docker image size | 31 MB | 31 MB | 240 MB (main image) | 250 MB | ❌ | > 2 GB |
|
||||
Min. Memory Requirements | 128 MB | 128 MB | 128 MB (main image) | 128 MB | ❌ | 4 GB |
|
||||
has standalone binary | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
price | free | free | free | free tier | free tier | $99+ |
|
||||
rich media preview | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
upload files from the web? | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
Upload files from the web? | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
Advanced Search? | ✅ | ❌ | ❌ | configurable | ✅ | ✅ |
|
||||
Indexed Search? | ✅ | ❌ | ❌ | configurable | ✅ | ✅ |
|
||||
Content-aware search? | ❌ | ❌ | ❌ | configurable | ✅ | ✅ |
|
||||
Custom job support | ❌ | ✅ | ❌ | ✅ | ❌ | ✅ |
|
||||
Multiple users | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
Single sign-on support | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||
LDAP sign-on support | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||
2FA sign-on support | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||
Single sign-on support | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||
LDAP sign-on support | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||
Long-live API key support | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||
API documentation page | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
Mobile App | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
|
||||
open source? | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
tags support | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ |
|
||||
sharable web links? | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
shareable web links? | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
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
|
||||
@@ -20,10 +20,11 @@ RUN npm run build-docker
|
||||
FROM alpine:latest
|
||||
ENV FILEBROWSER_NO_EMBEDED="true"
|
||||
RUN apk --no-cache add ca-certificates mailcap
|
||||
WORKDIR /home/filebrowser
|
||||
RUN adduser -D -s /bin/true -u 1000 filebrowser
|
||||
USER filebrowser
|
||||
WORKDIR /home/filebrowser
|
||||
COPY --from=base --chown=filebrowser:1000 /app/filebrowser* ./
|
||||
COPY --from=base --chown=filebrowser:1000 /app/config.yaml ./
|
||||
COPY --from=nbuild --chown=filebrowser:1000 /app/dist/ ./http/dist/
|
||||
USER root
|
||||
# exposing default port for auto discovery.
|
||||
@@ -0,0 +1,35 @@
|
||||
FROM mwader/static-ffmpeg AS ffmpeg
|
||||
FROM golang:1.24-alpine AS base
|
||||
ARG VERSION
|
||||
ARG REVISION
|
||||
WORKDIR /app
|
||||
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/backend/version.Version=${VERSION}' \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/version.CommitSHA=${REVISION}'" \
|
||||
-o filebrowser .
|
||||
|
||||
FROM node:slim AS nbuild
|
||||
WORKDIR /app
|
||||
COPY ./frontend/package.json ./
|
||||
RUN npm i --maxsockets 1
|
||||
COPY ./frontend/ ./
|
||||
RUN npm run build-docker
|
||||
|
||||
FROM alpine:latest
|
||||
COPY --from=ffmpeg [ "/ffmpeg", "/ffprobe", "/usr/local/bin/" ]
|
||||
ENV FILEBROWSER_FFMPEG_PATH="/usr/local/bin/"
|
||||
ENV FILEBROWSER_NO_EMBEDED="true"
|
||||
RUN apk --no-cache add ca-certificates mailcap
|
||||
RUN adduser -D -s /bin/true -u 1000 filebrowser
|
||||
USER filebrowser
|
||||
WORKDIR /home/filebrowser
|
||||
COPY --from=base --chown=filebrowser:1000 /app/filebrowser* ./
|
||||
COPY --from=base --chown=filebrowser:1000 /app/config.media.yaml ./config.yaml
|
||||
COPY --from=nbuild --chown=filebrowser:1000 /app/dist/ ./http/dist/
|
||||
USER root
|
||||
# exposing default port for auto discovery.
|
||||
EXPOSE 80
|
||||
ENTRYPOINT [ "./filebrowser" ]
|
||||
@@ -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,8 @@
|
||||
FROM gtstef/playwright-base
|
||||
WORKDIR /app
|
||||
COPY [ "./_docker/src/general/", "./" ]
|
||||
WORKDIR /app/frontend
|
||||
COPY [ "./frontend/", "./" ]
|
||||
WORKDIR /app/backend/
|
||||
COPY [ "./backend/filebrowser*", "./"]
|
||||
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
|
||||
@@ -0,0 +1,8 @@
|
||||
FROM gtstef/playwright-base
|
||||
WORKDIR /app
|
||||
COPY [ "./_docker/src/noauth/", "./" ]
|
||||
WORKDIR /app/frontend
|
||||
COPY [ "./frontend/", "./" ]
|
||||
WORKDIR /app/backend/
|
||||
COPY [ "./backend/filebrowser", "./"]
|
||||
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
|
||||
@@ -0,0 +1,11 @@
|
||||
FROM gtstef/playwright-base
|
||||
WORKDIR /app
|
||||
COPY [ "./_docker/src/proxy/", "./" ]
|
||||
WORKDIR /app/frontend
|
||||
COPY [ "./frontend/", "./" ]
|
||||
WORKDIR /app/backend/
|
||||
COPY [ "./backend/filebrowser", "./"]
|
||||
RUN apt update && apt install nginx -y
|
||||
RUN mv default.conf /etc/nginx/conf.d/default.conf
|
||||
RUN sed -i 's/filebrowser/localhost/g' /etc/nginx/conf.d/default.conf
|
||||
RUN nginx & ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
|
||||
@@ -0,0 +1,16 @@
|
||||
services:
|
||||
nginx-proxy-auth:
|
||||
image: nginx
|
||||
container_name: nginx-proxy-auth
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./src/proxy/backend/default.conf:/etc/nginx/conf.d/default.conf
|
||||
filebrowser:
|
||||
hostname: filebrowser
|
||||
volumes:
|
||||
- '../frontend:/home/frontend'
|
||||
- "./src/proxy/backend/config.yaml:/home/filebrowser/config.yaml"
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: ./_docker/Dockerfile
|
||||
@@ -0,0 +1,16 @@
|
||||
server:
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
logging:
|
||||
- levels: "info|error|debug"
|
||||
sources:
|
||||
- path: "../frontend/tests/playwright-files"
|
||||
- path: "."
|
||||
name: "docker"
|
||||
frontend:
|
||||
name: "Graham's Filebrowser"
|
||||
disableDefaultLinks: true
|
||||
externalLinks:
|
||||
- text: "A playwright test"
|
||||
url: "https://playwright.dev/"
|
||||
title: "Playwright"
|
||||
@@ -0,0 +1,47 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
globalSetup: "./tests-playwright/global-setup",
|
||||
timeout: 5000,
|
||||
testDir: "./tests-playwright/general",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: false,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: false,
|
||||
/* Retry on CI only */
|
||||
retries: 2,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: 1, // required for now! todo parallel some tests
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "line",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
actionTimeout: 5000,
|
||||
storageState: "loginAuth.json",
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1/",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
|
||||
/* Set default locale to English (US) */
|
||||
locale: "en-US",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "firefox",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
server:
|
||||
port: 80
|
||||
baseURL: "/files/"
|
||||
logging:
|
||||
- levels: "info|error|debug"
|
||||
sources:
|
||||
- path: "../frontend/tests/playwright-files"
|
||||
auth:
|
||||
methods:
|
||||
noauth: true
|
||||
frontend:
|
||||
name: "Graham's Filebrowser"
|
||||
disableDefaultLinks: true
|
||||
externalLinks:
|
||||
- text: "A playwright test"
|
||||
url: "https://playwright.dev/"
|
||||
title: "Playwright"
|
||||
userDefaults:
|
||||
permissions:
|
||||
realtime: true
|
||||
@@ -0,0 +1,48 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
globalSetup: "./tests-playwright/noauth-setup",
|
||||
timeout: 5000,
|
||||
testDir: "./tests-playwright/noauth",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: false,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: false,
|
||||
/* Retry on CI only */
|
||||
retries: 2,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: 1, // required for now! todo parallel some tests
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "line",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
storageState: "noauth.json",
|
||||
actionTimeout: 5000,
|
||||
//storageState: "loginAuth.json",
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
|
||||
/* Set default locale to English (US) */
|
||||
locale: "en-US",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "firefox",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
server:
|
||||
port: 8080
|
||||
baseURL: "/"
|
||||
logging:
|
||||
- levels: "info|error|debug"
|
||||
sources:
|
||||
- path: "../frontend/tests/playwright-files"
|
||||
config:
|
||||
defaultEnabled: true
|
||||
createUserDir: true
|
||||
|
||||
frontend:
|
||||
name: "Graham's Filebrowser"
|
||||
disableDefaultLinks: true
|
||||
externalLinks:
|
||||
- text: "A playwright test"
|
||||
url: "https://playwright.dev/"
|
||||
title: "Playwright"
|
||||
|
||||
auth:
|
||||
methods:
|
||||
proxy:
|
||||
enabled: true
|
||||
header: "X-Username"
|
||||
createUser: true
|
||||
|
||||
userDefaults:
|
||||
darkMode: true
|
||||
disableSettings: false
|
||||
singleClick: false
|
||||
permissions:
|
||||
admin: false
|
||||
modify: true
|
||||
share: false
|
||||
api: false
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
map $remote_addr $uuid {
|
||||
default "demo-${remote_addr}";
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost 127.0.0.1;
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Username $uuid;
|
||||
add_header X-Username $uuid;
|
||||
proxy_pass http://filebrowser:8080;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -10,19 +10,23 @@ import { defineConfig, devices } from "@playwright/test";
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
//globalSetup: "./global-setup",
|
||||
timeout: 3000,
|
||||
testDir: "./tests-playwright/proxy",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
fullyParallel: false,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
forbidOnly: false,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
retries: 2,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
workers: 1, // required for now! todo parallel some tests
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "line",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
actionTimeout: 3000,
|
||||
//storageState: "loginAuth.json",
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1",
|
||||
|
||||
@@ -40,4 +44,4 @@ export default defineConfig({
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
+42
-15
@@ -6,36 +6,63 @@ 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/common/version.Version={{ .Version }} -X github.com/gtsteffaniak/filebrowser/backend/common/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 macos without upx
|
||||
- id: macos
|
||||
ldflags: *ldflags
|
||||
main: main.go
|
||||
binary: filebrowser
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
|
||||
# Build configuration for freebsd without arm & upx
|
||||
- id: freeBSD
|
||||
ldflags: *ldflags
|
||||
main: main.go
|
||||
binary: filebrowser
|
||||
goos:
|
||||
- freebsd
|
||||
goarch:
|
||||
- amd64
|
||||
|
||||
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:
|
||||
|
||||
+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()
|
||||
File renamed without changes.
@@ -31,9 +31,10 @@ type FileCache struct {
|
||||
|
||||
// NewFileCache creates a new FileCache
|
||||
func NewFileCache(dir string) (*FileCache, error) {
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
if err := os.MkdirAll(filepath.Join(dir, "thumbnails", "video"), 0700); err != nil {
|
||||
return nil, fmt.Errorf("can't make directory %s: %v", dir, err)
|
||||
}
|
||||
|
||||
return &FileCache{dir: dir}, nil
|
||||
}
|
||||
|
||||
File renamed without changes.
File renamed without changes.
@@ -6,6 +6,10 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
|
||||
)
|
||||
|
||||
func Test_GetRealPath(t *testing.T) {
|
||||
@@ -13,7 +17,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 +35,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 +66,15 @@ func Test_GetRealPath(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
idx := indexing.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)
|
||||
@@ -76,61 +86,61 @@ func Test_GetRealPath(t *testing.T) {
|
||||
func TestSortItems(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input FileInfo
|
||||
expected FileInfo
|
||||
input iteminfo.FileInfo
|
||||
expected iteminfo.FileInfo
|
||||
}{
|
||||
{
|
||||
name: "Numeric and Lexicographical Sorting",
|
||||
input: FileInfo{
|
||||
Folders: []ItemInfo{
|
||||
{Name: "10"},
|
||||
{Name: "2"},
|
||||
input: iteminfo.FileInfo{
|
||||
Folders: []iteminfo.ItemInfo{
|
||||
{Name: "10.txt"},
|
||||
{Name: "2.txt"},
|
||||
{Name: "apple"},
|
||||
{Name: "Banana"},
|
||||
},
|
||||
Files: []ItemInfo{
|
||||
{Name: "File2"},
|
||||
{Name: "File10"},
|
||||
Files: []iteminfo.ItemInfo{
|
||||
{Name: "File2.txt"},
|
||||
{Name: "File10.txt"},
|
||||
{Name: "File1"},
|
||||
{Name: "banana"},
|
||||
},
|
||||
},
|
||||
expected: FileInfo{
|
||||
Folders: []ItemInfo{
|
||||
{Name: "2"},
|
||||
{Name: "10"},
|
||||
expected: iteminfo.FileInfo{
|
||||
Folders: []iteminfo.ItemInfo{
|
||||
{Name: "2.txt"},
|
||||
{Name: "10.txt"},
|
||||
{Name: "apple"},
|
||||
{Name: "Banana"},
|
||||
},
|
||||
Files: []ItemInfo{
|
||||
Files: []iteminfo.ItemInfo{
|
||||
{Name: "banana"},
|
||||
{Name: "File1"},
|
||||
{Name: "File10"},
|
||||
{Name: "File2"},
|
||||
{Name: "File10.txt"},
|
||||
{Name: "File2.txt"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Only Lexicographical Sorting",
|
||||
input: FileInfo{
|
||||
Folders: []ItemInfo{
|
||||
{Name: "dog"},
|
||||
{Name: "Cat"},
|
||||
input: iteminfo.FileInfo{
|
||||
Folders: []iteminfo.ItemInfo{
|
||||
{Name: "dog.txt"},
|
||||
{Name: "Cat.txt"},
|
||||
{Name: "apple"},
|
||||
},
|
||||
Files: []ItemInfo{
|
||||
Files: []iteminfo.ItemInfo{
|
||||
{Name: "Zebra"},
|
||||
{Name: "apple"},
|
||||
{Name: "cat"},
|
||||
},
|
||||
},
|
||||
expected: FileInfo{
|
||||
Folders: []ItemInfo{
|
||||
expected: iteminfo.FileInfo{
|
||||
Folders: []iteminfo.ItemInfo{
|
||||
{Name: "apple"},
|
||||
{Name: "Cat"},
|
||||
{Name: "dog"},
|
||||
{Name: "Cat.txt"},
|
||||
{Name: "dog.txt"},
|
||||
},
|
||||
Files: []ItemInfo{
|
||||
Files: []iteminfo.ItemInfo{
|
||||
{Name: "apple"},
|
||||
{Name: "cat"},
|
||||
{Name: "Zebra"},
|
||||
@@ -143,7 +153,7 @@ func TestSortItems(t *testing.T) {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
test.input.SortItems()
|
||||
|
||||
getNames := func(items []ItemInfo) []string {
|
||||
getNames := func(items []iteminfo.ItemInfo) []string {
|
||||
names := []string{}
|
||||
for _, folder := range items {
|
||||
names = append(names, folder.Name)
|
||||
@@ -0,0 +1,304 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/cache"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/fileutils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
|
||||
)
|
||||
|
||||
func FileInfoFaster(opts iteminfo.FileOptions) (iteminfo.ExtendedFileInfo, error) {
|
||||
response := iteminfo.ExtendedFileInfo{}
|
||||
if opts.Source == "" {
|
||||
opts.Source = settings.Config.Server.DefaultSource.Name
|
||||
}
|
||||
index := indexing.GetIndex(opts.Source)
|
||||
if index == nil {
|
||||
return response, fmt.Errorf("could not get index: %v ", opts.Source)
|
||||
}
|
||||
realPath, isDir, err := index.GetRealPath(opts.Path)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
opts.IsDir = isDir
|
||||
// TODO: whats the best way to save trips to disk here?
|
||||
// disabled using cache because its not clear if this is helping or hurting
|
||||
// check if the file exists in the index
|
||||
//info, exists := index.GetReducedMetadata(opts.Path, opts.IsDir)
|
||||
//if exists {
|
||||
// err := RefreshFileInfo(opts)
|
||||
// if err != nil {
|
||||
// return info, err
|
||||
// }
|
||||
// if opts.Content {
|
||||
// content := ""
|
||||
// content, err = getContent(opts.Path)
|
||||
// if err != nil {
|
||||
// return info, err
|
||||
// }
|
||||
// info.Content = content
|
||||
// }
|
||||
// return info, nil
|
||||
//}
|
||||
err = index.RefreshFileInfo(opts)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
info, exists := index.GetReducedMetadata(opts.Path, opts.IsDir)
|
||||
if !exists {
|
||||
return response, fmt.Errorf("could not get metadata for path: %v", opts.Path)
|
||||
}
|
||||
if opts.Content && strings.HasPrefix(info.Type, "text") {
|
||||
// Check file size
|
||||
if info.Size > 50*1024*1024 { // 50 megabytes in bytes
|
||||
logger.Debug(fmt.Sprintf("Reading large text file contents: "+info.Path, info.Name))
|
||||
}
|
||||
content, err := getContent(realPath)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
response.Content = content
|
||||
}
|
||||
response.FileInfo = *info
|
||||
response.RealPath = realPath
|
||||
response.Source = opts.Source
|
||||
if settings.Config.Integrations.OnlyOffice.Secret != "" && info.Type != "directory" && iteminfo.IsOnlyOffice(info.Name) {
|
||||
response.OnlyOfficeId = generateOfficeId(realPath)
|
||||
}
|
||||
if strings.HasPrefix(info.Type, "video") {
|
||||
parentInfo, exists := index.GetReducedMetadata(filepath.Dir(info.Path), true)
|
||||
if exists {
|
||||
response.DetectSubtitles(parentInfo)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
subs := map[string]string{}
|
||||
reader, err := os.Open(fullPath)
|
||||
if err != nil {
|
||||
return subs, err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
hashFuncs := map[string]hash.Hash{
|
||||
"md5": md5.New(),
|
||||
"sha1": sha1.New(),
|
||||
"sha256": sha256.New(),
|
||||
"sha512": sha512.New(),
|
||||
}
|
||||
|
||||
h, ok := hashFuncs[algo]
|
||||
if !ok {
|
||||
return subs, errors.ErrInvalidOption
|
||||
}
|
||||
|
||||
_, err = io.Copy(h, reader)
|
||||
if err != nil {
|
||||
return subs, err
|
||||
}
|
||||
subs[algo] = hex.EncodeToString(h.Sum(nil))
|
||||
return subs, nil
|
||||
}
|
||||
|
||||
func DeleteFiles(source, absPath string, absDirPath string) error {
|
||||
err := os.RemoveAll(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index := indexing.GetIndex(source)
|
||||
if index == nil {
|
||||
return fmt.Errorf("could not get index: %v ", source)
|
||||
}
|
||||
refreshConfig := iteminfo.FileOptions{Path: index.MakeIndexPath(absDirPath), IsDir: true}
|
||||
err = index.RefreshFileInfo(refreshConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func MoveResource(sourceIndex, destIndex, realsrc, realdst string) error {
|
||||
err := fileutils.MoveFile(realsrc, realdst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idxSrc := indexing.GetIndex(sourceIndex)
|
||||
if idxSrc == nil {
|
||||
return fmt.Errorf("could not get index: %v ", sourceIndex)
|
||||
}
|
||||
idxDst := indexing.GetIndex(destIndex)
|
||||
if idxDst == nil {
|
||||
return fmt.Errorf("could not get index: %v ", sourceIndex)
|
||||
}
|
||||
refreshSourceDir := idxSrc.MakeIndexPath(filepath.Dir(realsrc))
|
||||
refreshDestDir := idxDst.MakeIndexPath(filepath.Dir(realdst))
|
||||
// refresh info for source and dest
|
||||
err = idxSrc.RefreshFileInfo(iteminfo.FileOptions{
|
||||
Path: refreshSourceDir,
|
||||
IsDir: true,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not refresh index for source: %v", err)
|
||||
}
|
||||
if refreshSourceDir == refreshDestDir {
|
||||
return nil
|
||||
}
|
||||
refreshConfig := iteminfo.FileOptions{Path: refreshDestDir, IsDir: true}
|
||||
err = idxDst.RefreshFileInfo(refreshConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not refresh index for dest: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CopyResource(sourceIndex, destIndex, realsrc, realdst string) error {
|
||||
err := fileutils.CopyFile(realsrc, realdst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idxSrc := indexing.GetIndex(sourceIndex)
|
||||
if idxSrc == nil {
|
||||
return fmt.Errorf("could not get index: %v ", sourceIndex)
|
||||
}
|
||||
idxDst := indexing.GetIndex(destIndex)
|
||||
if idxDst == nil {
|
||||
return fmt.Errorf("could not get index: %v ", sourceIndex)
|
||||
}
|
||||
refreshSourceDir := idxSrc.MakeIndexPath(filepath.Dir(realsrc))
|
||||
refreshDestDir := idxDst.MakeIndexPath(filepath.Dir(realdst))
|
||||
index := indexing.GetIndex(sourceIndex)
|
||||
if index == nil {
|
||||
return fmt.Errorf("could not get index: %v ", sourceIndex)
|
||||
}
|
||||
refreshConfig := iteminfo.FileOptions{Path: refreshSourceDir, IsDir: true}
|
||||
// refresh info for source and dest
|
||||
err = index.RefreshFileInfo(refreshConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not refresh index for source: %v", err)
|
||||
}
|
||||
refreshConfig.Path = refreshDestDir
|
||||
err = index.RefreshFileInfo(refreshConfig)
|
||||
if err != nil {
|
||||
return errors.ErrEmptyKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteDirectory(opts iteminfo.FileOptions) error {
|
||||
idx := indexing.GetIndex(opts.Source)
|
||||
if idx == nil {
|
||||
return fmt.Errorf("could not get index: %v ", opts.Source)
|
||||
}
|
||||
realPath, _, _ := idx.GetRealPath(opts.Path)
|
||||
// Ensure the parent directories exist
|
||||
err := os.MkdirAll(realPath, 0775)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = idx.RefreshFileInfo(opts)
|
||||
if err != nil {
|
||||
return errors.ErrEmptyKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteFile(opts iteminfo.FileOptions, in io.Reader) error {
|
||||
idx := indexing.GetIndex(opts.Source)
|
||||
if idx == nil {
|
||||
return fmt.Errorf("could not get index: %v ", opts.Source)
|
||||
}
|
||||
dst, _, _ := idx.GetRealPath(opts.Path)
|
||||
parentDir := filepath.Dir(dst)
|
||||
// Create the directory and all necessary parents
|
||||
err := os.MkdirAll(parentDir, 0775)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Open the file for writing (create if it doesn't exist, truncate if it does)
|
||||
file, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Copy the contents from the reader to the file
|
||||
_, err = io.Copy(file, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Path = idx.MakeIndexPath(parentDir)
|
||||
opts.IsDir = true
|
||||
return idx.RefreshFileInfo(opts)
|
||||
}
|
||||
|
||||
// addContent reads and sets content based on the file type.
|
||||
func getContent(realPath string) (string, error) {
|
||||
content, err := os.ReadFile(realPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
stringContent := string(content)
|
||||
if !utf8.ValidString(stringContent) {
|
||||
return "", nil
|
||||
}
|
||||
if stringContent == "" {
|
||||
return "empty-file-x6OlSil", nil
|
||||
}
|
||||
return stringContent, nil
|
||||
}
|
||||
|
||||
func IsNamedPipe(mode os.FileMode) bool {
|
||||
return mode&os.ModeNamedPipe != 0
|
||||
}
|
||||
|
||||
func IsSymlink(mode os.FileMode) bool {
|
||||
return mode&os.ModeSymlink != 0
|
||||
}
|
||||
|
||||
func Exists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
// MakeUserDir makes the user directory according to settings.
|
||||
func MakeUserDir(fullPath string) error {
|
||||
if err := os.MkdirAll(fullPath, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func MakeUserDirs(u *users.User) error {
|
||||
cleanedUserName := users.CleanUsername(u.Username)
|
||||
if cleanedUserName == "" || cleanedUserName == "-" || cleanedUserName == "." {
|
||||
logger.Error(fmt.Sprintf("create user: invalid user for home dir creation: [%s]", u.Username))
|
||||
}
|
||||
for i, scope := range u.Scopes {
|
||||
source := settings.Config.Server.SourceMap[scope.Name]
|
||||
if source.Config.CreateUserDir {
|
||||
if !source.Config.CreateUserDir {
|
||||
continue
|
||||
}
|
||||
fullPath := ""
|
||||
if filepath.Base(scope.Scope) != cleanedUserName && !u.Permissions.Admin {
|
||||
scope.Scope = filepath.Join(scope.Scope, cleanedUserName)
|
||||
fullPath = filepath.Join(source.Path, scope.Scope)
|
||||
} else {
|
||||
fullPath = filepath.Join(source.Path, scope.Scope, cleanedUserName)
|
||||
}
|
||||
err := MakeUserDir(fullPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create user: failed to create user home dir: %s", err)
|
||||
}
|
||||
// update scope to reflect new user home dir
|
||||
u.Scopes[i] = scope
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,195 @@
|
||||
package fileutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
)
|
||||
|
||||
// MoveFile moves a file from src to dst.
|
||||
// By default, the rename system call is used. If src and dst point to different volumes,
|
||||
// the file copy is used as a fallback.
|
||||
func MoveFile(src, dst string) error {
|
||||
err := os.Rename(src, dst)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// fallback
|
||||
err = CopyFile(src, dst)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("CopyFile failed %v %v %v ", src, dst, err))
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
err = os.RemoveAll(src)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("os.Remove failed %v %v ", src, err))
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// Create the destination directory if needed.
|
||||
err = os.MkdirAll(filepath.Dir(dest), 0775) //nolint:gomnd
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the destination file.
|
||||
dst, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) //nolint:gomnd
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// Copy the contents of the file.
|
||||
_, err = io.Copy(dst, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy the mode.
|
||||
info, err := os.Stat(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Chmod(dest, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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.
|
||||
switch len(paths) {
|
||||
case 0:
|
||||
return ""
|
||||
case 1:
|
||||
return path.Clean(paths[0])
|
||||
}
|
||||
|
||||
// Treat string as []byte, not []rune as is often done in Go.
|
||||
c := []byte(path.Clean(paths[0]))
|
||||
|
||||
// Add a trailing sep to handle the case where the common prefix directory
|
||||
// is included in the path list.
|
||||
c = append(c, sep)
|
||||
|
||||
// Ignore the first path since it's already in c.
|
||||
for _, v := range paths[1:] {
|
||||
// Clean up each path before testing it.
|
||||
v = path.Clean(v) + string(sep)
|
||||
|
||||
// Find the first non-common byte and truncate c.
|
||||
if len(v) < len(c) {
|
||||
c = c[:len(v)]
|
||||
}
|
||||
for i := 0; i < len(c); i++ {
|
||||
if v[i] != c[i] {
|
||||
c = c[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove trailing non-separator characters and the final separator.
|
||||
for i := len(c) - 1; i >= 0; i-- {
|
||||
if c[i] == sep {
|
||||
c = c[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return string(c)
|
||||
}
|
||||
|
||||
func ClearCacheDir() {
|
||||
cacheDir := settings.Config.Server.CacheDir
|
||||
entries, err := os.ReadDir(cacheDir)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("failed clear cache dir: %v", err))
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
path := filepath.Join(cacheDir, entry.Name())
|
||||
err = os.RemoveAll(path)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("failed clear cache dir: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
File renamed without changes.
+18
-1
@@ -2,8 +2,14 @@ package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
var (
|
||||
revokedApiKeyList map[string]bool
|
||||
revokeMu sync.Mutex
|
||||
)
|
||||
|
||||
// Auther is the authentication interface.
|
||||
@@ -13,3 +19,14 @@ type Auther interface {
|
||||
// LoginPage indicates if this auther needs a login page.
|
||||
LoginPage() bool
|
||||
}
|
||||
|
||||
func IsRevokedApiKey(key string) bool {
|
||||
_, exists := revokedApiKeyList[key]
|
||||
return exists
|
||||
}
|
||||
|
||||
func RevokeAPIKey(key string) {
|
||||
revokeMu.Lock()
|
||||
delete(revokedApiKeyList, key)
|
||||
revokeMu.Unlock()
|
||||
}
|
||||
+52
-57
@@ -3,15 +3,15 @@ package auth
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
type hookCred struct {
|
||||
@@ -39,6 +39,7 @@ func (a *HookAuth) Auth(r *http.Request, usr *users.Storage) (*users.User, error
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&cred)
|
||||
if err != nil {
|
||||
logger.Error("decode body error")
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
@@ -59,11 +60,19 @@ func (a *HookAuth) Auth(r *http.Request, usr *users.Storage) (*users.User, error
|
||||
}
|
||||
return u, nil
|
||||
case "block":
|
||||
logger.Error("block error")
|
||||
|
||||
return nil, os.ErrPermission
|
||||
case "pass":
|
||||
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
|
||||
if err != nil || !users.CheckPwd(a.Cred.Password, u.Password) {
|
||||
return nil, os.ErrPermission
|
||||
logger.Error("pass error")
|
||||
|
||||
u, err := a.Users.Get(a.Cred.Username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get user from store: %v", err)
|
||||
}
|
||||
err = users.CheckPwd(cred.Password, u.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
default:
|
||||
@@ -138,7 +147,7 @@ func (a *HookAuth) GetValues(s string) {
|
||||
|
||||
// SaveUser updates the existing user or creates a new one when not found
|
||||
func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
|
||||
u, err := a.Users.Get(a.Cred.Username)
|
||||
if err != nil && err != errors.ErrNotExist {
|
||||
return nil, err
|
||||
}
|
||||
@@ -146,34 +155,33 @@ 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,
|
||||
NonAdminEditable: users.NonAdminEditable{
|
||||
Password: a.Cred.Password,
|
||||
Locale: a.Settings.UserDefaults.Locale,
|
||||
ViewMode: a.Settings.UserDefaults.ViewMode,
|
||||
SingleClick: a.Settings.UserDefaults.SingleClick,
|
||||
ShowHidden: a.Settings.UserDefaults.ShowHidden,
|
||||
},
|
||||
Username: a.Cred.Username,
|
||||
Permissions: a.Settings.UserDefaults.Permissions,
|
||||
}
|
||||
u = a.GetUser(d)
|
||||
|
||||
userHome, err := a.Settings.MakeUserDir(u.Username, u.Scope, a.Server.Root)
|
||||
if err != nil {
|
||||
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)
|
||||
|
||||
err = a.Users.Save(u)
|
||||
err = a.Users.Save(u, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if p := !users.CheckPwd(a.Cred.Password, u.Password); len(a.Fields.Values) > 1 || p {
|
||||
return u, nil
|
||||
}
|
||||
err = users.CheckPwd(a.Cred.Password, u.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(a.Fields.Values) > 1 {
|
||||
u = a.GetUser(u)
|
||||
// update user with provided fields
|
||||
err := a.Users.Update(u)
|
||||
err := a.Users.Update(u, u.Permissions.Admin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -185,32 +193,24 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||
// GetUser returns a User filled with hook values or provided defaults
|
||||
func (a *HookAuth) GetUser(d *users.User) *users.User {
|
||||
// adds all permissions when user is admin
|
||||
isAdmin := d.Perm.Admin
|
||||
isAdmin := d.Permissions.Admin
|
||||
perms := users.Permissions{
|
||||
Admin: isAdmin,
|
||||
Execute: isAdmin || d.Perm.Execute,
|
||||
Create: isAdmin || d.Perm.Create,
|
||||
Rename: isAdmin || d.Perm.Rename,
|
||||
Modify: isAdmin || d.Perm.Modify,
|
||||
Delete: isAdmin || d.Perm.Delete,
|
||||
Share: isAdmin || d.Perm.Share,
|
||||
Download: isAdmin || d.Perm.Download,
|
||||
Admin: isAdmin,
|
||||
Modify: isAdmin || d.Permissions.Modify,
|
||||
Share: isAdmin || d.Permissions.Share,
|
||||
}
|
||||
user := users.User{
|
||||
ID: d.ID,
|
||||
Username: d.Username,
|
||||
Password: d.Password,
|
||||
Scope: d.Scope,
|
||||
Locale: d.Locale,
|
||||
ViewMode: d.ViewMode,
|
||||
SingleClick: d.SingleClick,
|
||||
Sorting: users.Sorting{
|
||||
Asc: d.Sorting.Asc,
|
||||
By: d.Sorting.By,
|
||||
NonAdminEditable: users.NonAdminEditable{
|
||||
Password: d.Password,
|
||||
Locale: a.Fields.GetString("user.locale", d.Locale),
|
||||
ViewMode: a.Fields.GetString("user.viewMode", d.ViewMode),
|
||||
SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
|
||||
ShowHidden: a.Fields.GetBoolean("user.showHidden", d.ShowHidden),
|
||||
},
|
||||
Commands: d.Commands,
|
||||
HideDotfiles: d.HideDotfiles,
|
||||
Perm: perms,
|
||||
ID: d.ID,
|
||||
Username: d.Username,
|
||||
Scopes: d.Scopes,
|
||||
Permissions: perms,
|
||||
LockPassword: true,
|
||||
}
|
||||
|
||||
@@ -231,16 +231,11 @@ var validHookFields = []string{
|
||||
"user.singleClick",
|
||||
"user.sorting.by",
|
||||
"user.sorting.asc",
|
||||
"user.commands",
|
||||
"user.hideDotfiles",
|
||||
"user.showHidden",
|
||||
"user.perm.admin",
|
||||
"user.perm.execute",
|
||||
"user.perm.create",
|
||||
"user.perm.rename",
|
||||
"user.perm.modify",
|
||||
"user.perm.delete",
|
||||
"user.perm.share",
|
||||
"user.perm.download",
|
||||
"user.perm.api",
|
||||
}
|
||||
|
||||
// IsValid checks if the provided field is on the valid fields list
|
||||
|
||||
+12
-6
@@ -2,13 +2,14 @@ package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
type jsonCred struct {
|
||||
@@ -24,14 +25,15 @@ 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 {
|
||||
logger.Error("nil body error")
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
err := json.NewDecoder(r.Body).Decode(&cred)
|
||||
if err != nil {
|
||||
logger.Error("decode body error")
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
@@ -47,9 +49,13 @@ 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)
|
||||
if err != nil || !users.CheckPwd(cred.Password, u.Password) {
|
||||
return nil, os.ErrPermission
|
||||
u, err := userStore.Get(cred.Username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get user from store: %v", err)
|
||||
}
|
||||
err = users.CheckPwd(cred.Password, u.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u, nil
|
||||
|
||||
@@ -3,8 +3,7 @@ package auth
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
// MethodNoAuth is used to identify no auth.
|
||||
@@ -15,7 +14,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(uint(1))
|
||||
}
|
||||
|
||||
// LoginPage tells that no auth doesn't require a login page.
|
||||
|
||||
@@ -4,10 +4,8 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
// MethodProxyAuth is used to identify no auth.
|
||||
@@ -21,7 +19,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(username)
|
||||
if err == errors.ErrNotExist {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
+20
-3
@@ -1,7 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
// StorageBackend is a storage backend for auth storage.
|
||||
@@ -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/adapters/fs/diskcache 0.005s
|
||||
? github.com/gtsteffaniak/filebrowser/backend/common/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/adapters/fs/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/adapters/fs/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/adapters/fs/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/preview/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/common/settings 0.005s
|
||||
? github.com/gtsteffaniak/filebrowser/backend/database/share [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/backend/database/storage [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/backend/database/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/database/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/common/utils 0.002s
|
||||
? github.com/gtsteffaniak/filebrowser/backend/common/version [no test files]
|
||||
@@ -0,0 +1,127 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/version"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
var (
|
||||
configPath string
|
||||
)
|
||||
|
||||
// return bool to indicate if the program should continue running
|
||||
func runCLI() bool {
|
||||
// Global flags
|
||||
var help bool
|
||||
// Override the default usage output to use generalUsage()
|
||||
flag.Usage = generalUsage
|
||||
flag.StringVar(&configPath, "c", "", "Path to the config file, default: config.yaml")
|
||||
flag.BoolVar(&help, "h", false, "Get help about commands")
|
||||
|
||||
if configPath == "" {
|
||||
configPath = os.Getenv("FILEBROWSER_CONFIG")
|
||||
}
|
||||
|
||||
if configPath == "" {
|
||||
configPath = "config.yaml"
|
||||
}
|
||||
|
||||
// Parse global flags (before subcommands)
|
||||
flag.Parse() // print generalUsage on error
|
||||
|
||||
// Show help if requested
|
||||
if help {
|
||||
generalUsage()
|
||||
return false
|
||||
}
|
||||
|
||||
// Create a new FlagSet for the 'set' subcommand
|
||||
setCmd := flag.NewFlagSet("set", flag.ExitOnError)
|
||||
var user, scope, dbConfig string
|
||||
var asAdmin bool
|
||||
|
||||
setCmd.StringVar(&user, "u", "", "Comma-separated username and password: \"set -u <username>,<password>\"")
|
||||
setCmd.BoolVar(&asAdmin, "a", false, "Create user as admin user, used in combination with -u")
|
||||
setCmd.StringVar(&scope, "s", "", "Specify a user scope, otherwise default user config scope is used")
|
||||
setCmd.StringVar(&dbConfig, "c", "config.yaml", "Path to the config file, default: config.yaml")
|
||||
|
||||
// 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[2:])
|
||||
if err != nil {
|
||||
setCmd.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
userInfo := strings.Split(user, ",")
|
||||
if len(userInfo) < 2 {
|
||||
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]
|
||||
ok := getStore(dbConfig)
|
||||
if !ok {
|
||||
logger.Fatal("could not load db info")
|
||||
}
|
||||
user, err := store.Users.Get(username)
|
||||
if err != nil {
|
||||
newUser := users.User{
|
||||
Username: username,
|
||||
NonAdminEditable: users.NonAdminEditable{
|
||||
Password: password,
|
||||
},
|
||||
}
|
||||
for _, source := range settings.Config.Server.SourceMap {
|
||||
if source.Config.DefaultEnabled {
|
||||
newUser.Scopes = append(newUser.Scopes, users.SourceScope{
|
||||
Name: source.Name,
|
||||
Scope: source.Config.DefaultUserScope,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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 false
|
||||
}
|
||||
user.Password = password
|
||||
if asAdmin {
|
||||
user.Permissions.Admin = true
|
||||
}
|
||||
err = store.Users.Save(user, true)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("could not update user: %v", err))
|
||||
}
|
||||
fmt.Printf("successfully updated user: %s\n", username)
|
||||
return false
|
||||
|
||||
case "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 false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
)
|
||||
|
||||
// healthcheck attempt to save test file against configured url
|
||||
func validateOfficeIntegration() {
|
||||
if settings.Config.Integrations.OnlyOffice.Url != "" {
|
||||
// get url health
|
||||
// get request against the url
|
||||
_, err := http.NewRequest("GET", settings.Config.Integrations.OnlyOffice.Url, nil)
|
||||
if err != nil {
|
||||
logger.Warning("Could not create request to only office Url, make sure its valid and running")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
+84
-119
@@ -1,161 +1,126 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/diskcache"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/fileutils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/storage"
|
||||
fbhttp "github.com/gtsteffaniak/filebrowser/backend/http"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/img"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/preview"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/swagger/docs"
|
||||
"github.com/swaggo/swag"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/version"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/version"
|
||||
)
|
||||
|
||||
func getStore(config string) (*storage.Storage, bool) {
|
||||
var store *storage.Storage
|
||||
|
||||
func getStore(configFile string) bool {
|
||||
// Use the config file (global flag)
|
||||
settings.Initialize(config)
|
||||
store, hasDB, err := storage.InitializeDb(settings.Config.Server.Database)
|
||||
settings.Initialize(configFile)
|
||||
s, 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
|
||||
store = s
|
||||
return 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() {
|
||||
// Global flags
|
||||
var configPath string
|
||||
var help bool
|
||||
// Override the default usage output to use generalUsage()
|
||||
flag.Usage = generalUsage
|
||||
flag.StringVar(&configPath, "c", "config.yaml", "Path to the config file, default: config.yaml")
|
||||
flag.BoolVar(&help, "h", false, "Get help about commands")
|
||||
|
||||
// Parse global flags (before subcommands)
|
||||
flag.Parse() // print generalUsage on error
|
||||
|
||||
// Show help if requested
|
||||
if help {
|
||||
generalUsage()
|
||||
keepGoing := runCLI()
|
||||
if !keepGoing {
|
||||
return
|
||||
}
|
||||
// Create context and channels for graceful shutdown
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Create a new FlagSet for the 'set' subcommand
|
||||
setCmd := flag.NewFlagSet("set", flag.ExitOnError)
|
||||
var user, scope, dbConfig string
|
||||
var asAdmin bool
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
setCmd.StringVar(&user, "u", "", "Comma-separated username and password: \"set -u <username>,<password>\"")
|
||||
setCmd.BoolVar(&asAdmin, "a", false, "Create user as admin user, used in combination with -u")
|
||||
setCmd.StringVar(&scope, "s", "", "Specify a user scope, otherwise default user config scope is used")
|
||||
setCmd.StringVar(&dbConfig, "c", "config.yaml", "Path to the config file, default: config.yaml")
|
||||
|
||||
// Parse subcommand flags only if a subcommand is specified
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "set":
|
||||
err := setCmd.Parse(os.Args)
|
||||
if err != nil {
|
||||
setCmd.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
userInfo := strings.Split(user, ",")
|
||||
if len(userInfo) < 2 {
|
||||
fmt.Println("not enough info to create user: \"set -u username,password\"")
|
||||
setCmd.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
username := userInfo[0]
|
||||
password := userInfo[1]
|
||||
getStore(dbConfig)
|
||||
// Create the user logic
|
||||
if asAdmin {
|
||||
log.Printf("Creating user as admin: %s\n", username)
|
||||
} else {
|
||||
log.Printf("Creating user: %s\n", username)
|
||||
}
|
||||
newUser := users.User{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
if scope != "" {
|
||||
newUser.Scope = scope
|
||||
}
|
||||
err = storage.CreateUser(newUser, asAdmin)
|
||||
if err != nil {
|
||||
log.Fatal("Could not create user: ", err)
|
||||
}
|
||||
return
|
||||
case "version":
|
||||
fmt.Println("FileBrowser Quantum - A modern web-based file manager")
|
||||
fmt.Printf("Version : %v\n", version.Version)
|
||||
fmt.Printf("Commit : %v\n", version.CommitSHA)
|
||||
fmt.Printf("Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v\n", version.Version)
|
||||
return
|
||||
}
|
||||
}
|
||||
store, dbExists := getStore(configPath)
|
||||
done := make(chan struct{}) // Signals server has stopped
|
||||
shutdownComplete := make(chan struct{}) // Signals shutdown process is complete
|
||||
dbExists := getStore(configPath)
|
||||
database := fmt.Sprintf("Using existing database : %v", settings.Config.Server.Database)
|
||||
if !dbExists {
|
||||
database = fmt.Sprintf("Creating new database : %v", settings.Config.Server.Database)
|
||||
}
|
||||
log.Printf("Initializing FileBrowser Quantum (%v)\n", version.Version)
|
||||
log.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)
|
||||
|
||||
sourceList := []string{}
|
||||
for path, source := range settings.Config.Server.SourceMap {
|
||||
sourceList = append(sourceList, fmt.Sprintf("%v: %v", source.Name, path))
|
||||
}
|
||||
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", settings.Config.Auth.AuthMethods))
|
||||
logger.Info(database)
|
||||
logger.Info(fmt.Sprintf("Sources : %v", sourceList))
|
||||
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)
|
||||
if len(settings.Config.Server.SourceMap) == 0 {
|
||||
logger.Fatal("No sources configured, exiting...")
|
||||
}
|
||||
for _, source := range settings.Config.Server.SourceMap {
|
||||
go indexing.Initialize(source, false)
|
||||
}
|
||||
validateUserInfo()
|
||||
validateOfficeIntegration()
|
||||
// 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...")
|
||||
}
|
||||
fileutils.ClearCacheDir()
|
||||
|
||||
<-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"
|
||||
var fileCache diskcache.Interface
|
||||
|
||||
// Use file cache if cacheDir is specified
|
||||
if cacheDir != "" {
|
||||
var err error
|
||||
fileCache, err = diskcache.NewFileCache(cacheDir)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create file cache: %v", err)
|
||||
}
|
||||
} else {
|
||||
// No-op cache if no cacheDir is specified
|
||||
fileCache = diskcache.NewNoOp()
|
||||
cacheDir := settings.Config.Server.CacheDir
|
||||
numWorkers := settings.Config.Server.NumImageProcessors
|
||||
ffpmpegPath := settings.Config.Integrations.Media.FfmpegPath
|
||||
// setup disk cache
|
||||
err := preview.Start(numWorkers, ffpmpegPath, cacheDir)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error starting preview service: %v", err))
|
||||
}
|
||||
fbhttp.StartHttp(imgSvc, store, fileCache)
|
||||
|
||||
fbhttp.StartHttp(ctx, store, 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)
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/fileutils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
var createBackup = []bool{}
|
||||
|
||||
func validateUserInfo() {
|
||||
// update source info for users if names/sources/paths might have changed
|
||||
usersList, err := store.Users.Gets()
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("could not load users: %v", err))
|
||||
}
|
||||
for _, user := range usersList {
|
||||
updateUser := false
|
||||
if user.Username == "publicUser" {
|
||||
settings.ApplyUserDefaults(user)
|
||||
updateUser = true
|
||||
}
|
||||
if updateUserScopes(user) {
|
||||
updateUser = true
|
||||
}
|
||||
if updatePermissions(user) {
|
||||
updateUser = true
|
||||
}
|
||||
if updatePreviewSettings(user) {
|
||||
updateUser = true
|
||||
}
|
||||
if updateLoginType(user) {
|
||||
updateUser = true
|
||||
}
|
||||
if updateUser {
|
||||
if len(createBackup) == 1 {
|
||||
logger.Warning("Incompatible user settings detected, creating backup of database before converting.")
|
||||
err = fileutils.CopyFile(settings.Config.Server.Database, fmt.Sprintf("%s.bak", settings.Config.Server.Database))
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("Unable to create automatic backup of database due to error: %v", err))
|
||||
}
|
||||
}
|
||||
err := store.Users.Save(user, false)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("could not update user: %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateUserScopes(user *users.User) bool {
|
||||
updateUser := false
|
||||
finalScopes := []users.SourceScope{}
|
||||
seenNames := make(map[string]bool)
|
||||
|
||||
// Step 1: Start by including all existing scopes, updating Name if source.Path matches
|
||||
for _, existingScope := range user.Scopes {
|
||||
updated := false
|
||||
for _, source := range settings.Config.Server.SourceMap {
|
||||
if existingScope.Name == source.Path || existingScope.Scope == source.Path {
|
||||
// Update Name to match source.Path
|
||||
existingScope.Name = source.Path
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if existingScope.Scope == "" {
|
||||
existingScope.Scope = "/"
|
||||
updateUser = true
|
||||
}
|
||||
if !seenNames[existingScope.Name] {
|
||||
finalScopes = append(finalScopes, existingScope)
|
||||
seenNames[existingScope.Name] = true
|
||||
if updated {
|
||||
updateUser = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Add missing scopes from SourceMap if admin or DefaultEnabled
|
||||
for _, source := range settings.Config.Server.SourceMap {
|
||||
if seenNames[source.Path] {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if scope exists already via path
|
||||
scopePath, err := settings.GetScopeFromSourcePath(user.Scopes, source.Path)
|
||||
|
||||
// If user has no access, skip unless admin or default-enabled
|
||||
if err != nil && !(user.Permissions.Admin || source.Config.DefaultEnabled) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine scope path
|
||||
if scopePath == "" {
|
||||
scopePath = source.Config.DefaultUserScope
|
||||
}
|
||||
if scopePath == "" {
|
||||
scopePath = "/"
|
||||
}
|
||||
if source.Config.CreateUserDir && !user.Permissions.Admin {
|
||||
scopePath = fmt.Sprintf("%s%s", scopePath, users.CleanUsername(user.Username))
|
||||
}
|
||||
|
||||
// Add new scope
|
||||
finalScopes = append(finalScopes, users.SourceScope{
|
||||
Scope: scopePath,
|
||||
Name: source.Path,
|
||||
})
|
||||
seenNames[source.Path] = true
|
||||
updateUser = true
|
||||
}
|
||||
|
||||
// Step 3: If no scopes, apply default
|
||||
if len(finalScopes) == 0 {
|
||||
defaultScope := users.SourceScope{
|
||||
Scope: settings.Config.Server.DefaultSource.Config.DefaultUserScope,
|
||||
Name: settings.Config.Server.DefaultSource.Path,
|
||||
}
|
||||
finalScopes = append(finalScopes, defaultScope)
|
||||
updateUser = true
|
||||
}
|
||||
|
||||
// Step 4: Assign only if different
|
||||
if !scopesEqual(user.Scopes, finalScopes) {
|
||||
user.Scopes = finalScopes
|
||||
updateUser = true
|
||||
}
|
||||
|
||||
return updateUser
|
||||
}
|
||||
|
||||
// scopesEqual does a deep comparison to avoid unnecessary writes
|
||||
func scopesEqual(a, b []users.SourceScope) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i].Scope != b[i].Scope || a[i].Name != b[i].Name {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// func to convert legacy user with perm key to permissions
|
||||
func updatePermissions(user *users.User) bool {
|
||||
updateUser := false
|
||||
// if any keys are true, set the permissions to true
|
||||
if user.Perm.Api {
|
||||
user.Permissions.Api = true
|
||||
user.Perm.Api = false
|
||||
updateUser = true
|
||||
}
|
||||
if user.Perm.Admin {
|
||||
user.Permissions.Admin = true
|
||||
user.Perm.Admin = false
|
||||
updateUser = true
|
||||
}
|
||||
if user.Perm.Modify {
|
||||
user.Permissions.Modify = true
|
||||
user.Perm.Modify = false
|
||||
updateUser = true
|
||||
}
|
||||
if user.Perm.Share {
|
||||
user.Permissions.Share = true
|
||||
user.Perm.Share = false
|
||||
updateUser = true
|
||||
}
|
||||
if updateUser {
|
||||
createBackup = append(createBackup, true)
|
||||
}
|
||||
return updateUser
|
||||
}
|
||||
|
||||
func updateLoginType(user *users.User) bool {
|
||||
if user.LoginMethod == "" {
|
||||
user.LoginMethod = users.LoginMethodPassword
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func updatePreviewSettings(user *users.User) bool {
|
||||
// if user hasn't been updated yet
|
||||
if user.LoginMethod == "" {
|
||||
user.Preview.Image = true
|
||||
user.Preview.PopUp = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package settings
|
||||
|
||||
type Auth struct {
|
||||
TokenExpirationHours int `json:"tokenExpirationHours"` // the number of hours until the token expires. Default is 2 hours.
|
||||
Methods LoginMethods `json:"methods"`
|
||||
Key string `json:"key"` // the key used to sign the JWT tokens. If not set, a random key will be generated.
|
||||
AdminUsername string `json:"adminUsername"` // the username of the admin user. If not set, the default is "admin".
|
||||
AdminPassword string `json:"adminPassword"` // the password of the admin user. If not set, the default is "admin".
|
||||
AuthMethods []string `json:"-"`
|
||||
}
|
||||
|
||||
type LoginMethods struct {
|
||||
ProxyAuth ProxyAuthConfig `json:"proxy" validate:"omitempty"`
|
||||
NoAuth bool `json:"noauth"` // if set to true, overrides all other auth methods and disables authentication
|
||||
PasswordAuth PasswordAuthConfig `json:"password" validate:"omitempty"`
|
||||
OidcAuth OidcConfig `json:"oidc" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type PasswordAuthConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
MinLength int `json:"minLength" validate:"omitempty,min=5"`
|
||||
Signup bool `json:"signup" validate:"omitempty"` // currently not used by filebrowser
|
||||
Recaptcha Recaptcha `json:"recaptcha" validate:"omitempty"` // recaptcha config, only used if signup is enabled
|
||||
}
|
||||
|
||||
type ProxyAuthConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
CreateUser bool `json:"createUser"` // create user if not exists
|
||||
Header string `json:"header"` // required header to use for authentication. Security Warning: FileBrowser blindly accepts the header value as username.
|
||||
}
|
||||
|
||||
type Recaptcha struct {
|
||||
Host string `json:"host" validate:"required"`
|
||||
Key string `json:"key" validate:"required"`
|
||||
Secret string `json:"secret" validate:"required"`
|
||||
}
|
||||
|
||||
// OpenID OAuth2.0
|
||||
type OidcConfig struct {
|
||||
Enabled bool `json:"enabled"` // whether to enable OIDC authentication
|
||||
ClientID string `json:"clientId" validate:"required"` // client id of the OIDC application
|
||||
ClientSecret string `json:"clientSecret" validate:"required"` // client secret of the OIDC application
|
||||
AuthorizationUrl string `json:"authorizationUrl" validate:"required"` // authorization URL of the OIDC provider
|
||||
TokenUrl string `json:"tokenUrl" validate:"required"` // token URL of the OIDC provider
|
||||
UserInfoUrl string `json:"userInfoUrl" validate:"required"` // user info URL of the OIDC provider
|
||||
Scopes string `json:"scopes" validate:"required"` // space separated list of scopes to request from the OIDC provider
|
||||
UserIdentifier string `json:"userIdentifier"` // optional: which attribute should be used as the username? options: email, username, name, phone_number, sub
|
||||
JwksUrl string `json:"jwksUrl"` // currently not used by filebrowser
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/version"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
var Config Settings
|
||||
|
||||
func Initialize(configFile string) {
|
||||
err := loadConfigWithDefaults(configFile)
|
||||
if err != nil {
|
||||
logger.Fatal(err.Error())
|
||||
}
|
||||
err = ValidateConfig(Config)
|
||||
if err != nil {
|
||||
errmsg := "the provided config file failed validation. "
|
||||
errmsg += "If you are seeing this on a config that worked previeously, "
|
||||
errmsg += "this is because v0.6.8 requires a fully validated config to run. "
|
||||
errmsg += "Please review your config for typos and invalid keys which are no longer supported. "
|
||||
errmsg += "visit https://github.com/gtsteffaniak/filebrowser/wiki/Full-Config-Example for more information."
|
||||
logger.Error(errmsg)
|
||||
logger.Fatal(err.Error())
|
||||
}
|
||||
setupLogging()
|
||||
setupAuth()
|
||||
setupSources()
|
||||
setupUrls()
|
||||
setupFrontend()
|
||||
}
|
||||
|
||||
func setupFrontend() {
|
||||
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 getRealPath(path string) string {
|
||||
realPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("could not find configured source path: %v", err))
|
||||
}
|
||||
// check path exists
|
||||
if _, err = os.Stat(realPath); os.IsNotExist(err) {
|
||||
logger.Fatal(fmt.Sprintf("configured source path does not exist: %v", realPath))
|
||||
}
|
||||
return realPath
|
||||
}
|
||||
|
||||
func setupSources() {
|
||||
if len(Config.Server.Sources) == 0 {
|
||||
logger.Fatal("There are no `server.sources` configured. If you have `server.root` configured, please update the config and add at least one `server.sources` with a `path` configured.")
|
||||
} else {
|
||||
for k, source := range Config.Server.Sources {
|
||||
realPath := getRealPath(source.Path)
|
||||
name := filepath.Base(realPath)
|
||||
if name == "\\" {
|
||||
name = strings.Split(realPath, ":")[0]
|
||||
}
|
||||
source.Path = realPath // use absolute path
|
||||
if source.Name == "" {
|
||||
_, ok := Config.Server.SourceMap[source.Path]
|
||||
if ok {
|
||||
source.Name = name + fmt.Sprintf("-%v", k)
|
||||
} else {
|
||||
source.Name = name
|
||||
}
|
||||
if Config.Server.DefaultSource.Path == "" {
|
||||
Config.Server.DefaultSource = source
|
||||
}
|
||||
}
|
||||
Config.Server.SourceMap[source.Path] = source
|
||||
Config.Server.NameToSource[source.Name] = source
|
||||
}
|
||||
}
|
||||
// clean up the in memory source list to be accurate and unique
|
||||
sourceList := []Source{}
|
||||
defaultScopes := []users.SourceScope{}
|
||||
allSourceNames := []string{}
|
||||
first := true
|
||||
for _, sourcePathOnly := range Config.Server.Sources {
|
||||
realPath := getRealPath(sourcePathOnly.Path)
|
||||
source, ok := Config.Server.SourceMap[realPath]
|
||||
if ok && !slices.Contains(allSourceNames, source.Name) {
|
||||
if first {
|
||||
source.Config.DefaultEnabled = true
|
||||
Config.Server.SourceMap[source.Path] = source
|
||||
Config.Server.NameToSource[source.Name] = source
|
||||
Config.Server.DefaultSource = source
|
||||
}
|
||||
first = false
|
||||
sourceList = append(sourceList, source)
|
||||
if source.Config.DefaultEnabled {
|
||||
Config.Server.DefaultSource = source
|
||||
defaultScopes = append(defaultScopes, users.SourceScope{
|
||||
Name: source.Path,
|
||||
Scope: source.Config.DefaultUserScope,
|
||||
})
|
||||
}
|
||||
allSourceNames = append(allSourceNames, source.Name)
|
||||
} else {
|
||||
logger.Warning(fmt.Sprintf("source %v is not configured correctly, skipping", sourcePathOnly.Path))
|
||||
}
|
||||
}
|
||||
Config.UserDefaults.DefaultScopes = defaultScopes
|
||||
Config.Server.Sources = sourceList
|
||||
}
|
||||
|
||||
func setupUrls() {
|
||||
baseurl := strings.Trim(Config.Server.BaseURL, "/")
|
||||
if baseurl == "" {
|
||||
Config.Server.BaseURL = "/"
|
||||
} else {
|
||||
Config.Server.BaseURL = "/" + baseurl + "/"
|
||||
}
|
||||
Config.Server.InternalUrl = strings.Trim(Config.Server.InternalUrl, "/")
|
||||
Config.Server.ExternalUrl = strings.Trim(Config.Server.ExternalUrl, "/")
|
||||
Config.Integrations.OnlyOffice.Url = strings.Trim(Config.Integrations.OnlyOffice.Url, "/")
|
||||
Config.Integrations.OnlyOffice.InternalUrl = strings.Trim(Config.Integrations.OnlyOffice.InternalUrl, "/")
|
||||
}
|
||||
|
||||
func setupAuth() {
|
||||
if Config.Auth.Methods.PasswordAuth.Enabled {
|
||||
Config.Auth.AuthMethods = append(Config.Auth.AuthMethods, "Password")
|
||||
}
|
||||
if Config.Auth.Methods.ProxyAuth.Enabled {
|
||||
Config.Auth.AuthMethods = append(Config.Auth.AuthMethods, "Proxy")
|
||||
}
|
||||
if Config.Auth.Methods.OidcAuth.Enabled {
|
||||
Config.Auth.AuthMethods = append(Config.Auth.AuthMethods, "OIDC")
|
||||
}
|
||||
if Config.Auth.Methods.NoAuth {
|
||||
logger.Warning("Configured with no authentication, this is not recommended.")
|
||||
Config.Auth.AuthMethods = []string{"Disabled"}
|
||||
}
|
||||
// use password auth as default if no auth methods are set
|
||||
if len(Config.Auth.AuthMethods) == 0 {
|
||||
Config.Auth.Methods.PasswordAuth.Enabled = true
|
||||
Config.Auth.AuthMethods = append(Config.Auth.AuthMethods, "Password")
|
||||
}
|
||||
}
|
||||
|
||||
func setupLogging() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfigWithDefaults(configFile string) error {
|
||||
Config = setDefaults()
|
||||
// Open and read the YAML file
|
||||
yamlFile, err := os.Open(configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer yamlFile.Close()
|
||||
stat, err := yamlFile.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
yamlData := make([]byte, stat.Size())
|
||||
_, err = yamlFile.Read(yamlData)
|
||||
if err != nil && configFile != "config.yaml" {
|
||||
return fmt.Errorf("could not load specified config file: %v", err.Error())
|
||||
}
|
||||
if err != nil {
|
||||
logger.Warning(fmt.Sprintf("Could not load config file '%v', using default settings: %v", configFile, err))
|
||||
}
|
||||
err = yaml.NewDecoder(strings.NewReader(string(yamlData)), yaml.DisallowUnknownField()).Decode(&Config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error unmarshaling YAML data: %v", err)
|
||||
}
|
||||
loadEnvConfig()
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateConfig(config Settings) error {
|
||||
|
||||
validate := validator.New()
|
||||
err := validate.Struct(Config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not validate config: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadEnvConfig() {
|
||||
|
||||
adminPassword, ok := os.LookupEnv("FILEBROWSER_ADMIN_PASSWORD")
|
||||
if ok {
|
||||
logger.Info("Using admin password from FILEBROWSER_ADMIN_PASSWORD environment variable")
|
||||
Config.Auth.AdminPassword = adminPassword
|
||||
}
|
||||
officeSecret, ok := os.LookupEnv("FILEBROWSER_ONLYOFFICE_SECRET")
|
||||
if ok {
|
||||
logger.Info("Using OnlyOffice secret from FILEBROWSER_ONLYOFFICE_SECRET environment variable")
|
||||
Config.Integrations.OnlyOffice.Secret = officeSecret
|
||||
}
|
||||
|
||||
ffmpegPath, ok := os.LookupEnv("FILEBROWSER_FFMPEG_PATH")
|
||||
if ok {
|
||||
Config.Integrations.Media.FfmpegPath = ffmpegPath
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func setDefaults() Settings {
|
||||
return Settings{
|
||||
Server: Server{
|
||||
Port: 80,
|
||||
NumImageProcessors: 4,
|
||||
BaseURL: "",
|
||||
Database: "database.db",
|
||||
SourceMap: map[string]Source{},
|
||||
NameToSource: map[string]Source{},
|
||||
MaxArchiveSizeGB: 50,
|
||||
CacheDir: "tmp",
|
||||
},
|
||||
Auth: Auth{
|
||||
AdminUsername: "admin",
|
||||
AdminPassword: "admin",
|
||||
TokenExpirationHours: 2,
|
||||
Methods: LoginMethods{
|
||||
PasswordAuth: PasswordAuthConfig{
|
||||
Enabled: true,
|
||||
MinLength: 5,
|
||||
Signup: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
Frontend: Frontend{
|
||||
Name: "FileBrowser Quantum",
|
||||
},
|
||||
UserDefaults: UserDefaults{
|
||||
DisableOnlyOfficeExt: ".txt .csv .html .pdf",
|
||||
StickySidebar: true,
|
||||
LockPassword: false,
|
||||
ShowHidden: false,
|
||||
DarkMode: true,
|
||||
DisableSettings: false,
|
||||
ViewMode: "normal",
|
||||
Locale: "en",
|
||||
GallerySize: 3,
|
||||
ThemeColor: "var(--blue)",
|
||||
Permissions: users.Permissions{
|
||||
Modify: false,
|
||||
Share: false,
|
||||
Admin: false,
|
||||
Api: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertToBackendScopes(scopes []users.SourceScope) ([]users.SourceScope, error) {
|
||||
if len(scopes) == 0 {
|
||||
return Config.UserDefaults.DefaultScopes, nil
|
||||
}
|
||||
newScopes := []users.SourceScope{}
|
||||
for _, scope := range scopes {
|
||||
if scope.Scope == "" {
|
||||
scope.Scope = "/"
|
||||
}
|
||||
if !strings.HasPrefix(scope.Scope, "/") {
|
||||
scope.Scope = "/" + scope.Scope
|
||||
}
|
||||
if !strings.HasSuffix(scope.Scope, "/") {
|
||||
scope.Scope = scope.Scope + "/"
|
||||
}
|
||||
// first check if its already a path name and keep it
|
||||
source, ok := Config.Server.SourceMap[scope.Name]
|
||||
if ok {
|
||||
newScopes = append(newScopes, users.SourceScope{
|
||||
Name: source.Path, // backend name is path
|
||||
Scope: scope.Scope,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// check if its the name of a source and convert it to a path
|
||||
source, ok = Config.Server.NameToSource[scope.Name]
|
||||
if !ok {
|
||||
// source might no longer be configured
|
||||
continue
|
||||
}
|
||||
newScopes = append(newScopes, users.SourceScope{
|
||||
Name: source.Path, // backend name is path
|
||||
Scope: scope.Scope,
|
||||
})
|
||||
}
|
||||
return newScopes, nil
|
||||
}
|
||||
|
||||
func ConvertToFrontendScopes(scopes []users.SourceScope) []users.SourceScope {
|
||||
newScopes := []users.SourceScope{}
|
||||
for _, scope := range scopes {
|
||||
if source, ok := Config.Server.SourceMap[scope.Name]; ok {
|
||||
// Replace scope.Name with source.Path while keeping the same Scope value
|
||||
newScopes = append(newScopes, users.SourceScope{
|
||||
Name: source.Name,
|
||||
Scope: scope.Scope,
|
||||
})
|
||||
}
|
||||
}
|
||||
return newScopes
|
||||
}
|
||||
|
||||
func HasSourceByPath(scopes []users.SourceScope, sourcePath string) bool {
|
||||
for _, scope := range scopes {
|
||||
if scope.Name == sourcePath {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetScopeFromSourceName(scopes []users.SourceScope, sourceName string) (string, error) {
|
||||
source, ok := Config.Server.NameToSource[sourceName]
|
||||
if !ok {
|
||||
logger.Debug(fmt.Sprint("Could not get scope from source name: ", sourceName))
|
||||
return "", fmt.Errorf("source with name not found %v", sourceName)
|
||||
}
|
||||
for _, scope := range scopes {
|
||||
if scope.Name == source.Path {
|
||||
return scope.Scope, nil
|
||||
}
|
||||
}
|
||||
logger.Debug(fmt.Sprintf("scope not found for source %v", sourceName))
|
||||
return "", fmt.Errorf("scope not found for source %v", sourceName)
|
||||
}
|
||||
|
||||
func GetScopeFromSourcePath(scopes []users.SourceScope, sourcePath string) (string, error) {
|
||||
for _, scope := range scopes {
|
||||
if scope.Name == sourcePath {
|
||||
return scope.Scope, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("scope not found for source %v", sourcePath)
|
||||
}
|
||||
|
||||
// assumes backend style scopes
|
||||
func GetSources(u *users.User) []string {
|
||||
sources := []string{}
|
||||
for _, scope := range u.Scopes {
|
||||
source, ok := Config.Server.SourceMap[scope.Name]
|
||||
if ok {
|
||||
sources = append(sources, source.Name)
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestInitialize(t *testing.T) {
|
||||
type args struct {
|
||||
configFile string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
Initialize(tt.args.configFile)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_setDefaults(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want Settings
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := setDefaults(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("setDefaults() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigLoadChanged(t *testing.T) {
|
||||
defaultConfig := setDefaults()
|
||||
err := loadConfigWithDefaults("./validConfig.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("error loading config file: %v", err)
|
||||
}
|
||||
// Use go-cmp to compare the two structs
|
||||
if diff := cmp.Diff(defaultConfig, Config); diff == "" {
|
||||
t.Errorf("No change when there should have been (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigLoadEnvVars(t *testing.T) {
|
||||
defaultConfig := setDefaults()
|
||||
expectedKey := "MYKEY"
|
||||
// mock environment variables
|
||||
os.Setenv("FILEBROWSER_ONLYOFFICE_SECRET", expectedKey)
|
||||
err := loadConfigWithDefaults("./validConfig.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("error loading config file: %v", err)
|
||||
}
|
||||
if Config.Integrations.OnlyOffice.Secret != expectedKey {
|
||||
t.Errorf("Expected OnlyOffice.Secret to be '%v', got '%s'", expectedKey, Config.Integrations.OnlyOffice.Secret)
|
||||
}
|
||||
// Use go-cmp to compare the two structs
|
||||
if diff := cmp.Diff(defaultConfig, Config); diff == "" {
|
||||
t.Errorf("No change when there should have been (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigLoadSpecificValues(t *testing.T) {
|
||||
defaultConfig := setDefaults()
|
||||
err := loadConfigWithDefaults("./validConfig.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("error loading config file: %v", err)
|
||||
}
|
||||
testCases := []struct {
|
||||
fieldName string
|
||||
globalVal interface{}
|
||||
newVal interface{}
|
||||
}{
|
||||
{"Server.Database", Config.Server.Database, defaultConfig.Server.Database},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
if tc.globalVal == tc.newVal {
|
||||
t.Errorf("Differences should have been found:\nConfig.%s: %v \nSetConfig: %v \n", tc.fieldName, tc.globalVal, tc.newVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidConfig(t *testing.T) {
|
||||
configFile := "./invalidConfig.yaml"
|
||||
err := loadConfigWithDefaults(configFile)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error loading config file %s, got nil", configFile)
|
||||
}
|
||||
}
|
||||
@@ -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,9 +1,10 @@
|
||||
//go:generate go run ./tools/yaml.go -input=common/settings/settings.go -output=config.generated.yaml
|
||||
package settings
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
const DefaultUsersHomeBasePath = "/users"
|
||||
@@ -29,30 +30,35 @@ func GetSettingsConfig(nameType string, Value string) string {
|
||||
|
||||
func AdminPerms() users.Permissions {
|
||||
return users.Permissions{
|
||||
Create: true,
|
||||
Rename: true,
|
||||
Modify: true,
|
||||
Delete: true,
|
||||
Share: true,
|
||||
Download: true,
|
||||
Admin: true,
|
||||
Api: true,
|
||||
Modify: true,
|
||||
Share: true,
|
||||
Admin: true,
|
||||
Api: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Apply applies the default options to a user.
|
||||
func ApplyUserDefaults(u users.User) users.User {
|
||||
func ApplyUserDefaults(u *users.User) {
|
||||
u.StickySidebar = Config.UserDefaults.StickySidebar
|
||||
u.DisableSettings = Config.UserDefaults.DisableSettings
|
||||
u.DarkMode = Config.UserDefaults.DarkMode
|
||||
u.Scope = Config.UserDefaults.Scope
|
||||
u.Locale = Config.UserDefaults.Locale
|
||||
u.ViewMode = Config.UserDefaults.ViewMode
|
||||
u.SingleClick = Config.UserDefaults.SingleClick
|
||||
u.Perm = Config.UserDefaults.Perm
|
||||
u.Sorting = Config.UserDefaults.Sorting
|
||||
u.Commands = Config.UserDefaults.Commands
|
||||
u.HideDotfiles = Config.UserDefaults.HideDotfiles
|
||||
u.Permissions = Config.UserDefaults.Permissions
|
||||
u.ShowHidden = Config.UserDefaults.ShowHidden
|
||||
u.DateFormat = Config.UserDefaults.DateFormat
|
||||
return u
|
||||
u.DisableOnlyOfficeExt = Config.UserDefaults.DisableOnlyOfficeExt
|
||||
u.ThemeColor = Config.UserDefaults.ThemeColor
|
||||
u.GallerySize = Config.UserDefaults.GallerySize
|
||||
u.QuickDownload = Config.UserDefaults.QuickDownload
|
||||
u.LockPassword = Config.UserDefaults.LockPassword
|
||||
if len(u.Scopes) == 0 {
|
||||
u.Scopes = []users.SourceScope{
|
||||
{
|
||||
Scope: Config.Server.DefaultSource.Config.DefaultUserScope,
|
||||
Name: Config.Server.DefaultSource.Path,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
)
|
||||
|
||||
// StorageBackend is a settings storage backend.
|
||||
@@ -29,20 +28,9 @@ func (s *Storage) Get() (*Settings, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if set.Server.UserHomeBasePath == "" {
|
||||
set.Server.UserHomeBasePath = DefaultUsersHomeBasePath
|
||||
}
|
||||
return set, nil
|
||||
}
|
||||
|
||||
var defaultEvents = []string{
|
||||
"save",
|
||||
"copy",
|
||||
"rename",
|
||||
"upload",
|
||||
"delete",
|
||||
}
|
||||
|
||||
// Save saves the settings for the current instance.
|
||||
func (s *Storage) Save(set *Settings) error {
|
||||
if len(set.Auth.Key) == 0 {
|
||||
@@ -53,32 +41,10 @@ func (s *Storage) Save(set *Settings) error {
|
||||
set.UserDefaults.Locale = "en"
|
||||
}
|
||||
|
||||
if set.UserDefaults.Commands == nil {
|
||||
set.UserDefaults.Commands = []string{}
|
||||
}
|
||||
|
||||
if set.UserDefaults.ViewMode == "" {
|
||||
set.UserDefaults.ViewMode = "normal"
|
||||
}
|
||||
|
||||
if set.Rules == nil {
|
||||
set.Rules = []users.Rule{}
|
||||
}
|
||||
|
||||
if set.Commands == nil {
|
||||
set.Commands = map[string][]string{}
|
||||
}
|
||||
|
||||
for _, event := range defaultEvents {
|
||||
if _, ok := set.Commands["before_"+event]; !ok {
|
||||
set.Commands["before_"+event] = []string{}
|
||||
}
|
||||
|
||||
if _, ok := set.Commands["after_"+event]; !ok {
|
||||
set.Commands["after_"+event] = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
err := s.back.Save(set)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -0,0 +1,129 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
type AllowedMethods string
|
||||
|
||||
const (
|
||||
ProxyAuth AllowedMethods = "proxyAuth"
|
||||
NoAuth AllowedMethods = "noAuth"
|
||||
PasswordAuth AllowedMethods = "passwordAuth"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
Server Server `json:"server"`
|
||||
Auth Auth `json:"auth"`
|
||||
Frontend Frontend `json:"frontend"`
|
||||
UserDefaults UserDefaults `json:"userDefaults"`
|
||||
Integrations Integrations `json:"integrations"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
NumImageProcessors int `json:"numImageProcessors"` // number of concurrent image processing jobs
|
||||
Socket string `json:"socket"` // socket to listen on
|
||||
TLSKey string `json:"tlsKey"` // path to TLS key
|
||||
TLSCert string `json:"tlsCert"` // path to TLS cert
|
||||
DisablePreviews bool `json:"disablePreviews"` // disable all previews thumbnails, simple icons will be used
|
||||
DisableResize bool `json:"disablePreviewResize"` // disable resizing of previews for faster loading over slow connections
|
||||
DisableTypeDetectionByHeader bool `json:"disableTypeDetectionByHeader"` // disable type detection by header, useful if filesystem is slow.
|
||||
Port int `json:"port"` // port to listen on
|
||||
BaseURL string `json:"baseURL"` // base URL for the server, the subpath that the server is running on.
|
||||
Logging []LogConfig `json:"logging"`
|
||||
Database string `json:"database"` // path to the database file
|
||||
Sources []Source `json:"sources" validate:"required,dive"`
|
||||
ExternalUrl string `json:"externalUrl"` // used by share links if set
|
||||
InternalUrl string `json:"internalUrl"` // used by integrations if set, this is the url that an integration service will use to communicate with filebrowser
|
||||
CacheDir string `json:"cacheDir"` // path to the cache directory, used for thumbnails and other cached files
|
||||
MaxArchiveSizeGB int64 `json:"maxArchiveSize"` // max pre-archive combined size of files/folder that are allowed to be archived (in GB)
|
||||
// not exposed to config
|
||||
SourceMap map[string]Source `json:"-" validate:"omitempty"` // uses realpath as key
|
||||
NameToSource map[string]Source `json:"-" validate:"omitempty"` // uses name as key
|
||||
DefaultSource Source `json:"-" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type Integrations struct {
|
||||
OnlyOffice OnlyOffice `json:"office" validate:"omitempty"`
|
||||
Media Media `json:"media" validate:"omitempty"`
|
||||
}
|
||||
|
||||
// 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" validate:"required"` // The URL to the OnlyOffice Document Server, needs to be accessible to the user.
|
||||
InternalUrl string `json:"internalUrl"` // An optional internal address that the filebrowser server can use to communicate with the OnlyOffice Document Server, could be useful to bypass proxy.
|
||||
Secret string `json:"secret" validate:"required"`
|
||||
}
|
||||
|
||||
type Media struct {
|
||||
FfmpegPath string `json:"ffmpegPath"` // path to ffmpeg directory with ffmpeg and ffprobe (eg. /usr/local/bin)
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Levels string `json:"levels"` // separated list of log levels to enable. (eg. "info|warning|error|debug")
|
||||
ApiLevels string `json:"apiLevels"` // separated list of log levels to enable for the API. (eg. "info|warning|error")
|
||||
Output string `json:"output"` // output location. (eg. "stdout" or "path/to/file.log")
|
||||
NoColors bool `json:"noColors"` // disable colors in the output
|
||||
Json bool `json:"json"` // output in json format, currently not supported
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
Path string `json:"path" validate:"required"` // file system path. (Can be relative)
|
||||
Name string `json:"name"` // display name
|
||||
Config SourceConfig `json:"config"`
|
||||
}
|
||||
|
||||
type SourceConfig struct {
|
||||
IndexingInterval uint32 `json:"indexingInterval"` // optional manual overide interval in seconds to re-index the source
|
||||
Disabled bool `json:"disabled"` // disable the indexing of this source
|
||||
MaxWatchers int `json:"maxWatchers"` // number of concurrent watchers to use for this source, currently not supported
|
||||
NeverWatch []string `json:"neverWatchPaths"` // paths to never watch, relative to the source path (eg. "/folder/file.txt")
|
||||
IgnoreHidden bool `json:"ignoreHidden"` // ignore hidden files and folders.
|
||||
IgnoreZeroSizeFolders bool `json:"ignoreZeroSizeFolders"` // ignore folders with 0 size
|
||||
Exclude IndexFilter `json:"exclude"` // exclude files and folders from indexing, if include is not set
|
||||
Include IndexFilter `json:"include"` // include files and folders from indexing, if exclude is not set
|
||||
DefaultUserScope string `json:"defaultUserScope"` // default "/" should match folders under path
|
||||
DefaultEnabled bool `json:"defaultEnabled"` // should be added as a default source for new users?
|
||||
CreateUserDir bool `json:"createUserDir"` // create a user directory for each user
|
||||
}
|
||||
|
||||
type IndexFilter struct {
|
||||
Files []string `json:"files"` // array of file names to include/exclude
|
||||
Folders []string `json:"folders"` // array of folder names to include/exclude
|
||||
FileEndsWith []string `json:"fileEndsWith"` // array of file names to include/exclude (eg "a.jpg")
|
||||
}
|
||||
|
||||
type Frontend struct {
|
||||
Name string `json:"name"` // display name
|
||||
DisableDefaultLinks bool `json:"disableDefaultLinks"` // disable default links in the sidebar
|
||||
DisableUsedPercentage bool `json:"disableUsedPercentage"` // disable used percentage for the sources in the sidebar
|
||||
ExternalLinks []ExternalLink `json:"externalLinks"`
|
||||
}
|
||||
|
||||
type ExternalLink struct {
|
||||
Text string `json:"text" validate:"required"` // the text to display on the link
|
||||
Title string `json:"title"` // the title to display on hover
|
||||
Url string `json:"url" validate:"required"` // the url to link to
|
||||
}
|
||||
|
||||
// UserDefaults is a type that holds the default values
|
||||
// for some fields on User.
|
||||
type UserDefaults struct {
|
||||
PreviewOptions users.PreviewOptions `json:"preview"`
|
||||
StickySidebar bool `json:"stickySidebar"`
|
||||
DarkMode bool `json:"darkMode"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
DisableSettings bool `json:"disableSettings,omitempty"`
|
||||
Locale string `json:"locale"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
GallerySize int `json:"gallerySize"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Permissions users.Permissions `json:"permissions"`
|
||||
ShowHidden bool `json:"showHidden"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
ThemeColor string `json:"themeColor"`
|
||||
QuickDownload bool `json:"quickDownload"`
|
||||
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"`
|
||||
DefaultScopes []users.SourceScope `json:"-"`
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
server:
|
||||
numImageProcessors: 4
|
||||
socket: ""
|
||||
tlsKey: ""
|
||||
tlsCert: ""
|
||||
disablePreviews: false
|
||||
disablePreviewResize: false
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
database: "mydb.db"
|
||||
sources:
|
||||
- path: "."
|
||||
integrations:
|
||||
office:
|
||||
url: "http://localhost:9052"
|
||||
secret: 6H1H7GzUk2alRkNxAPav0W02g8JS5oUd
|
||||
auth:
|
||||
methods:
|
||||
password:
|
||||
enabled: true
|
||||
frontend:
|
||||
name: ""
|
||||
disableUsedPercentage: true
|
||||
userDefaults:
|
||||
locale: ""
|
||||
viewMode: ""
|
||||
darkMode: true
|
||||
disableSettings: false
|
||||
singleClick: true
|
||||
permissions:
|
||||
admin: true
|
||||
modify: true
|
||||
share: true
|
||||
api: true
|
||||
dateFormat: false
|
||||
@@ -0,0 +1,146 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
math "math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
|
||||
)
|
||||
|
||||
func CheckErr(source string, err error) {
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("%s: %v", source, err))
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateKey() string {
|
||||
b := make([]byte, 64)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// CapitalizeFirst returns the input string with the first letter capitalized.
|
||||
func CapitalizeFirst(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s // Return the empty string as is
|
||||
}
|
||||
return strings.ToUpper(string(s[0])) + s[1:]
|
||||
}
|
||||
|
||||
func InsecureRandomIdentifier(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
math.New(math.NewSource(time.Now().UnixNano()))
|
||||
result := make([]byte, length)
|
||||
for i := range result {
|
||||
result[i] = charset[math.Intn(len(charset))]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func PrintStructFields(v interface{}) {
|
||||
val := reflect.ValueOf(v)
|
||||
typ := reflect.TypeOf(v)
|
||||
|
||||
// Ensure the input is a struct
|
||||
if val.Kind() != reflect.Struct {
|
||||
logger.Debug("Provided value is not a struct")
|
||||
return
|
||||
}
|
||||
|
||||
// Iterate over the fields of the struct
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
fieldType := typ.Field(i)
|
||||
|
||||
// Convert field value to string, if possible
|
||||
fieldValue := fmt.Sprintf("%v", field.Interface())
|
||||
|
||||
// Limit to 50 characters
|
||||
if len(fieldValue) > 100 {
|
||||
fieldValue = fieldValue[:100] + "..."
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("Field: %s, %s\n", fieldType.Name, fieldValue))
|
||||
}
|
||||
}
|
||||
|
||||
func GetParentDirectoryPath(path string) string {
|
||||
if path == "/" || path == "" {
|
||||
return ""
|
||||
}
|
||||
path = strings.TrimSuffix(path, "/") // Remove trailing slash if any
|
||||
lastSlash := strings.LastIndex(path, "/")
|
||||
if lastSlash == -1 {
|
||||
return "" // No parent directory for a relative path without slashes
|
||||
}
|
||||
if lastSlash == 0 {
|
||||
return "/" // If the last slash is the first character, return root
|
||||
}
|
||||
return path[:lastSlash]
|
||||
}
|
||||
|
||||
func HashSHA256(data string) string {
|
||||
bytes := sha256.Sum256([]byte(data))
|
||||
return hex.EncodeToString(bytes[:])
|
||||
}
|
||||
|
||||
func GetLastComponent(path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
path = strings.TrimSuffix(path, "/") // Remove trailing slash if any
|
||||
lastSlash := strings.LastIndex(path, "/")
|
||||
if lastSlash == -1 {
|
||||
return path // No parent directory for a relative path without slashes
|
||||
}
|
||||
return path[lastSlash+1:]
|
||||
}
|
||||
|
||||
func JoinPathAsUnix(parts ...string) string {
|
||||
joinedPath := filepath.Join(parts...)
|
||||
if runtime.GOOS == "windows" {
|
||||
joinedPath = strings.ReplaceAll(joinedPath, "\\", "/")
|
||||
}
|
||||
return joinedPath
|
||||
}
|
||||
|
||||
// ResolveSymlinks resolves symlinks in the given path and returns
|
||||
// the final resolved path, whether it's a directory (considering bundle logic), and any error.
|
||||
func ResolveSymlinks(path string) (string, bool, error) {
|
||||
for {
|
||||
// Get the file info using os.Lstat to handle symlinks
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return path, false, fmt.Errorf("could not stat path: %s, %v", path, err)
|
||||
}
|
||||
|
||||
// Check if the path is a symlink
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
// Read the symlink target
|
||||
target, err := os.Readlink(path)
|
||||
if err != nil {
|
||||
return path, false, fmt.Errorf("could not read symlink: %s, %v", path, err)
|
||||
}
|
||||
|
||||
// Resolve the symlink's target relative to its directory
|
||||
path = filepath.Join(filepath.Dir(path), target)
|
||||
} else {
|
||||
// Not a symlink, check with bundle-aware directory logic
|
||||
isDir := iteminfo.IsDirectory(info)
|
||||
return path, isDir, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -1,17 +1,19 @@
|
||||
package files
|
||||
package utils
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"math/rand"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
|
||||
)
|
||||
|
||||
func CreateMockData(numDirs, numFilesPerDir int) FileInfo {
|
||||
dir := FileInfo{}
|
||||
func CreateMockData(numDirs, numFilesPerDir int) iteminfo.FileInfo {
|
||||
dir := iteminfo.FileInfo{}
|
||||
dir.Path = "/here/is/your/mock/dir"
|
||||
for i := 0; i < numDirs; i++ {
|
||||
newFile := ItemInfo{
|
||||
Name: "file-" + getRandomTerm() + getRandomExtension(),
|
||||
newFile := iteminfo.ItemInfo{
|
||||
Name: "file-" + GetRandomTerm() + GetRandomExtension(),
|
||||
Size: rand.Int63n(1000), // Random size
|
||||
ModTime: time.Now().Add(-time.Duration(rand.Intn(100)) * time.Hour), // Random mod time
|
||||
Type: "blob",
|
||||
@@ -20,8 +22,8 @@ func CreateMockData(numDirs, numFilesPerDir int) FileInfo {
|
||||
}
|
||||
// Simulating files and directories with FileInfo
|
||||
for j := 0; j < numFilesPerDir; j++ {
|
||||
newFile := ItemInfo{
|
||||
Name: "file-" + getRandomTerm() + getRandomExtension(),
|
||||
newFile := iteminfo.ItemInfo{
|
||||
Name: "file-" + GetRandomTerm() + GetRandomExtension(),
|
||||
Size: rand.Int63n(1000), // Random size
|
||||
ModTime: time.Now().Add(-time.Duration(rand.Intn(100)) * time.Hour), // Random mod time
|
||||
Type: "blob",
|
||||
@@ -31,16 +33,16 @@ func CreateMockData(numDirs, numFilesPerDir int) FileInfo {
|
||||
return dir
|
||||
}
|
||||
|
||||
func generateRandomPath(levels int) string {
|
||||
func GenerateRandomPath(levels int) string {
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
dirName := "srv"
|
||||
for i := 0; i < levels; i++ {
|
||||
dirName += "/" + getRandomTerm()
|
||||
dirName += "/" + GetRandomTerm()
|
||||
}
|
||||
return dirName
|
||||
}
|
||||
|
||||
func getRandomTerm() string {
|
||||
func GetRandomTerm() string {
|
||||
wordbank := []string{
|
||||
"hi", "test", "other", "name",
|
||||
"cool", "things", "more", "items",
|
||||
@@ -51,7 +53,7 @@ func getRandomTerm() string {
|
||||
return wordbank[index]
|
||||
}
|
||||
|
||||
func getRandomExtension() string {
|
||||
func GetRandomExtension() string {
|
||||
wordbank := []string{
|
||||
".txt", ".mp3", ".mov", ".doc",
|
||||
".mp4", ".bak", ".zip", ".jpg",
|
||||
@@ -61,11 +63,11 @@ func getRandomExtension() string {
|
||||
return wordbank[index]
|
||||
}
|
||||
|
||||
func generateRandomSearchTerms(numTerms int) []string {
|
||||
func GenerateRandomSearchTerms(numTerms int) []string {
|
||||
// Generate random search terms
|
||||
searchTerms := make([]string, numTerms)
|
||||
for i := 0; i < numTerms; i++ {
|
||||
searchTerms[i] = getRandomTerm()
|
||||
searchTerms[i] = GetRandomTerm()
|
||||
}
|
||||
return searchTerms
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,11 @@
|
||||
server:
|
||||
logging:
|
||||
- levels: "info|warning|error"
|
||||
sources:
|
||||
- path: "/srv"
|
||||
userDefaults:
|
||||
preview:
|
||||
image: true
|
||||
popup: true
|
||||
video: true
|
||||
highQuality: true
|
||||
+11
-10
@@ -1,21 +1,22 @@
|
||||
server:
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
root: "/srv"
|
||||
auth:
|
||||
method: password
|
||||
signup: false
|
||||
logging:
|
||||
- levels: "info|warning|error"
|
||||
sources:
|
||||
- path: "/srv"
|
||||
userDefaults:
|
||||
preview:
|
||||
image: true
|
||||
popup: true
|
||||
video: false
|
||||
office: false
|
||||
highQuality: false
|
||||
darkMode: true
|
||||
disableSettings: false
|
||||
scope: "."
|
||||
hideDotfiles: true
|
||||
singleClick: false
|
||||
permissions:
|
||||
admin: false
|
||||
create: false
|
||||
rename: false
|
||||
modify: false
|
||||
delete: false
|
||||
share: false
|
||||
download: false
|
||||
api: false
|
||||
@@ -10,6 +10,7 @@ type CreateBody struct {
|
||||
type Link struct {
|
||||
Hash string `json:"hash" storm:"id,index"`
|
||||
Path string `json:"path" storm:"index"`
|
||||
Source string `json:"source" storm:"index"`
|
||||
UserID uint `json:"userID"`
|
||||
Expire int64 `json:"expire"`
|
||||
PasswordHash string `json:"password_hash,omitempty"`
|
||||
@@ -3,7 +3,7 @@ package share
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
)
|
||||
|
||||
// StorageBackend is the interface to implement for a share storage.
|
||||
@@ -11,8 +11,8 @@ type StorageBackend interface {
|
||||
All() ([]*Link, error)
|
||||
FindByUserID(id uint) ([]*Link, error)
|
||||
GetByHash(hash string) (*Link, error)
|
||||
GetPermanent(path string, id uint) (*Link, error)
|
||||
Gets(path string, id uint) ([]*Link, error)
|
||||
GetPermanent(path, source string, id uint) (*Link, error)
|
||||
Gets(path, source string, id uint) ([]*Link, error)
|
||||
Save(s *Link) error
|
||||
Delete(hash string) error
|
||||
}
|
||||
@@ -85,13 +85,13 @@ func (s *Storage) GetByHash(hash string) (*Link, error) {
|
||||
}
|
||||
|
||||
// GetPermanent wraps a StorageBackend.GetPermanent
|
||||
func (s *Storage) GetPermanent(path string, id uint) (*Link, error) {
|
||||
return s.back.GetPermanent(path, id)
|
||||
func (s *Storage) GetPermanent(path, source string, id uint) (*Link, error) {
|
||||
return s.back.GetPermanent(path, source, id)
|
||||
}
|
||||
|
||||
// Gets wraps a StorageBackend.Gets
|
||||
func (s *Storage) Gets(path string, id uint) ([]*Link, error) {
|
||||
links, err := s.back.Gets(path, id)
|
||||
func (s *Storage) Gets(sourcePath, source string, id uint) ([]*Link, error) {
|
||||
links, err := s.back.Gets(sourcePath, source, id)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1,9 +1,9 @@
|
||||
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"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
)
|
||||
|
||||
type authBackend struct {
|
||||
@@ -1,12 +1,12 @@
|
||||
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"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
// NewStorage creates a storage.Storage based on Bolt DB.
|
||||
@@ -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,8 +1,8 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
storm "github.com/asdine/storm/v3"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
)
|
||||
|
||||
type settingsBackend struct {
|
||||
@@ -0,0 +1,85 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
storm "github.com/asdine/storm/v3"
|
||||
"github.com/asdine/storm/v3/q"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/share"
|
||||
)
|
||||
|
||||
type shareBackend struct {
|
||||
db *storm.DB
|
||||
}
|
||||
|
||||
func (s shareBackend) All() ([]*share.Link, error) {
|
||||
var v []*share.Link
|
||||
err := s.db.All(&v)
|
||||
if err == storm.ErrNotFound {
|
||||
return v, errors.ErrNotExist
|
||||
}
|
||||
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (s shareBackend) FindByUserID(id uint) ([]*share.Link, error) {
|
||||
var v []*share.Link
|
||||
err := s.db.Select(q.Eq("UserID", id)).Find(&v)
|
||||
if err == storm.ErrNotFound {
|
||||
return v, errors.ErrNotExist
|
||||
}
|
||||
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (s shareBackend) GetByHash(hash string) (*share.Link, error) {
|
||||
var v share.Link
|
||||
err := s.db.One("Hash", hash, &v)
|
||||
if err == storm.ErrNotFound {
|
||||
return nil, errors.ErrNotExist
|
||||
}
|
||||
|
||||
return &v, err
|
||||
}
|
||||
|
||||
func (s shareBackend) GetPermanent(path, source string, id uint) (*share.Link, error) {
|
||||
var v share.Link
|
||||
// TODO remove legacy and return notfound errors
|
||||
_ = s.db.Select(q.Eq("Path", path), q.Eq("Source", source), q.Eq("Expire", 0), q.Eq("UserID", id)).First(&v)
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
func (s shareBackend) Gets(path, sourcePath string, id uint) ([]*share.Link, error) {
|
||||
var v []*share.Link
|
||||
_ = s.db.Select(q.Eq("Path", path), q.Eq("Source", sourcePath), q.Eq("UserID", id)).Find(&v)
|
||||
filteredList := []*share.Link{}
|
||||
var err error
|
||||
// through and filter out expired share
|
||||
for i := range v {
|
||||
if v[i].Expire < time.Now().Unix() && v[i].Expire != 0 {
|
||||
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 {
|
||||
return s.db.Save(l)
|
||||
}
|
||||
|
||||
func (s shareBackend) Delete(hash string) error {
|
||||
err := s.db.DeleteStruct(&share.Link{Hash: hash})
|
||||
if err == storm.ErrNotFound {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
storm "github.com/asdine/storm/v3"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
type usersBackend struct {
|
||||
db *storm.DB
|
||||
}
|
||||
|
||||
func (st usersBackend) GetBy(i interface{}) (user *users.User, err error) {
|
||||
user = &users.User{}
|
||||
|
||||
var arg string
|
||||
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, val, user)
|
||||
|
||||
if err != nil {
|
||||
if err == storm.ErrNotFound {
|
||||
return nil, errors.ErrNotExist
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (st usersBackend) Gets() ([]*users.User, error) {
|
||||
var allUsers []*users.User
|
||||
err := st.db.All(&allUsers)
|
||||
if err == storm.ErrNotFound {
|
||||
return nil, errors.ErrNotExist
|
||||
}
|
||||
if err != nil {
|
||||
return allUsers, err
|
||||
}
|
||||
return allUsers, err
|
||||
}
|
||||
|
||||
func (st usersBackend) Update(user *users.User, actorIsAdmin bool, fields ...string) error {
|
||||
existingUser, err := st.GetBy(user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fields, err = parseFields(user, fields, actorIsAdmin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !slices.Contains(fields, "Password") {
|
||||
user.Password = existingUser.Password
|
||||
} else {
|
||||
if existingUser.LockPassword {
|
||||
return fmt.Errorf("password cannot be changed when lock password is enabled")
|
||||
}
|
||||
}
|
||||
|
||||
if !actorIsAdmin {
|
||||
fmt.Println("User is not admin, checking restricted fields")
|
||||
err := checkRestrictedFields(existingUser, fields)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("existingUser, fields", existingUser, fields)
|
||||
}
|
||||
|
||||
if len(fields) == 0 {
|
||||
return fmt.Errorf("no fields to update")
|
||||
}
|
||||
|
||||
// converting scopes to map of paths intead of names (names can change)
|
||||
if slices.Contains(fields, "Scopes") {
|
||||
adjustedScopes, err := settings.ConvertToBackendScopes(user.Scopes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Scopes = adjustedScopes
|
||||
}
|
||||
// Use reflection to access struct fields
|
||||
userFields := reflect.ValueOf(user).Elem() // Get struct value
|
||||
for _, field := range fields {
|
||||
// Get the corresponding field using reflection
|
||||
fieldValue := userFields.FieldByName(field)
|
||||
if !fieldValue.IsValid() {
|
||||
return fmt.Errorf("invalid field: %s", field)
|
||||
}
|
||||
|
||||
// Ensure the field is settable
|
||||
if !fieldValue.CanSet() {
|
||||
return fmt.Errorf("cannot set value of field: %s", field)
|
||||
}
|
||||
|
||||
// Get the value to be stored
|
||||
val := fieldValue.Interface()
|
||||
// Update the database
|
||||
if err := st.db.UpdateField(existingUser, field, val); err != nil {
|
||||
return fmt.Errorf("failed to update user field: %s, error: %v", field, err)
|
||||
}
|
||||
}
|
||||
|
||||
// last revoke api keys if needed.
|
||||
if existingUser.Permissions.Api && !user.Permissions.Api && slices.Contains(fields, "Permissions") {
|
||||
for _, key := range existingUser.ApiKeys {
|
||||
auth.RevokeAPIKey(key.Key) // add to blacklist
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st usersBackend) Save(user *users.User, changePass bool) error {
|
||||
if user.LoginMethod == "" {
|
||||
user.LoginMethod = users.LoginMethodPassword
|
||||
}
|
||||
logger.Debug(fmt.Sprintf("Saving user [%s] changepass: %v", user.Username, changePass))
|
||||
if user.LoginMethod == users.LoginMethodPassword && changePass {
|
||||
err := checkPassword(user.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pass, err := users.HashPwd(user.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Password = pass
|
||||
}
|
||||
|
||||
// converting scopes to map of paths intead of names (names can change)
|
||||
adjustedScopes, err := settings.ConvertToBackendScopes(user.Scopes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Scopes = adjustedScopes
|
||||
|
||||
err = files.MakeUserDirs(user)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
err = st.db.Save(user)
|
||||
if err == storm.ErrAlreadyExists {
|
||||
return errors.ErrExist
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (st usersBackend) DeleteByID(id uint) error {
|
||||
return st.db.DeleteStruct(&users.User{ID: id})
|
||||
}
|
||||
|
||||
func (st usersBackend) DeleteByUsername(username string) error {
|
||||
user, err := st.GetBy(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return st.db.DeleteStruct(user)
|
||||
}
|
||||
|
||||
// Define a function to check for restricted fields
|
||||
func checkRestrictedFields(existingUser *users.User, fields []string) error {
|
||||
// Get a list of allowed fields from NonAdminEditable
|
||||
allowed := getNonAdminEditableFieldNames()
|
||||
|
||||
for _, field := range fields {
|
||||
if !slices.Contains(allowed, field) {
|
||||
return fmt.Errorf("non-admins cannot modify field: %s", field)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper to return list of field names from NonAdminEditable struct
|
||||
func getNonAdminEditableFieldNames() []string {
|
||||
var names []string
|
||||
t := reflect.TypeOf(users.NonAdminEditable{})
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
names = append(names, t.Field(i).Name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func parseFields(user *users.User, fields []string, actorIsAdmin bool) ([]string, error) {
|
||||
// If `Which` is not specified, default to updating all fields
|
||||
if len(fields) == 0 || fields[0] == "all" {
|
||||
fields = []string{}
|
||||
v := reflect.ValueOf(user)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
t := v.Type()
|
||||
|
||||
// Dynamically populate fields to update
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
// which=all can't update password
|
||||
if strings.ToLower(field.Name) != "password" && strings.ToLower(field.Name) != "id" && strings.ToLower(field.Name) != "username" {
|
||||
fields = append(fields, field.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
newfields := []string{}
|
||||
for _, field := range fields {
|
||||
capitalField := utils.CapitalizeFirst(field)
|
||||
if capitalField == "Scopes" {
|
||||
if !actorIsAdmin {
|
||||
continue
|
||||
}
|
||||
newScopes, err := settings.ConvertToBackendScopes(user.Scopes)
|
||||
if err == nil {
|
||||
user.Scopes = newScopes
|
||||
}
|
||||
err = files.MakeUserDirs(user)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
if capitalField == "Password" {
|
||||
if user.LoginMethod != users.LoginMethodPassword {
|
||||
return nil, fmt.Errorf("password cannot be changed when login method is not password")
|
||||
}
|
||||
err := checkPassword(user.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("password does not meet complexity requirements")
|
||||
}
|
||||
value, err := users.HashPwd(user.Password)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
user.Password = value
|
||||
}
|
||||
newfields = append(newfields, capitalField)
|
||||
}
|
||||
|
||||
return newfields, nil
|
||||
}
|
||||
|
||||
func checkPassword(password string) error {
|
||||
if len(password) < settings.Config.Auth.Methods.PasswordAuth.MinLength {
|
||||
return fmt.Errorf("password must be at least %d characters long", settings.Config.Auth.Methods.PasswordAuth.MinLength)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/asdine/storm/v3"
|
||||
storm "github.com/asdine/storm/v3"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
)
|
||||
|
||||
func get(db *storm.DB, name string, to interface{}) error {
|
||||
@@ -2,19 +2,18 @@ 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/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage/bolt"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/storage/bolt"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
// Storage is a storage powered by a Backend which makes the necessary
|
||||
@@ -41,7 +40,7 @@ func InitializeDb(path string) (*Storage, bool, error) {
|
||||
return nil, exists, err
|
||||
}
|
||||
|
||||
err = bolt.Save(db, "version", 2) //nolint:gomnd
|
||||
err = bolt.Save(db, "version", 2)
|
||||
if err != nil {
|
||||
return nil, exists, err
|
||||
}
|
||||
@@ -80,55 +79,46 @@ 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)
|
||||
utils.CheckErr("store.Settings.SaveServer", err)
|
||||
user := settings.ApplyUserDefaults(users.User{})
|
||||
user := &users.User{}
|
||||
settings.ApplyUserDefaults(user)
|
||||
user.Username = settings.Config.Auth.AdminUsername
|
||||
user.Password = settings.Config.Auth.AdminPassword
|
||||
user.Perm.Admin = true
|
||||
user.Scope = "./"
|
||||
user.DarkMode = true
|
||||
user.ViewMode = "normal"
|
||||
user.Permissions.Admin = true
|
||||
user.Scopes = []users.SourceScope{}
|
||||
for _, val := range settings.Config.Server.Sources {
|
||||
user.Scopes = append(user.Scopes, users.SourceScope{
|
||||
Name: val.Path, // backend name is path
|
||||
Scope: "",
|
||||
})
|
||||
}
|
||||
user.LockPassword = false
|
||||
user.Perm = settings.AdminPerms()
|
||||
err = store.Users.Save(&user)
|
||||
user.Permissions = settings.AdminPerms()
|
||||
logger.Debug(fmt.Sprintf("Creating user as admin: %v %v", user.Username, user.Password))
|
||||
err = store.Users.Save(user, true)
|
||||
utils.CheckErr("store.Users.Save", err)
|
||||
}
|
||||
|
||||
// create new user
|
||||
func CreateUser(userInfo users.User, asAdmin bool) error {
|
||||
newUser := &userInfo
|
||||
// must have username or password to create
|
||||
if userInfo.Username == "" || userInfo.Password == "" {
|
||||
return errors.ErrInvalidRequestParams
|
||||
}
|
||||
newUser := settings.ApplyUserDefaults(userInfo)
|
||||
logger.Debug(fmt.Sprintf("Creating user: %v %v", userInfo.Username, userInfo.Scopes))
|
||||
settings.ApplyUserDefaults(newUser)
|
||||
if asAdmin {
|
||||
newUser.Perm = settings.AdminPerms()
|
||||
userInfo.Permissions = settings.AdminPerms()
|
||||
}
|
||||
// create new home directory
|
||||
userHome, err := settings.Config.MakeUserDir(newUser.Username, newUser.Scope, settings.Config.Server.Root)
|
||||
if err != nil {
|
||||
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
||||
return err
|
||||
if len(userInfo.Scopes) == 0 {
|
||||
userInfo.Scopes = settings.Config.UserDefaults.DefaultScopes
|
||||
}
|
||||
newUser.Scope = userHome
|
||||
log.Printf("user: %s, home dir: [%s].", newUser.Username, userHome)
|
||||
_, _, err = files.GetRealPath(settings.Config.Server.Root, newUser.Scope)
|
||||
if err != nil {
|
||||
log.Println("user path is not valid", newUser.Scope)
|
||||
return nil
|
||||
}
|
||||
err = store.Users.Save(&newUser)
|
||||
// create new home directories
|
||||
err := store.Users.Save(newUser, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -11,7 +11,6 @@ func HashPwd(password string) (string, error) {
|
||||
}
|
||||
|
||||
// CheckPwd checks if a password is correct.
|
||||
func CheckPwd(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
func CheckPwd(password, hash string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
}
|
||||
@@ -4,31 +4,29 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
)
|
||||
|
||||
// StorageBackend is the interface to implement for a users storage.
|
||||
type StorageBackend interface {
|
||||
GetBy(interface{}) (*User, error)
|
||||
Gets() ([]*User, error)
|
||||
Save(u *User) error
|
||||
Update(u *User, fields ...string) error
|
||||
Save(u *User, changePass bool) error
|
||||
Update(u *User, adminActor bool, fields ...string) error
|
||||
DeleteByID(uint) error
|
||||
DeleteByUsername(string) error
|
||||
}
|
||||
|
||||
// Store is an interface for user storage.
|
||||
type Store interface {
|
||||
Get(baseScope string, id interface{}) (user *User, err error)
|
||||
Gets(baseScope string) ([]*User, error)
|
||||
Update(user *User, fields ...string) error
|
||||
Save(user *User) error
|
||||
Get(id interface{}) (user *User, err error)
|
||||
Gets() ([]*User, error)
|
||||
Update(user *User, adminActor bool, fields ...string) error
|
||||
Save(user *User, changePass bool) error
|
||||
Delete(id interface{}) error
|
||||
LastUpdate(id uint) int64
|
||||
AddApiKey(username uint, name string, key AuthToken) error
|
||||
DeleteApiKey(username uint, name string) error
|
||||
AddRule(username string, rule Rule) error
|
||||
DeleteRule(username string, ruleID string) error
|
||||
}
|
||||
|
||||
// Storage is a users storage.
|
||||
@@ -49,7 +47,7 @@ func NewStorage(back StorageBackend) *Storage {
|
||||
// Get allows you to get a user by its name or username. The provided
|
||||
// id must be a string for username lookup or a uint for id lookup. If id
|
||||
// is neither, a ErrInvalidDataType will be returned.
|
||||
func (s *Storage) Get(baseScope string, id interface{}) (user *User, err error) {
|
||||
func (s *Storage) Get(id interface{}) (user *User, err error) {
|
||||
user, err = s.back.GetBy(id)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -58,7 +56,7 @@ func (s *Storage) Get(baseScope string, id interface{}) (user *User, err error)
|
||||
}
|
||||
|
||||
// Gets gets a list of all users.
|
||||
func (s *Storage) Gets(baseScope string) ([]*User, error) {
|
||||
func (s *Storage) Gets() ([]*User, error) {
|
||||
users, err := s.back.Gets()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -67,8 +65,8 @@ func (s *Storage) Gets(baseScope string) ([]*User, error) {
|
||||
}
|
||||
|
||||
// Update updates a user in the database.
|
||||
func (s *Storage) Update(user *User, fields ...string) error {
|
||||
err := s.back.Update(user, fields...)
|
||||
func (s *Storage) Update(user *User, adminIActor bool, fields ...string) error {
|
||||
err := s.back.Update(user, adminIActor, fields...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -79,25 +77,8 @@ func (s *Storage) Update(user *User, fields ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddRule adds a rule to the user's rules list and updates the user in the database.
|
||||
func (s *Storage) AddRule(userID string, rule Rule) error {
|
||||
user, err := s.Get("", userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.Rules = append(user.Rules, rule)
|
||||
|
||||
err = s.Update(user, "Rules")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Storage) AddApiKey(userID uint, name string, key AuthToken) error {
|
||||
user, err := s.Get("", userID)
|
||||
user, err := s.Get(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -106,7 +87,7 @@ func (s *Storage) AddApiKey(userID uint, name string, key AuthToken) error {
|
||||
user.ApiKeys = make(map[string]AuthToken)
|
||||
}
|
||||
user.ApiKeys[name] = key
|
||||
err = s.Update(user, "ApiKeys")
|
||||
err = s.Update(user, true, "ApiKeys")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -115,7 +96,7 @@ func (s *Storage) AddApiKey(userID uint, name string, key AuthToken) error {
|
||||
}
|
||||
|
||||
func (s *Storage) DeleteApiKey(userID uint, name string) error {
|
||||
user, err := s.Get("", userID)
|
||||
user, err := s.Get(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -124,32 +105,7 @@ func (s *Storage) DeleteApiKey(userID uint, name string) error {
|
||||
user.ApiKeys = make(map[string]AuthToken)
|
||||
}
|
||||
delete(user.ApiKeys, name)
|
||||
err = s.Update(user, "ApiKeys")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteRule deletes a rule specified by ID from the user's rules list and updates the user in the database.
|
||||
func (s *Storage) DeleteRule(userID string, ruleID string) error {
|
||||
user, err := s.Get("", userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find and remove the rule with the specified ID
|
||||
var updatedRules []Rule
|
||||
for _, r := range user.Rules {
|
||||
if r.Id != ruleID {
|
||||
updatedRules = append(updatedRules, r)
|
||||
}
|
||||
}
|
||||
|
||||
user.Rules = updatedRules
|
||||
|
||||
err = s.Update(user, "Rules")
|
||||
err = s.Update(user, true, "ApiKeys")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -158,8 +114,8 @@ func (s *Storage) DeleteRule(userID string, ruleID string) error {
|
||||
}
|
||||
|
||||
// Save saves the user in a storage.
|
||||
func (s *Storage) Save(user *User) error {
|
||||
return s.back.Save(user)
|
||||
func (s *Storage) Save(user *User, changePass bool) error {
|
||||
return s.back.Save(user, changePass)
|
||||
}
|
||||
|
||||
// Delete allows you to delete a user by its name or username. The provided
|
||||
File renamed without changes.
@@ -0,0 +1,116 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
jwt "github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
var (
|
||||
invalidFilenameChars = regexp.MustCompile(`[^0-9A-Za-z@_\-.]`)
|
||||
dashes = regexp.MustCompile(`[\-]+`)
|
||||
)
|
||||
|
||||
const ()
|
||||
|
||||
type LoginMethod string
|
||||
|
||||
const (
|
||||
LoginMethodPassword LoginMethod = "password"
|
||||
LoginMethodProxy LoginMethod = "proxy"
|
||||
LoginMethodOidc LoginMethod = "oidc"
|
||||
)
|
||||
|
||||
type AuthToken struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
Created int64 `json:"createdAt"`
|
||||
Expires int64 `json:"expiresAt"`
|
||||
BelongsTo uint `json:"belongsTo"`
|
||||
Permissions Permissions `json:"Permissions"`
|
||||
jwt.RegisteredClaims `json:"-"`
|
||||
}
|
||||
|
||||
type Permissions struct {
|
||||
Api bool `json:"api"`
|
||||
Admin bool `json:"admin"`
|
||||
Modify bool `json:"modify"`
|
||||
Share bool `json:"share"`
|
||||
Realtime bool `json:"realtime"`
|
||||
}
|
||||
|
||||
// SortingSettings represents the sorting settings.
|
||||
type Sorting struct {
|
||||
By string `json:"by"`
|
||||
Asc bool `json:"asc"`
|
||||
}
|
||||
|
||||
type PreviewOptions struct {
|
||||
HighQuality bool `json:"highQuality"`
|
||||
Image bool `json:"image"`
|
||||
Video bool `json:"video"`
|
||||
Office bool `json:"office"`
|
||||
PopUp bool `json:"popup"`
|
||||
}
|
||||
|
||||
// User describes a user.
|
||||
type User struct {
|
||||
NonAdminEditable
|
||||
DisableSettings bool `json:"disableSettings"`
|
||||
ID uint `storm:"id,increment" json:"id"`
|
||||
Username string `storm:"unique" json:"username"`
|
||||
Scopes []SourceScope `json:"scopes"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
Permissions Permissions `json:"permissions"`
|
||||
ApiKeys map[string]AuthToken `json:"apiKeys,omitempty"`
|
||||
LoginMethod LoginMethod `json:"loginMethod"`
|
||||
// legacy for migration purposes... og filebrowser has perm attribute
|
||||
Perm Permissions `json:"perm,omitzero"`
|
||||
}
|
||||
|
||||
type SourceScope struct {
|
||||
Name string `json:"name"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
type NonAdminEditable struct {
|
||||
Preview PreviewOptions `json:"preview"`
|
||||
StickySidebar bool `json:"stickySidebar"`
|
||||
DarkMode bool `json:"darkMode"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Locale string `json:"locale"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Sorting Sorting `json:"sorting"`
|
||||
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{
|
||||
NonAdminEditable: NonAdminEditable{
|
||||
Password: "publicUser", // temp user not registered
|
||||
ViewMode: "normal",
|
||||
},
|
||||
Username: "publicUser", // temp user not registered
|
||||
LockPassword: true,
|
||||
Permissions: Permissions{},
|
||||
}
|
||||
|
||||
func CleanUsername(s string) string {
|
||||
// Remove any trailing space to avoid ending on -
|
||||
s = strings.Trim(s, " ")
|
||||
s = strings.Replace(s, "..", "", -1)
|
||||
|
||||
// Replace all characters which not in the list `0-9A-Za-z@_\-.` with a dash
|
||||
s = invalidFilenameChars.ReplaceAllString(s, "-")
|
||||
|
||||
// Remove any multiple dashes caused by replacements above
|
||||
s = dashes.ReplaceAllString(s, "-")
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type EventMessage struct {
|
||||
EventType string `json:"eventType"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type sourceEvent struct {
|
||||
source string
|
||||
event EventMessage
|
||||
}
|
||||
|
||||
type userEvent struct {
|
||||
event EventMessage
|
||||
users []string
|
||||
}
|
||||
|
||||
var (
|
||||
BroadcastChan = make(chan EventMessage, 100)
|
||||
userEventChan = make(chan userEvent, 100)
|
||||
debounceInputChan = make(chan EventMessage, 100)
|
||||
sourceUpdateChan = make(chan sourceEvent, 100)
|
||||
|
||||
userClientsMu sync.RWMutex
|
||||
userClients = make(map[string][]chan EventMessage)
|
||||
sourceClientsMu sync.RWMutex
|
||||
sourceClients = make(map[string]map[chan EventMessage]struct{})
|
||||
)
|
||||
|
||||
func init() {
|
||||
go handleUserEvents()
|
||||
go handleSourceUpdates() // Add this
|
||||
|
||||
}
|
||||
|
||||
func handleUserEvents() {
|
||||
for ue := range userEventChan {
|
||||
for _, user := range ue.users {
|
||||
userClientsMu.RLock()
|
||||
conns := userClients[user]
|
||||
userClientsMu.RUnlock()
|
||||
|
||||
for _, ch := range conns {
|
||||
select {
|
||||
case ch <- ue.event:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Register(username string, sources []string) chan EventMessage {
|
||||
ch := make(chan EventMessage, 10)
|
||||
|
||||
userClientsMu.Lock()
|
||||
userClients[username] = append(userClients[username], ch)
|
||||
userClientsMu.Unlock()
|
||||
|
||||
sourceClientsMu.Lock()
|
||||
for _, source := range sources {
|
||||
if sourceClients[source] == nil {
|
||||
sourceClients[source] = make(map[chan EventMessage]struct{})
|
||||
}
|
||||
sourceClients[source][ch] = struct{}{}
|
||||
}
|
||||
sourceClientsMu.Unlock()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func Unregister(username string, ch chan EventMessage) {
|
||||
userClientsMu.Lock()
|
||||
conns := userClients[username]
|
||||
for i, c := range conns {
|
||||
if c == ch {
|
||||
userClients[username] = append(conns[:i], conns[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(userClients[username]) == 0 {
|
||||
delete(userClients, username)
|
||||
}
|
||||
userClientsMu.Unlock()
|
||||
|
||||
sourceClientsMu.Lock()
|
||||
for source, clients := range sourceClients {
|
||||
delete(clients, ch)
|
||||
if len(clients) == 0 {
|
||||
delete(sourceClients, source)
|
||||
}
|
||||
}
|
||||
sourceClientsMu.Unlock()
|
||||
|
||||
close(ch)
|
||||
}
|
||||
|
||||
func SendToUsers(eventType, message string, users []string) {
|
||||
userEventChan <- userEvent{
|
||||
event: EventMessage{EventType: eventType, Message: message},
|
||||
users: users,
|
||||
}
|
||||
}
|
||||
|
||||
func SendSourceUpdate(source string, message string) {
|
||||
sourceUpdateChan <- sourceEvent{
|
||||
source: source,
|
||||
event: EventMessage{
|
||||
EventType: "sourceUpdate",
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func DebouncedBroadcast(eventType, message string) {
|
||||
debounceInputChan <- EventMessage{
|
||||
EventType: eventType,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
func handleSourceUpdates() {
|
||||
for update := range sourceUpdateChan {
|
||||
sourceClientsMu.RLock()
|
||||
clients := sourceClients[update.source]
|
||||
sourceClientsMu.RUnlock()
|
||||
|
||||
for ch := range clients {
|
||||
select {
|
||||
case ch <- update.event:
|
||||
default:
|
||||
// Optional: log dropped message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var typeRegexp = regexp.MustCompile(`type:(\S+)`)
|
||||
var AllFiletypeOptions = []string{
|
||||
"image",
|
||||
"audio",
|
||||
"archive",
|
||||
"video",
|
||||
"doc",
|
||||
"text",
|
||||
}
|
||||
|
||||
// Document file extensions
|
||||
var documentTypes = []string{
|
||||
// Common Document Formats
|
||||
".doc", ".docx", // Microsoft Word
|
||||
".pdf", // Portable Document Format
|
||||
".odt", // OpenDocument Text
|
||||
".rtf", // Rich Text Format
|
||||
".conf",
|
||||
".bash_history",
|
||||
".gitignore",
|
||||
".htpasswd",
|
||||
".profile",
|
||||
".dockerignore",
|
||||
".editorconfig",
|
||||
|
||||
// Presentation Formats
|
||||
".ppt", ".pptx", // Microsoft PowerPoint
|
||||
".odp", // OpenDocument Presentation
|
||||
|
||||
// google docs
|
||||
".gdoc",
|
||||
|
||||
// google sheet
|
||||
".gsheet",
|
||||
|
||||
// Spreadsheet Formats
|
||||
".xls", ".xlsx", // Microsoft Excel
|
||||
".ods", // OpenDocument Spreadsheet
|
||||
|
||||
// Other Document Formats
|
||||
".epub", // Electronic Publication
|
||||
".mobi", // Amazon Kindle
|
||||
".fb2", // FictionBook
|
||||
}
|
||||
|
||||
// Text-based file extensions
|
||||
var textTypes = []string{
|
||||
// Common Text Formats
|
||||
".txt",
|
||||
".md", // Markdown
|
||||
|
||||
// Scripting and Programming Languages
|
||||
".sh", // Bash script
|
||||
".py", // Python
|
||||
".js", // JavaScript
|
||||
".ts", // TypeScript
|
||||
".php", // PHP
|
||||
".rb", // Ruby
|
||||
".go", // Go
|
||||
".java", // Java
|
||||
".c", ".cpp", // C/C++
|
||||
".cs", // C#
|
||||
".swift", // Swift
|
||||
|
||||
// Configuration Files
|
||||
".yaml", ".yml", // YAML
|
||||
".json", // JSON
|
||||
".xml", // XML
|
||||
".ini", // INI
|
||||
".toml", // TOML
|
||||
".cfg", // Configuration file
|
||||
|
||||
// Other Text-Based Formats
|
||||
".css", // Cascading Style Sheets
|
||||
".html", ".htm", // HyperText Markup Language
|
||||
".sql", // SQL
|
||||
".csv", // Comma-Separated Values
|
||||
".tsv", // Tab-Separated Values
|
||||
".log", // Log file
|
||||
".bat", // Batch file
|
||||
".ps1", // PowerShell script
|
||||
".tex", // LaTeX
|
||||
".bib", // BibTeX
|
||||
}
|
||||
|
||||
// Compressed file extensions
|
||||
var compressedFile = []string{
|
||||
".7z",
|
||||
".rar",
|
||||
".zip",
|
||||
".tar",
|
||||
".gz",
|
||||
".xz",
|
||||
".bz2",
|
||||
".tgz", // tar.gz
|
||||
".tbz2", // tar.bz2
|
||||
".lzma",
|
||||
".lz4",
|
||||
".zstd",
|
||||
}
|
||||
|
||||
type SearchOptions struct {
|
||||
Conditions map[string]bool
|
||||
LargerThan int
|
||||
SmallerThan int
|
||||
Terms []string
|
||||
}
|
||||
|
||||
func extendedMimeTypeCheck(extension string) string {
|
||||
if isDoc(extension) {
|
||||
return "application/document"
|
||||
}
|
||||
if isText(extension) {
|
||||
return "text/plain"
|
||||
}
|
||||
return "blob"
|
||||
}
|
||||
|
||||
func ParseSearch(value string) SearchOptions {
|
||||
opts := SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
"exact": strings.Contains(value, "case:exact"),
|
||||
},
|
||||
Terms: []string{},
|
||||
}
|
||||
|
||||
// removes the options from the value
|
||||
value = strings.Replace(value, "case:exact", "", -1)
|
||||
value = strings.TrimSpace(value)
|
||||
|
||||
types := typeRegexp.FindAllStringSubmatch(value, -1)
|
||||
for _, filterType := range types {
|
||||
if len(filterType) == 1 {
|
||||
continue
|
||||
}
|
||||
filter := filterType[1]
|
||||
switch filter {
|
||||
case "image":
|
||||
opts.Conditions["image"] = true
|
||||
case "audio", "music":
|
||||
opts.Conditions["audio"] = true
|
||||
case "video":
|
||||
opts.Conditions["video"] = true
|
||||
case "doc":
|
||||
opts.Conditions["doc"] = true
|
||||
case "archive":
|
||||
opts.Conditions["archive"] = true
|
||||
case "folder":
|
||||
opts.Conditions["dir"] = true
|
||||
case "file":
|
||||
opts.Conditions["dir"] = false
|
||||
}
|
||||
if len(filter) < 8 {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(filter, "largerThan=") {
|
||||
opts.Conditions["larger"] = true
|
||||
size := strings.TrimPrefix(filter, "largerThan=")
|
||||
opts.LargerThan = updateSize(size)
|
||||
}
|
||||
if strings.HasPrefix(filter, "smallerThan=") {
|
||||
opts.Conditions["smaller"] = true
|
||||
size := strings.TrimPrefix(filter, "smallerThan=")
|
||||
opts.SmallerThan = updateSize(size)
|
||||
}
|
||||
}
|
||||
|
||||
if len(types) > 0 {
|
||||
// Remove the fields from the search value
|
||||
value = typeRegexp.ReplaceAllString(value, "")
|
||||
}
|
||||
|
||||
if value == "" {
|
||||
return opts
|
||||
}
|
||||
|
||||
// if the value starts with " and finishes what that character, we will
|
||||
// only search for that term
|
||||
if value[0] == '"' && value[len(value)-1] == '"' {
|
||||
unique := strings.TrimPrefix(value, "\"")
|
||||
unique = strings.TrimSuffix(unique, "\"")
|
||||
|
||||
opts.Terms = []string{unique}
|
||||
return opts
|
||||
}
|
||||
value = strings.TrimSpace(value)
|
||||
opts.Terms = strings.Split(value, "|")
|
||||
return opts
|
||||
}
|
||||
|
||||
func toInt(str string) int {
|
||||
val, err := strconv.Atoi(str)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func updateSize(given string) int {
|
||||
size := toInt(given)
|
||||
if size == 0 {
|
||||
return 100
|
||||
} else {
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func IsMatchingType(extension string, matchType string) bool {
|
||||
mimetype := mime.TypeByExtension(extension)
|
||||
if strings.HasPrefix(mimetype, matchType) {
|
||||
return true
|
||||
}
|
||||
switch matchType {
|
||||
case "doc":
|
||||
return isDoc(extension)
|
||||
case "text":
|
||||
return isText(extension)
|
||||
case "archive":
|
||||
return isArchive(extension)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isText(extension string) bool {
|
||||
for _, typefile := range textTypes {
|
||||
if extension == typefile {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isDoc(extension string) bool {
|
||||
for _, typefile := range documentTypes {
|
||||
if extension == typefile {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isArchive(extension string) bool {
|
||||
for _, typefile := range compressedFile {
|
||||
if extension == typefile {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Loaded 100 of 339 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user