forked from upstream/filebrowser
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c876613bf | ||
|
|
b4b44fdad0 | ||
|
|
46631837fa | ||
|
|
d880518029 |
No files matched your search
@@ -6,32 +6,7 @@ on:
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
test_frontend:
|
||||
name: Test Playwright
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i && npm run build
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go build -o filebrowser .
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.playwright
|
||||
push: false
|
||||
push_latest_to_registry:
|
||||
needs: [ test_frontend ]
|
||||
name: Push latest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -39,9 +14,9 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Find latest beta tag
|
||||
- name: Find latest tag
|
||||
run: |
|
||||
echo "LATEST_TAG=$(git describe --tags $(git rev-list --tags="*-beta*" --max-count=1))" >> $GITHUB_ENV
|
||||
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
|
||||
|
||||
+19
-28
@@ -1,37 +1,28 @@
|
||||
name: PR Request
|
||||
name: pr-request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "dev/v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "beta/v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "stable/v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "main"
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
jobs:
|
||||
test_frontend:
|
||||
name: Test Playwright
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i && npm run build
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go build -o filebrowser .
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.playwright
|
||||
push: false
|
||||
#test_frontend:
|
||||
# name: Push release
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v4
|
||||
# - name: Set up QEMU
|
||||
# uses: docker/setup-qemu-action@v3.0.0
|
||||
# - name: Set up Docker Buildx
|
||||
# uses: docker/setup-buildx-action@v3.0.0
|
||||
# - name: Build
|
||||
# uses: docker/build-push-action@v6
|
||||
# with:
|
||||
# context: .
|
||||
# file: ./Dockerfile.playwright
|
||||
# push: false
|
||||
push_pr_to_registry:
|
||||
name: Push PR
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
name: regular tests
|
||||
name: dev tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
test-backend:
|
||||
|
||||
@@ -1,40 +1,15 @@
|
||||
name: stable release
|
||||
name: version release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "stable/v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
test_frontend:
|
||||
name: Test Playwright
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i && npm run build
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go build -o filebrowser .
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.playwright
|
||||
push: false
|
||||
create_release_tag:
|
||||
needs: [ test_frontend ]
|
||||
create_release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -44,11 +19,7 @@ jobs:
|
||||
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
|
||||
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
|
||||
id: extract_branch
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
@@ -56,16 +27,15 @@ jobs:
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
target_commitish: ${{ steps.extract_branch.outputs.branch_name }}
|
||||
target_commitish: ${{ steps.extract_branch.outputs.branch }}
|
||||
token: ${{ secrets.PAT }}
|
||||
tag_name: ${{ steps.extract_branch.outputs.tag_name }}
|
||||
tag_name: ${{ steps.extract_branch.outputs.branch }}
|
||||
prerelease: false
|
||||
draft: false
|
||||
generate_release_notes: true
|
||||
name: ${{ steps.extract_branch.outputs.tag_name }}
|
||||
name: ${{ steps.extract_branch.outputs.branch }}
|
||||
|
||||
push_release_to_registry:
|
||||
needs: [ test_frontend ]
|
||||
name: Push release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -90,7 +60,7 @@ jobs:
|
||||
run: |
|
||||
JSON="${{ steps.meta.outputs.tags }}"
|
||||
# Use jq to remove 'v' from the version field
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:stable\/v/filebrowser:/')
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
|
||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -1,107 +0,0 @@
|
||||
name: beta release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "beta/v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
test_frontend:
|
||||
name: Test Playwright
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i && npm run build
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go build -o filebrowser .
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.playwright
|
||||
push: false
|
||||
create_release_tag:
|
||||
needs: [ test_frontend ]
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
run: |
|
||||
original_branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
|
||||
echo "branch_name=$transformed_branch" >> $GITHUB_OUTPUT
|
||||
tag_name=$(echo "$original_branch" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+')-beta
|
||||
echo "tag_name=$tag_name" >> $GITHUB_OUTPUT
|
||||
id: extract_branch
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
target_commitish: ${{ steps.extract_branch.outputs.branch_name }}
|
||||
token: ${{ secrets.PAT }}
|
||||
tag_name: ${{ steps.extract_branch.outputs.tag_name }}
|
||||
prerelease: false # change this to false when stable gets released
|
||||
make_latest: true # change this to false when stable gets released
|
||||
draft: false
|
||||
generate_release_notes: true
|
||||
name: ${{ steps.extract_branch.outputs.tag_name }}
|
||||
|
||||
push_release_to_registry:
|
||||
needs: [ test_frontend ]
|
||||
name: Push release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: gtstef/filebrowser
|
||||
- name: Strip v from version number
|
||||
id: modify-json
|
||||
run: |
|
||||
JSON="${{ steps.meta.outputs.tags }}"
|
||||
# Use jq to remove 'v' from the version field
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:beta\/v/filebrowser:beta_v/')
|
||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
build-args: |
|
||||
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
||||
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -3,7 +3,7 @@ name: dev release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "dev/v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "dev_v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
run: |
|
||||
JSON="${{ steps.meta.outputs.tags }}"
|
||||
# Use jq to remove 'v' from the version field
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:dev\/v/filebrowser:dev_v/')
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
|
||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
name: tag update
|
||||
name: tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+*"
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -8,7 +8,6 @@ rice-box.go
|
||||
/frontend/dist
|
||||
/frontend/pkg
|
||||
/frontend/test-results
|
||||
/frontend/loginAuth.json
|
||||
/frontend/package-lock.json
|
||||
/backend/vendor
|
||||
/backend/*.cov
|
||||
@@ -39,9 +38,3 @@ yarn-error.log*
|
||||
*.sw*
|
||||
bin/
|
||||
build/
|
||||
|
||||
# docs
|
||||
|
||||
/autodocs/public
|
||||
/autodocs/themes
|
||||
/autodocs/resources
|
||||
@@ -2,41 +2,6 @@
|
||||
|
||||
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.4.0-beta
|
||||
|
||||
**New Features**
|
||||
- Better logging
|
||||
- highly configurable
|
||||
- api logs include user
|
||||
- openoffice support
|
||||
|
||||
## 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**
|
||||
|
||||
+18
-4
@@ -1,5 +1,19 @@
|
||||
FROM gtstef/playwright-base
|
||||
FROM golang:1.23-alpine AS base
|
||||
WORKDIR /app
|
||||
COPY [ "./backend/filebrowser*", "./"]
|
||||
COPY [ "./frontend/", "./" ]
|
||||
RUN ./filebrowser -c filebrowser-playwright.yaml & sleep 2 && npx playwright test
|
||||
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,5 +0,0 @@
|
||||
FROM node:22-slim
|
||||
WORKDIR /app
|
||||
COPY ./frontend/package.json ./
|
||||
RUN npm i @playwright/test
|
||||
RUN npx playwright install --with-deps firefox
|
||||
@@ -9,9 +9,11 @@
|
||||
<img width="800" src="https://github.com/user-attachments/assets/b16acd67-0292-437a-a06c-bc83f95758e6" title="Main Screenshot">
|
||||
</p>
|
||||
|
||||
> [!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)
|
||||
|
||||
> [!WARNING]
|
||||
> There is no stable version yet.
|
||||
> (planned for later this year after these are complete: multiple sources support, initial onboarding page, official automated docs website)
|
||||
> 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.
|
||||
|
||||
FileBrowser Quantum is a fork of the file browser open-source project with the following changes:
|
||||
|
||||
@@ -153,6 +155,3 @@ Starred/pinned files | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||
Content preview icons | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ |
|
||||
Plugins support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
Chromecast support | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
Share collections of files | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
Can archive selected files | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
Can browse archive files | ❌ | ❌ | ❌ | ❌ | ❌ | ✅
|
||||
@@ -1,5 +0,0 @@
|
||||
+++
|
||||
date = '{{ .Date }}'
|
||||
draft = true
|
||||
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
|
||||
+++
|
||||
@@ -1,334 +0,0 @@
|
||||
---
|
||||
weight: 200
|
||||
title: "Installation"
|
||||
description: "How to install Filebrowser"
|
||||
icon: "settings"
|
||||
date: "2025-01-15"
|
||||
lastmod: "2025-01-15"
|
||||
draft: false
|
||||
---
|
||||
|
||||
## Install
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
## Example frontend config:
|
||||
```
|
||||
frontend:
|
||||
name: "Your custom name"
|
||||
disableDefaultLinks: true
|
||||
externalLinks:
|
||||
- text: "Your custom link"
|
||||
url: "https://example.com"
|
||||
```
|
||||
|
||||
## Example advanced source config:
|
||||
```
|
||||
server:
|
||||
port: 80
|
||||
sources:
|
||||
- name: "default"
|
||||
path: "/mnt/folder"
|
||||
config:
|
||||
ignoreHidden: true
|
||||
ignoreZeroSizeFolders: true
|
||||
exclude:
|
||||
files:
|
||||
- "/myfile.txt" # for example, corresponds to "/mnt/folder/myfile.txt"
|
||||
- "/subfolder/another.txt"
|
||||
folders:
|
||||
- "/subfolder/ignoreMe"
|
||||
fileEndsWith:
|
||||
- ".zip" # excludes any files that end with ".zip"
|
||||
- ".tar.gz"
|
||||
- "-hidden.jpg"
|
||||
```
|
||||
|
||||
## All possible configurations
|
||||
|
||||
Here is an expanded config file which includes all possible configurations:
|
||||
|
||||
```
|
||||
server:
|
||||
CreateUserDir: false
|
||||
UserHomeBasePath: ""
|
||||
indexing: true
|
||||
numImageProcessors: 4
|
||||
socket: ""
|
||||
tlsKey: ""
|
||||
tlsCert: ""
|
||||
enableThumbnails: false
|
||||
resizePreview: true
|
||||
typeDetectionByHeader: true
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
address: ""
|
||||
log: "stdout"
|
||||
database: "/database/database.db"
|
||||
root: "/srv"
|
||||
sources: # optional only needed for custom index configuration, can use root still for default index config.
|
||||
auth:
|
||||
adminUsername: admin
|
||||
adminPassword: admin
|
||||
recaptcha:
|
||||
host: ""
|
||||
key: ""
|
||||
secret: ""
|
||||
tokenExpirationTime: 2h
|
||||
header: ""
|
||||
method: json
|
||||
signup: false
|
||||
frontend:
|
||||
name: ""
|
||||
disableDefaultLinks: false
|
||||
disableUsedPercentage: true
|
||||
externalLinks:
|
||||
- text: "Your custom link"
|
||||
url: "https://example.com"
|
||||
title: "this text appears when hovering over link"
|
||||
color: ""
|
||||
userDefaults:
|
||||
settingsAllowed: true
|
||||
darkMode: false
|
||||
scope: ""
|
||||
locale: "en"
|
||||
viewMode: ""
|
||||
singleClick: true
|
||||
permissions:
|
||||
admin: true
|
||||
execute: true
|
||||
create: true
|
||||
rename: true
|
||||
modify: true
|
||||
delete: true
|
||||
share: true
|
||||
download: true
|
||||
api: true
|
||||
hideDotfiles: false
|
||||
dateFormat: false
|
||||
```
|
||||
|
||||
Here are the defaults if nothing is set:
|
||||
|
||||
```
|
||||
server:
|
||||
enableThumbnails: true
|
||||
enableExec: false
|
||||
port: 80
|
||||
numImageProcessors: 4
|
||||
baseURL: ""
|
||||
database: database.db
|
||||
log: stdout
|
||||
root: /srv
|
||||
sources:
|
||||
auth:
|
||||
adminUsername: admin
|
||||
adminPassword: admin
|
||||
method: password
|
||||
recaptcha:
|
||||
host: ""
|
||||
userDefaults:
|
||||
settingsAllowed: true
|
||||
darkMode: false
|
||||
locale: "en"
|
||||
scope: "."
|
||||
lockPassword: false
|
||||
hideDotfiles: true
|
||||
permissions:
|
||||
create: true
|
||||
rename: true
|
||||
modify: true
|
||||
delete: true
|
||||
share: true
|
||||
download: true
|
||||
api: false
|
||||
```
|
||||
|
||||
## About each configuration
|
||||
|
||||
# Configuration Settings Documentation
|
||||
|
||||
## About each configuration
|
||||
|
||||
### Server configuration settings
|
||||
|
||||
- `indexingInterval`: This optional paramter disables smart indexing and specifies a time in minutes the system waits before checking for filesystem changes. See [indexing readme](indexing.md) for more information.
|
||||
|
||||
- `indexing`: This enables or disables indexing. (Note: search will not work without indexing) Default: `true`
|
||||
|
||||
- `numImageProcessors`: This is the number of image processors available. Default: `4`
|
||||
|
||||
- `socket`: This is the socket configuration.
|
||||
|
||||
- `tlsKey`: This is the TLS key configuration.
|
||||
|
||||
- `tlsCert`: This is the TLS certificate configuration.
|
||||
|
||||
- `enableThumbnails`: This boolean value determines whether thumbnails are enabled on ui. Default: `true`
|
||||
|
||||
- `resizePreview`: This boolean value determines whether preview resizing is enabled. Default: `false`
|
||||
|
||||
- `typeDetectionByHeader`: This boolean value determines whether type detection is based on headers.
|
||||
|
||||
- `port`: This is the port number on which the server is running. Default: `80`
|
||||
|
||||
- `baseURL`: This is the base URL for the server. Default: `""`
|
||||
|
||||
- `address`: This is the server address configuration. Default: `0.0.0.0`
|
||||
|
||||
- `log`: This specifies the log destination. Default: `stdout`
|
||||
|
||||
- `database`: This is the database file path + filename that will be created if it does not already exist. If it exists, it will use the existing file. Default `database.db`
|
||||
|
||||
- `root`: This is the root directory path. Default: `/srv`
|
||||
|
||||
- `CreateUserDir`: Boolean to create user directory on user creation. Default: `false`
|
||||
|
||||
- `UserHomeBasePath`: String to define user home directory base path. Default: `""`
|
||||
|
||||
- `externalUrl`: String to define a domain and potentially additional subpath for shared links to use. Example "https://mydomain.com/", Default: `""`
|
||||
|
||||
|
||||
### Auth configuration settings
|
||||
|
||||
- `recaptcha`:
|
||||
|
||||
- `host`: This is the host for reCAPTCHA.
|
||||
|
||||
- `key`: This is the reCAPTCHA key.
|
||||
|
||||
- `secret`: This is the reCAPTCHA secret.
|
||||
|
||||
- `header`: This is the authentication header.
|
||||
|
||||
- `method`: This is the authentication method used (e.g., "json"). Possible values:
|
||||
- `password` - username and password
|
||||
- `hook` - hook authentication
|
||||
- `proxy` - proxy authentication
|
||||
- `oath` - oath authentication
|
||||
- `noauth` - no authentication/login required.
|
||||
|
||||
- `Signup`: This boolean value indicates whether user signup is enabled on the login page. NOTE: Be mindful of `userDefaults` settings if enabled. Default: `false`
|
||||
|
||||
- `AdminUsername`: This is the username of the admin user. Default: `admin`
|
||||
|
||||
- `AdminPassword`: This is the password of the admin user. Default: `admin`
|
||||
|
||||
### Frontend configuration settings
|
||||
|
||||
- `name`: This is the name of the frontend.
|
||||
|
||||
- `disableExternal`: This boolean value determines whether external access is disabled.
|
||||
|
||||
- `disableUsedPercentage`: This boolean value determines whether used percentage is disabled.
|
||||
|
||||
- `files`: This is the files configuration.
|
||||
|
||||
- `theme`: This is the theme configuration.
|
||||
|
||||
- `color`: This is the color configuration.
|
||||
|
||||
### UserDefaults configuration settings
|
||||
|
||||
- `darkMode`: Determines whether dark mode is enabled for the user (`true` or `false`)
|
||||
|
||||
- `settingsAllowed`: Determines whether settings page is enabled for the user (`true` or `false`)
|
||||
|
||||
- `scope`: This is a scope of the permissions, "." or "./" means all directories, "./downloads" would mean only the downloads folder.
|
||||
|
||||
- `locale`: String locale configuration. Default: `en`
|
||||
|
||||
- `viewMode`: This is the view mode configuration. Possible values: `normal`, `compact`, `list`, and `gallery`. default: `normal`
|
||||
|
||||
- `singleClick`: This boolean value determines whether single-click is enabled. (`true` or `false`)
|
||||
|
||||
- `sorting`:
|
||||
|
||||
- `by`: This is the sorting method used (e.g., "asc").
|
||||
|
||||
- `asc`: This boolean value determines the sorting order is ascending or descending. (`true` or `false`)
|
||||
|
||||
- `permissions`:
|
||||
|
||||
- `admin`: This boolean value determines whether admin permissions are granted.
|
||||
|
||||
- `execute`: This boolean value determines whether execute permissions are granted.
|
||||
|
||||
- `create`: This boolean value determines whether create permissions are granted.
|
||||
|
||||
- `rename`: This boolean value determines whether rename permissions are granted.
|
||||
|
||||
- `modify`: This boolean value determines whether modify permissions are granted.
|
||||
|
||||
- `delete`: This boolean value determines whether delete permissions are granted.
|
||||
|
||||
- `share`: This boolean value determines whether share permissions are granted.
|
||||
|
||||
- `download`: This boolean value determines whether download permissions are granted.
|
||||
|
||||
- `api`: Ability to create and manage API keys.
|
||||
|
||||
- `hideDotfiles`: This boolean value determines whether dotfiles are hidden. (`true` or `false`)
|
||||
|
||||
- `dateFormat`: This boolean value determines whether date formatting is enabled. (`true` or `false`)
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
weight: 200
|
||||
title: "API"
|
||||
description: "API Help"
|
||||
icon: "edit"
|
||||
date: "2025-01-15"
|
||||
lastmod: "2025-01-15"
|
||||
draft: false
|
||||
---
|
||||
|
||||
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>
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
weight: 100
|
||||
date: "2023-05-03T22:37:22+01:00"
|
||||
draft: false
|
||||
author: "Colin Wilson"
|
||||
title: "Quickstart"
|
||||
icon: "rocket_launch"
|
||||
description: "A quickstart guide to creating new content in Lotus Docs"
|
||||
publishdate: "2023-05-03T22:37:22+01:00"
|
||||
tags: ["Beginners"]
|
||||
---
|
||||
|
||||
## Requirements
|
||||
@@ -1,89 +0,0 @@
|
||||
# Note: Template blocks require a 'weight' parameter so they're correctly ordered on the landing page
|
||||
|
||||
# Hero
|
||||
hero:
|
||||
enable: true
|
||||
weight: 10
|
||||
width: 100%
|
||||
template: hero
|
||||
|
||||
|
||||
badge:
|
||||
text: "v0.4.0"
|
||||
color: success # primary, secondary, success, danger, warning, info, light, dark
|
||||
pill: true # boolean
|
||||
soft: false # boolean
|
||||
|
||||
titleLogo:
|
||||
path: "images/logos"
|
||||
filename: "title_logo.png"
|
||||
alt: "Lotus Docs Logo"
|
||||
height: 80px
|
||||
|
||||
title: "FileBrowser Quantum"
|
||||
subtitle: A modern self-hosted **file manager** for the web.
|
||||
|
||||
ctaButton:
|
||||
color: success
|
||||
btnText: "Go to the Docs"
|
||||
url: "docs"
|
||||
info: "**Open Source** Apache 2.0 Licensed."
|
||||
|
||||
# Image compare
|
||||
imageCompare:
|
||||
enable: true
|
||||
weight: 30
|
||||
template: image compare
|
||||
|
||||
title: Beautifully Customizable.
|
||||
subtitle: You can make it your own.
|
||||
|
||||
items:
|
||||
- title: Dark Mode
|
||||
config: {
|
||||
startingPoint: 50,
|
||||
addCircle: true,
|
||||
addCircleBlur: false,
|
||||
showLabels: true,
|
||||
labelOptions: {
|
||||
before: 'Dark',
|
||||
after: 'Light',
|
||||
onHover: false
|
||||
}
|
||||
}
|
||||
imagePath: "images/screenshots"
|
||||
imageBefore: "lotusdocs_dark_v0.8.webp"
|
||||
imageAfter: "lotusdocs_light_v0.8.webp"
|
||||
|
||||
- title: Branding
|
||||
config: {
|
||||
controlColor: "#3C4257",
|
||||
startingPoint: 25,
|
||||
addCircle: true,
|
||||
addCircleBlur: false,
|
||||
showLabels: true,
|
||||
labelOptions: {
|
||||
before: 'Inter',
|
||||
after: 'Life Saver',
|
||||
onHover: false
|
||||
}
|
||||
}
|
||||
imagePath: "images/screenshots"
|
||||
imageBefore: "lotusdocs_google_font_demo_inter_screenshot.webp"
|
||||
imageAfter: "lotusdocs_google_font_demo_lifesavers_screenshot.webp"
|
||||
|
||||
- title: Accent Color
|
||||
config: {
|
||||
startingPoint: 25,
|
||||
addCircle: true,
|
||||
addCircleBlur: true,
|
||||
showLabels: true,
|
||||
labelOptions: {
|
||||
before: 'Blue',
|
||||
after: 'Cardinal',
|
||||
onHover: false
|
||||
}
|
||||
}
|
||||
imagePath: "images/screenshots"
|
||||
imageBefore: "lotusdocs_blue_theme_colour.webp"
|
||||
imageAfter: "lotusdocs_cardinal_theme_colour.webp"
|
||||
@@ -1,8 +0,0 @@
|
||||
module github.com/gtsteffaniak/filebrowser
|
||||
|
||||
go 1.22.5
|
||||
|
||||
require (
|
||||
github.com/gohugoio/hugo-mod-bootstrap-scss/v5 v5.20300.20200 // indirect
|
||||
github.com/gtsteffaniak/filebrowserDocsTheme v0.0.0-20250118144715-40025a28c8d5 // indirect
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
github.com/gohugoio/hugo-mod-bootstrap-scss/v5 v5.20300.20200 h1:SmpwwN3DNzJWbV+IT8gaFu07ENUFpCvKou5BHYUKuVs=
|
||||
github.com/gohugoio/hugo-mod-bootstrap-scss/v5 v5.20300.20200/go.mod h1:kx8MBj9T7SFR8ZClWvKZPmmUxBaltkoXvnWlZZcSnYA=
|
||||
github.com/gohugoio/hugo-mod-jslibs-dist/popperjs/v2 v2.21100.20000/go.mod h1:mFberT6ZtcchrsDtfvJM7aAH2bDKLdOnruUHl0hlapI=
|
||||
github.com/gtsteffaniak/filebrowserDocsTheme v0.0.0-20250118144715-40025a28c8d5 h1:dwZ1GlVhtfgigYUoPHZd+PBczUgl7FpSJrxMhPQXXJQ=
|
||||
github.com/gtsteffaniak/filebrowserDocsTheme v0.0.0-20250118144715-40025a28c8d5/go.mod h1:9Ee1u+ac9+utA3Tt3SnoIYmoewMlx7uf/WLb0CVZ2ys=
|
||||
github.com/twbs/bootstrap v5.3.2+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0=
|
||||
@@ -1,8 +0,0 @@
|
||||
baseURL = 'https://example.org/'
|
||||
languageCode = 'en-us'
|
||||
title = 'Docs'
|
||||
|
||||
[module]
|
||||
[[module.imports]]
|
||||
path = "github.com/gtsteffaniak/filebrowserDocsTheme"
|
||||
disable = false
|
||||
@@ -21,20 +21,17 @@ builds:
|
||||
- "7"
|
||||
hooks:
|
||||
post:
|
||||
- upx --force-macos {{ .Path }} # Compress the binary with UPX
|
||||
- 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
|
||||
hooks:
|
||||
post:
|
||||
- upx {{ .Path }} # Compress the binary with UPX
|
||||
# - id: windows
|
||||
# ldflags: *ldflags
|
||||
# main: main.go
|
||||
# binary: filebrowser
|
||||
# goos:
|
||||
# - windows
|
||||
# goarch:
|
||||
# - amd64
|
||||
|
||||
archives:
|
||||
- name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
|
||||
|
||||
@@ -3,13 +3,13 @@ package auth
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
@@ -164,7 +164,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||
return nil, fmt.Errorf("user: failed to mkdir user home dir: [%s]", userHome)
|
||||
}
|
||||
u.Scope = userHome
|
||||
logger.Debug(fmt.Sprintf("user: %s, home dir: [%s].", u.Username, userHome))
|
||||
log.Printf("user: %s, home dir: [%s].", u.Username, userHome)
|
||||
|
||||
err = a.Users.Save(u)
|
||||
if err != nil {
|
||||
|
||||
+26
-28
@@ -3,6 +3,7 @@ package cmd
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -10,7 +11,6 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
fbhttp "github.com/gtsteffaniak/filebrowser/backend/http"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/img"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/swagger/docs"
|
||||
@@ -25,22 +25,21 @@ func getStore(config string) (*storage.Storage, bool) {
|
||||
settings.Initialize(config)
|
||||
store, hasDB, err := storage.InitializeDb(settings.Config.Server.Database)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("could not load db info: %v", err))
|
||||
log.Fatal("could not load db info: ", err)
|
||||
}
|
||||
return store, hasDB
|
||||
}
|
||||
|
||||
func generalUsage() {
|
||||
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
|
||||
`)
|
||||
commands:
|
||||
-v Print the version
|
||||
-c Print the default config file
|
||||
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
|
||||
` + "\n")
|
||||
}
|
||||
|
||||
func StartFilebrowser() {
|
||||
@@ -91,9 +90,9 @@ func StartFilebrowser() {
|
||||
getStore(dbConfig)
|
||||
// Create the user logic
|
||||
if asAdmin {
|
||||
logger.Info(fmt.Sprintf("Creating user as admin: %s\n", username))
|
||||
log.Printf("Creating user as admin: %s\n", username)
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Creating non-admin user: %s\n", username))
|
||||
log.Printf("Creating user: %s\n", username)
|
||||
}
|
||||
newUser := users.User{
|
||||
Username: username,
|
||||
@@ -104,15 +103,14 @@ func StartFilebrowser() {
|
||||
}
|
||||
err = storage.CreateUser(newUser, asAdmin)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("could not create user: %v", err))
|
||||
log.Fatal("Could not create user: ", err)
|
||||
}
|
||||
return
|
||||
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)
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -121,15 +119,15 @@ Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v
|
||||
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)
|
||||
sources := []string{}
|
||||
for _, v := range settings.Config.Server.Sources {
|
||||
sources = append(sources, v.Name+": "+v.Path)
|
||||
}
|
||||
logger.Info(fmt.Sprintf("Initializing FileBrowser Quantum (%v)", version.Version))
|
||||
logger.Info(fmt.Sprintf("Using Config file : %v", configPath))
|
||||
logger.Debug(fmt.Sprintf("Embeded frontend : %v", os.Getenv("FILEBROWSER_NO_EMBEDED") != "true"))
|
||||
logger.Info(database)
|
||||
logger.Info(fmt.Sprintf("Sources : %v", sources))
|
||||
log.Println("Sources :", sources)
|
||||
|
||||
serverConfig := settings.Config.Server
|
||||
swagInfo := docs.SwaggerInfo
|
||||
@@ -138,19 +136,19 @@ Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v
|
||||
// initialize indexing and schedule indexing ever n minutes (default 5)
|
||||
sourceConfigs := settings.Config.Server.Sources
|
||||
if len(sourceConfigs) == 0 {
|
||||
logger.Fatal("No sources configured, exiting...")
|
||||
log.Fatal("No sources configured, exiting...")
|
||||
}
|
||||
for _, source := range sourceConfigs {
|
||||
go files.Initialize(source)
|
||||
}
|
||||
if err := rootCMD(store, &serverConfig); err != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error starting filebrowser: %v", err))
|
||||
log.Fatal("Error starting filebrowser:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func rootCMD(store *storage.Storage, serverConfig *settings.Server) error {
|
||||
if serverConfig.NumImageProcessors < 1 {
|
||||
logger.Fatal("Image resize workers count could not be < 1")
|
||||
log.Fatal("Image resize workers count could not be < 1")
|
||||
}
|
||||
imgSvc := img.New(serverConfig.NumImageProcessors)
|
||||
|
||||
@@ -162,7 +160,7 @@ func rootCMD(store *storage.Storage, serverConfig *settings.Server) error {
|
||||
var err error
|
||||
fileCache, err = diskcache.NewFileCache(cacheDir)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("failed to create file cache: %v", err))
|
||||
log.Fatalf("failed to create file cache: %v", err)
|
||||
}
|
||||
} else {
|
||||
// No-op cache if no cacheDir is specified
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
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)
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
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)
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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,54 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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})
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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)
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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)
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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")
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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})
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
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) {
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
server:
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
root: "./tests/playwright-files"
|
||||
auth:
|
||||
method: password
|
||||
signup: false
|
||||
userDefaults:
|
||||
darkMode: true
|
||||
disableSettings: false
|
||||
scope: "."
|
||||
hideDotfiles: true
|
||||
singleClick: false
|
||||
permissions:
|
||||
admin: false
|
||||
create: false
|
||||
rename: false
|
||||
modify: false
|
||||
delete: false
|
||||
share: false
|
||||
download: false
|
||||
@@ -189,7 +189,7 @@ func MoveResource(source, realsrc, realdst string, isSrcDir bool) error {
|
||||
index := GetIndex(source)
|
||||
// refresh info for source and dest
|
||||
err = index.RefreshFileInfo(FileOptions{
|
||||
Path: filepath.Dir(realsrc),
|
||||
Path: realsrc,
|
||||
IsDir: isSrcDir,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -73,6 +74,7 @@ func Test_GetRealPath(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
realPath, isDir, _ := idx.GetRealPath(tt.paths...)
|
||||
fmt.Println(realPath, trimPrefix)
|
||||
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)
|
||||
|
||||
@@ -2,6 +2,7 @@ package files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -9,7 +10,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
@@ -52,11 +52,11 @@ func Initialize(source settings.Source) {
|
||||
|
||||
if !newIndex.Source.Config.Disabled {
|
||||
time.Sleep(time.Second)
|
||||
logger.Info("Initializing index and assessing file system complexity")
|
||||
log.Println("Initializing index and assessing file system complexity")
|
||||
newIndex.RunIndexing("/", false)
|
||||
go newIndex.setupIndexingScanners()
|
||||
} else {
|
||||
logger.Debug("Indexing disabled for source: " + newIndex.Source.Name)
|
||||
log.Println("Indexing disabled for source: ", newIndex.Source.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ func (idx *Index) indexDirectory(adjustedPath string, quick, recursive bool) err
|
||||
for _, item := range cacheDirItems {
|
||||
err = idx.indexDirectory(combinedPath+item.Name, quick, true)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("error indexing directory %v : %v", combinedPath+item.Name, err))
|
||||
fmt.Printf("error indexing directory %v : %v", combinedPath+item.Name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -147,7 +147,7 @@ func (idx *Index) indexDirectory(adjustedPath string, quick, recursive bool) err
|
||||
// Recursively index the subdirectory
|
||||
err = idx.indexDirectory(dirPath, quick, recursive)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to index directory %s: %v", dirPath, err))
|
||||
log.Printf("Failed to index directory %s: %v", dirPath, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -192,6 +192,7 @@ func (idx *Index) indexDirectory(adjustedPath string, quick, recursive bool) err
|
||||
}
|
||||
|
||||
func (idx *Index) makeIndexPath(subPath string) string {
|
||||
subPath = strings.ReplaceAll(subPath, "\\", "/")
|
||||
if strings.HasPrefix(subPath, "./") {
|
||||
subPath = strings.TrimPrefix(subPath, ".")
|
||||
}
|
||||
@@ -202,7 +203,6 @@ func (idx *Index) makeIndexPath(subPath string) string {
|
||||
subPath = strings.TrimSuffix(subPath, "/")
|
||||
// remove index prefix
|
||||
adjustedPath := strings.TrimPrefix(subPath, idx.Source.Path)
|
||||
adjustedPath = strings.ReplaceAll(adjustedPath, "\\", "/")
|
||||
// remove trailing slash
|
||||
adjustedPath = strings.TrimSuffix(adjustedPath, "/")
|
||||
if !strings.HasPrefix(adjustedPath, "/") {
|
||||
@@ -250,6 +250,7 @@ func (idx *Index) RefreshFileInfo(opts FileOptions) error {
|
||||
Path: opts.Path,
|
||||
IsDir: opts.IsDir,
|
||||
}
|
||||
|
||||
if !refreshOptions.IsDir {
|
||||
refreshOptions.Path = idx.makeIndexPath(filepath.Dir(refreshOptions.Path))
|
||||
refreshOptions.IsDir = true
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
)
|
||||
|
||||
// schedule in minutes
|
||||
@@ -34,7 +32,7 @@ func (idx *Index) newScanner(origin string) {
|
||||
}
|
||||
|
||||
// Log and sleep before indexing
|
||||
logger.Debug(fmt.Sprintf("Next scan in %v\n", sleepTime))
|
||||
log.Printf("Next scan in %v\n", sleepTime)
|
||||
time.Sleep(sleepTime)
|
||||
|
||||
idx.scannerMu.Lock()
|
||||
@@ -76,9 +74,9 @@ func (idx *Index) RunIndexing(origin string, quick bool) {
|
||||
prevNumDirs := idx.NumDirs
|
||||
prevNumFiles := idx.NumFiles
|
||||
if quick {
|
||||
logger.Debug("Starting quick scan")
|
||||
log.Println("Starting quick scan")
|
||||
} else {
|
||||
logger.Debug("Starting full scan")
|
||||
log.Println("Starting full scan")
|
||||
idx.NumDirs = 0
|
||||
idx.NumFiles = 0
|
||||
}
|
||||
@@ -87,9 +85,8 @@ func (idx *Index) RunIndexing(origin string, quick bool) {
|
||||
// Perform the indexing operation
|
||||
err := idx.indexDirectory("/", quick, true)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Error during indexing: %v", err))
|
||||
log.Printf("Error during indexing: %v", err)
|
||||
}
|
||||
firstRun := idx.LastIndexed == time.Time{}
|
||||
// Update the LastIndexed time
|
||||
idx.LastIndexed = time.Now()
|
||||
idx.indexingTime = int(time.Since(startTime).Seconds())
|
||||
@@ -105,20 +102,12 @@ func (idx *Index) RunIndexing(origin string, quick bool) {
|
||||
} else {
|
||||
idx.assessment = "normal"
|
||||
}
|
||||
if firstRun {
|
||||
logger.Info(fmt.Sprintf("Index assessment : complexity=%v directories=%v files=%v", idx.assessment, idx.NumDirs, idx.NumFiles))
|
||||
} else {
|
||||
logger.Debug(fmt.Sprintf("Index assessment : complexity=%v directories=%v files=%v", idx.assessment, idx.NumDirs, idx.NumFiles))
|
||||
}
|
||||
log.Printf("Index assessment : complexity=%v directories=%v files=%v \n", idx.assessment, idx.NumDirs, idx.NumFiles)
|
||||
if idx.NumDirs != prevNumDirs || idx.NumFiles != prevNumFiles {
|
||||
idx.FilesChangedDuringIndexing = true
|
||||
}
|
||||
}
|
||||
if firstRun {
|
||||
logger.Info(fmt.Sprintf("Time spent indexing : %v seconds", idx.indexingTime))
|
||||
} else {
|
||||
logger.Debug(fmt.Sprintf("Time spent indexing : %v seconds", idx.indexingTime))
|
||||
}
|
||||
log.Printf("Time Spent Indexing : %v seconds\n", idx.indexingTime)
|
||||
}
|
||||
|
||||
func (idx *Index) setupIndexingScanners() {
|
||||
|
||||
@@ -91,6 +91,9 @@ func TestMakeIndexPath(t *testing.T) {
|
||||
{"Trailing slash removed", "/test/", "/test"},
|
||||
{"Subpath without root prefix", "/other/test", "/other/test"},
|
||||
{"Complex nested paths", "/nested/path", "/nested/path"},
|
||||
// Windows
|
||||
{"Mixed slash", "/first\\second", "/first/second"},
|
||||
{"Windows slash", "\\first\\second", "/first/second"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -102,27 +105,6 @@ func TestMakeIndexPath(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
tests = []struct {
|
||||
name string
|
||||
subPath string
|
||||
expected string
|
||||
}{
|
||||
// Windows
|
||||
{"Mixed slash", "/first\\second", "/first/second"},
|
||||
{"Windows slash", "\\first\\second", "/first/second"},
|
||||
{"Windows full path", "C:\\Users\\testfolder\\nestedfolder", "/testfolder/nestedfolder"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
idx := &Index{Source: settings.Source{Path: "C:\\Users"}}
|
||||
result := idx.makeIndexPath(tt.subPath)
|
||||
if result != tt.expected {
|
||||
t.Errorf("makeIndexPath(%q)\ngot %q\nwant %q", tt.name, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeIndexPathRoot(t *testing.T) {
|
||||
|
||||
@@ -26,25 +26,9 @@ func MoveFile(src, dst string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyFile copies a file or directory from source to dest and returns an error if any.
|
||||
// CopyFile copies a file 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 {
|
||||
@@ -52,7 +36,7 @@ func copySingleFile(source, dest string) error {
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// Create the destination directory if needed.
|
||||
// Makes the directory needed to create the dst file.
|
||||
err = os.MkdirAll(filepath.Dir(dest), 0775) //nolint:gomnd
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -84,43 +68,6 @@ func copySingleFile(source, dest string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyDirectory handles copying directories recursively.
|
||||
func copyDirectory(source, dest string) error {
|
||||
// Create the destination directory.
|
||||
err := os.MkdirAll(dest, 0775) //nolint:gomnd
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Read the contents of the source directory.
|
||||
entries, err := os.ReadDir(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Iterate over each entry in the directory.
|
||||
for _, entry := range entries {
|
||||
srcPath := filepath.Join(source, entry.Name())
|
||||
destPath := filepath.Join(dest, entry.Name())
|
||||
|
||||
if entry.IsDir() {
|
||||
// Recursively copy subdirectories.
|
||||
err = copyDirectory(srcPath, destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Copy files.
|
||||
err = copySingleFile(srcPath, destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommonPrefix returns the common directory path of provided files.
|
||||
func CommonPrefix(sep byte, paths ...string) string {
|
||||
// Handle special cases.
|
||||
|
||||
+4
-4
@@ -17,7 +17,7 @@ require (
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/swaggo/http-swagger v1.3.4
|
||||
github.com/swaggo/swag v1.16.4
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/image v0.23.0
|
||||
golang.org/x/text v0.21.0
|
||||
)
|
||||
@@ -43,9 +43,9 @@ require (
|
||||
github.com/swaggo/files v1.0.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.3.11 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/tools v0.28.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
+8
-8
@@ -114,8 +114,8 @@ go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
|
||||
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
@@ -133,8 +133,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
@@ -153,8 +153,8 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -168,8 +168,8 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
libError "errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -17,7 +18,6 @@ import (
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
@@ -129,12 +129,12 @@ func signupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
userHome, err := config.MakeUserDir(user.Username, user.Scope, files.RootPaths["default"])
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("create user: failed to mkdir user home dir: [%s]", userHome))
|
||||
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
user.Scope = userHome
|
||||
logger.Debug(fmt.Sprintf("new user: %s, home dir: [%s].", user.Username, userHome))
|
||||
log.Printf("new user: %s, home dir: [%s].", user.Username, userHome)
|
||||
err = store.Users.Save(&user)
|
||||
if err == errors.ErrExist {
|
||||
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
|
||||
|
||||
+22
-28
@@ -4,6 +4,7 @@ import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -11,7 +12,6 @@ import (
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/runner"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
@@ -131,8 +131,6 @@ func withUserHelper(fn handleFunc) handleFunc {
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
setUserInResponseWriter(w, data.user)
|
||||
|
||||
// Call the handler function, passing in the context
|
||||
return fn(w, r, data)
|
||||
}
|
||||
@@ -174,14 +172,14 @@ func wrapHandler(fn handleFunc) http.HandlerFunc {
|
||||
// Marshal the error response to JSON
|
||||
errorBytes, marshalErr := json.Marshal(response)
|
||||
if marshalErr != nil {
|
||||
logger.Error(fmt.Sprintf("Error marshalling error response: %v", marshalErr))
|
||||
log.Printf("Error marshalling error response: %v", marshalErr)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Write the JSON error response
|
||||
if _, writeErr := w.Write(errorBytes); writeErr != nil {
|
||||
logger.Error(fmt.Sprintf("Error writing error response: %v", writeErr))
|
||||
log.Printf("Error writing error response: %v", writeErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -235,7 +233,6 @@ type ResponseWriterWrapper struct {
|
||||
StatusCode int
|
||||
wroteHeader bool
|
||||
PayloadSize int
|
||||
User string
|
||||
}
|
||||
|
||||
// WriteHeader captures the status code and ensures it's only written once
|
||||
@@ -258,16 +255,6 @@ func (w *ResponseWriterWrapper) Write(b []byte) (int, error) {
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
// Helper function to set the user in the ResponseWriterWrapper
|
||||
func setUserInResponseWriter(w http.ResponseWriter, user *users.User) {
|
||||
// Wrap the response writer to set the user field
|
||||
if wrappedWriter, ok := w.(*ResponseWriterWrapper); ok {
|
||||
if user != nil {
|
||||
wrappedWriter.User = user.Username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LoggingMiddleware logs each request and its status code.
|
||||
func LoggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -280,23 +267,30 @@ func LoggingMiddleware(next http.Handler) http.Handler {
|
||||
// Call the next handler.
|
||||
next.ServeHTTP(wrappedWriter, r)
|
||||
|
||||
// Determine the color based on the status code.
|
||||
color := "\033[32m" // Default green color
|
||||
if wrappedWriter.StatusCode >= 300 && wrappedWriter.StatusCode < 500 {
|
||||
color = "\033[33m" // Yellow for client errors (4xx)
|
||||
} else if wrappedWriter.StatusCode >= 500 {
|
||||
color = "\033[31m" // Red for server errors (5xx)
|
||||
}
|
||||
|
||||
// Capture the full URL path including the query parameters.
|
||||
fullURL := r.URL.Path
|
||||
if r.URL.RawQuery != "" {
|
||||
fullURL += "?" + r.URL.RawQuery
|
||||
}
|
||||
truncUser := wrappedWriter.User
|
||||
if len(truncUser) > 12 {
|
||||
truncUser = truncUser[:10] + ".."
|
||||
}
|
||||
logger.Api(
|
||||
fmt.Sprintf("%-7s | %3d | %-15s | %-12s | %-12s | \"%s\"",
|
||||
r.Method,
|
||||
wrappedWriter.StatusCode, // Captured status code
|
||||
r.RemoteAddr,
|
||||
truncUser,
|
||||
time.Since(start).String(),
|
||||
fullURL), wrappedWriter.StatusCode)
|
||||
|
||||
// Log the request, status code, and response size.
|
||||
log.Printf("%s%-7s | %3d | %-15s | %-12s | \"%s\"%s",
|
||||
color,
|
||||
r.Method,
|
||||
wrappedWriter.StatusCode, // Captured status code
|
||||
r.RemoteAddr,
|
||||
time.Since(start).String(),
|
||||
fullURL,
|
||||
"\033[0m", // Reset color
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/img"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
)
|
||||
|
||||
type ImgService interface {
|
||||
@@ -142,7 +141,7 @@ func createPreview(imgSvc ImgService, fileCache FileCache, file files.ExtendedFi
|
||||
go func() {
|
||||
cacheKey := previewCacheKey(file.RealPath, previewSize, file.FileInfo.ModTime)
|
||||
if err := fileCache.Store(context.Background(), cacheKey, buf.Bytes()); err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to cache resized image: %v", err))
|
||||
fmt.Printf("failed to cache resized image: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
+18
-1
@@ -15,7 +15,7 @@ import (
|
||||
func publicShareHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
file, ok := d.raw.(files.ExtendedFileInfo)
|
||||
if !ok {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type files.FileInfo")
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type *files.FileInfo")
|
||||
}
|
||||
file.Path = strings.TrimPrefix(file.Path, files.RootPaths["default"])
|
||||
return renderJSON(w, r, file)
|
||||
@@ -28,6 +28,23 @@ func publicUserGetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(status), status)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func publicDlHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
file, ok := d.raw.(files.ExtendedFileInfo)
|
||||
if !ok {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type files.FileInfo")
|
||||
}
|
||||
if d.user == nil {
|
||||
return http.StatusUnauthorized, fmt.Errorf("failed to get user")
|
||||
}
|
||||
|
||||
if file.Type == "directory" {
|
||||
return rawFilesHandler(w, r, d, []string{file.Path})
|
||||
}
|
||||
|
||||
return rawFileHandler(w, r, file.FileInfo)
|
||||
}
|
||||
|
||||
// health godoc
|
||||
|
||||
+15
-51
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -14,7 +15,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
)
|
||||
|
||||
func setContentDisposition(w http.ResponseWriter, r *http.Request, fileName string) {
|
||||
@@ -45,25 +45,16 @@ func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int,
|
||||
if !d.user.Perm.Download {
|
||||
return http.StatusAccepted, nil
|
||||
}
|
||||
|
||||
filePrefix := ""
|
||||
file, ok := d.raw.(files.ExtendedFileInfo)
|
||||
if ok {
|
||||
filePrefix = file.Path
|
||||
}
|
||||
encodedFiles := r.URL.Query().Get("files")
|
||||
// Decode the URL-encoded path
|
||||
files, err := url.QueryUnescape(encodedFiles)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
fileList := strings.Split(files, ",")
|
||||
for i, f := range fileList {
|
||||
fileList[i] = filepath.Join(filePrefix, f)
|
||||
}
|
||||
return rawFilesHandler(w, r, d, fileList)
|
||||
return rawFilesHandler(w, r, d, strings.Split(files, ","))
|
||||
}
|
||||
func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *zip.Writer, flatten bool) error {
|
||||
|
||||
func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *zip.Writer) error {
|
||||
idx := files.GetIndex("default")
|
||||
realPath, _, _ := idx.GetRealPath(d.user.Scope, path)
|
||||
if !d.user.Check(realPath) {
|
||||
@@ -74,36 +65,16 @@ func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *z
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the base name of the top-level folder or file
|
||||
baseName := filepath.Base(realPath)
|
||||
|
||||
if info.IsDir() {
|
||||
// Walk through directory contents
|
||||
return filepath.Walk(realPath, func(filePath string, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Calculate the relative path
|
||||
relPath, err := filepath.Rel(realPath, filePath) // Use realPath directly
|
||||
relPath, err := filepath.Rel(filepath.Dir(realPath), filePath) // Relative path including base dir
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Normalize for tar: convert \ to /
|
||||
relPath = filepath.ToSlash(relPath)
|
||||
|
||||
// Skip adding `.` (current directory)
|
||||
if relPath == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prepend base folder name unless flatten is true
|
||||
if !flatten {
|
||||
relPath = filepath.Join(baseName, relPath)
|
||||
relPath = filepath.ToSlash(relPath) // Ensure normalized separators
|
||||
}
|
||||
|
||||
if fileInfo.IsDir() {
|
||||
if tarWriter != nil {
|
||||
header := &tar.Header{
|
||||
@@ -124,8 +95,8 @@ func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *z
|
||||
})
|
||||
}
|
||||
|
||||
// For a single file, use the base name as the archive path
|
||||
return addSingleFile(realPath, baseName, zipWriter, tarWriter)
|
||||
// Add a single file if it's not a directory
|
||||
return addSingleFile(realPath, filepath.Base(realPath), zipWriter, tarWriter)
|
||||
}
|
||||
|
||||
func addSingleFile(realPath, archivePath string, zipWriter *zip.Writer, tarWriter *tar.Writer) error {
|
||||
@@ -141,12 +112,11 @@ func addSingleFile(realPath, archivePath string, zipWriter *zip.Writer, tarWrite
|
||||
}
|
||||
|
||||
if tarWriter != nil {
|
||||
header, err := tar.FileInfoHeader(info, "")
|
||||
header, err := tar.FileInfoHeader(info, realPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Ensure correct relative path
|
||||
header.Name = filepath.ToSlash(archivePath)
|
||||
header.Name = archivePath
|
||||
if err = tarWriter.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -211,14 +181,12 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
default:
|
||||
return http.StatusInternalServerError, errors.New("format not implemented")
|
||||
}
|
||||
|
||||
baseDirName := filepath.Base(filepath.Dir(realPath))
|
||||
if baseDirName == "" || baseDirName == "/" {
|
||||
baseDirName = "download"
|
||||
}
|
||||
if len(fileList) == 1 && isDir {
|
||||
baseDirName = filepath.Base(realPath)
|
||||
}
|
||||
downloadFileName := url.PathEscape(baseDirName + extension)
|
||||
downloadFileName := url.PathEscape(baseDirName + "." + algo)
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+downloadFileName)
|
||||
// Create the archive and stream it directly to the response
|
||||
if extension == ".zip" {
|
||||
@@ -238,12 +206,10 @@ func createZip(w io.Writer, d *requestContext, filenames ...string) error {
|
||||
zipWriter := zip.NewWriter(w)
|
||||
defer zipWriter.Close()
|
||||
|
||||
// Check if we have exactly one directory
|
||||
//flatten := len(filenames) == 1
|
||||
for _, fname := range filenames {
|
||||
err := addFile(fname, d, nil, zipWriter, false)
|
||||
err := addFile(fname, d, nil, zipWriter)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to add %s to ZIP: %v", fname, err))
|
||||
log.Printf("Failed to add %s to ZIP: %v", fname, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,12 +223,10 @@ func createTarGz(w io.Writer, d *requestContext, filenames ...string) error {
|
||||
tarWriter := tar.NewWriter(gzWriter)
|
||||
defer tarWriter.Close()
|
||||
|
||||
// Check if we have exactly one directory
|
||||
//flatten := len(filenames) == 1
|
||||
for _, fname := range filenames {
|
||||
err := addFile(fname, d, tarWriter, nil, false)
|
||||
err := addFile(fname, d, tarWriter, nil)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to add %s to TAR.GZ: %v", fname, err))
|
||||
log.Printf("Failed to add %s to TAR.GZ: %v", fname, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -283,10 +283,10 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
return errToStatus(err), err
|
||||
}
|
||||
if !d.user.Check(src) || !d.user.Check(dst) {
|
||||
return http.StatusForbidden, fmt.Errorf("forbidden: user rules deny access to source or destination")
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
if dst == "/" || src == "/" {
|
||||
return http.StatusForbidden, fmt.Errorf("forbidden: source or destination is attempting to modify root")
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
idx := files.GetIndex(source)
|
||||
@@ -307,7 +307,7 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
}
|
||||
// Permission for overwriting the file
|
||||
if overwrite && !d.user.Perm.Modify {
|
||||
return http.StatusForbidden, fmt.Errorf("forbidden: user does not have permission to overwrite file")
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
err = d.RunHook(func() error {
|
||||
return patchAction(r.Context(), action, realSrc, realDest, d, fileCache, isSrcDir, source)
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/version"
|
||||
@@ -56,7 +56,7 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
|
||||
// Embedded mode: Serve files from the embedded assets
|
||||
assetFs, err = fs.Sub(assets, "embed")
|
||||
if err != nil {
|
||||
logger.Fatal("Could not embed frontend. Does dist exist?")
|
||||
log.Fatal("Could not embed frontend. Does dist exist?")
|
||||
}
|
||||
} else {
|
||||
assetFs = dirFS{Dir: http.Dir("http/dist")}
|
||||
@@ -107,7 +107,7 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
|
||||
|
||||
// Public routes
|
||||
api.HandleFunc("GET /public/publicUser", publicUserGetHandler)
|
||||
api.HandleFunc("GET /public/dl", withHashFile(rawHandler))
|
||||
api.HandleFunc("GET /public/dl", withHashFile(publicDlHandler))
|
||||
api.HandleFunc("GET /public/share", withHashFile(publicShareHandler))
|
||||
|
||||
// Settings routes
|
||||
@@ -143,7 +143,7 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
|
||||
// Load the TLS certificate and key
|
||||
cer, err := tls.LoadX509KeyPair(config.Server.TLSCert, config.Server.TLSKey)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("could not load certificate: %v", err))
|
||||
log.Fatalf("could not load certificate: %v", err)
|
||||
}
|
||||
|
||||
// Create a custom TLS listener
|
||||
@@ -158,17 +158,17 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
|
||||
// Listen on TCP and wrap with TLS
|
||||
listener, err := tls.Listen("tcp", fmt.Sprintf(":%v", config.Server.Port), tlsConfig)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("could not start TLS server: %v", err))
|
||||
log.Fatalf("could not start TLS server: %v", err)
|
||||
}
|
||||
if config.Server.Port != 443 {
|
||||
port = fmt.Sprintf(":%d", config.Server.Port)
|
||||
}
|
||||
// Build the full URL with host and port
|
||||
fullURL := fmt.Sprintf("%s://localhost%s%s", scheme, port, config.Server.BaseURL)
|
||||
logger.Info(fmt.Sprintf("Running at : %s", fullURL))
|
||||
log.Printf("Running at : %s", fullURL)
|
||||
err = http.Serve(listener, muxWithMiddleware(router))
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("could not start server: %v", err))
|
||||
log.Fatalf("could not start server: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Set HTTP scheme and the default port for HTTP
|
||||
@@ -178,10 +178,10 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
|
||||
}
|
||||
// Build the full URL with host and port
|
||||
fullURL := fmt.Sprintf("%s://localhost%s%s", scheme, port, config.Server.BaseURL)
|
||||
logger.Info(fmt.Sprintf("Running at : %s", fullURL))
|
||||
log.Printf("Running at : %s", fullURL)
|
||||
err := http.ListenAndServe(fmt.Sprintf(":%v", config.Server.Port), muxWithMiddleware(router))
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("could not start server: %v", err))
|
||||
log.Fatalf("could not start server: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -11,7 +12,6 @@ import (
|
||||
"text/template"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/version"
|
||||
)
|
||||
@@ -63,7 +63,6 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
|
||||
"EnableExec": config.Server.EnableExec,
|
||||
"ReCaptchaHost": config.Auth.Recaptcha.Host,
|
||||
"ExternalLinks": config.Frontend.ExternalLinks,
|
||||
"ExternalUrl": strings.TrimSuffix(config.Server.ExternalUrl, "/"),
|
||||
}
|
||||
|
||||
if config.Frontend.Files != "" {
|
||||
@@ -71,7 +70,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
|
||||
_, err := os.Stat(fPath) //nolint:govet
|
||||
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
logger.Error(fmt.Sprintf("couldn't load custom styles: %v", err))
|
||||
log.Printf("couldn't load custom styles: %v", err)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
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
|
||||
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
|
||||
}
|
||||
flags := log.Ldate | log.Ltime
|
||||
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),
|
||||
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 "
|
||||
}
|
||||
// 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 "
|
||||
}
|
||||
// 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
|
||||
})
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
)
|
||||
|
||||
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",
|
||||
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,
|
||||
"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
|
||||
if prefix {
|
||||
writeOut = fmt.Sprintf("[%s] ", level) + writeOut
|
||||
}
|
||||
if logger.colors && color != "" {
|
||||
writeOut = color + 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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
## TEST file used by docker testing containers
|
||||
checkExit() {
|
||||
if [ "$?" -ne 0 ];then
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
if command -v go &> /dev/null
|
||||
then
|
||||
printf "\n == Running tests == \n"
|
||||
go test -race -parallel -v ./...
|
||||
checkExit
|
||||
else
|
||||
echo "ERROR: unable to perform tests"
|
||||
exit 1
|
||||
fi
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
if ! [ -x "$(command -v standard-version)" ]; then
|
||||
echo "standard-version is not installed. please run 'npm i -g standard-version'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
standard-version --dry-run --skip
|
||||
read -p "Continue (y/n)? " -n 1 -r
|
||||
echo ;
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
standard-version -s ;
|
||||
fi
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
if ! [ -x "$(command -v commitlint)" ]; then
|
||||
echo "commitlint is not installed. please run 'npm i -g commitlint'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for commit_hash in $(git log --pretty=format:%H origin/master..HEAD); do
|
||||
commitlint -f ${commit_hash}~1 -t ${commit_hash}
|
||||
done
|
||||
+12
-32
@@ -8,7 +8,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/version"
|
||||
)
|
||||
@@ -20,25 +19,25 @@ func Initialize(configFile string) {
|
||||
Config = setDefaults()
|
||||
err := yaml.Unmarshal(yamlData, &Config)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error unmarshaling YAML data: %v", err))
|
||||
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
||||
}
|
||||
Config.UserDefaults.Perm = Config.UserDefaults.Permissions
|
||||
// Convert relative path to absolute path
|
||||
if len(Config.Server.Sources) > 0 {
|
||||
// TODO allow multipe sources not named default
|
||||
for _, source := range Config.Server.Sources {
|
||||
realPath, err2 := filepath.Abs(source.Path)
|
||||
if err2 != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error getting source path: %v", err2))
|
||||
realPath, err := filepath.Abs(source.Path)
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting source path: %v", err)
|
||||
}
|
||||
source.Path = realPath
|
||||
source.Name = "default" // Modify the local copy of the map value
|
||||
Config.Server.Sources["default"] = source // Assign the modified value back to the map
|
||||
}
|
||||
} else {
|
||||
realPath, err2 := filepath.Abs(Config.Server.Root)
|
||||
if err2 != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error getting source path: %v", err2))
|
||||
realPath, err := filepath.Abs(Config.Server.Root)
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting source path: %v", err)
|
||||
}
|
||||
Config.Server.Sources = map[string]Source{
|
||||
"default": {
|
||||
@@ -68,43 +67,26 @@ func Initialize(configFile string) {
|
||||
Url: "https://github.com/gtsteffaniak/filebrowser/wiki",
|
||||
})
|
||||
}
|
||||
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 loadConfigFile(configFile string) []byte {
|
||||
// Open and read the YAML file
|
||||
yamlFile, err := os.Open(configFile)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("%v", err))
|
||||
log.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer yamlFile.Close()
|
||||
|
||||
stat, err := yamlFile.Stat()
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("error getting file information: %s", err.Error()))
|
||||
log.Fatalf("error getting file information: %s", err.Error())
|
||||
}
|
||||
|
||||
yamlData := make([]byte, stat.Size())
|
||||
_, err = yamlFile.Read(yamlData)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error reading YAML data: %v", err))
|
||||
log.Fatalf("Error reading YAML data: %v", err)
|
||||
}
|
||||
return yamlData
|
||||
}
|
||||
@@ -119,6 +101,7 @@ func setDefaults() Settings {
|
||||
NumImageProcessors: 4,
|
||||
BaseURL: "",
|
||||
Database: "database.db",
|
||||
Log: "stdout",
|
||||
Root: ".",
|
||||
},
|
||||
Auth: Auth{
|
||||
@@ -131,9 +114,6 @@ func setDefaults() Settings {
|
||||
Host: "",
|
||||
},
|
||||
},
|
||||
Frontend: Frontend{
|
||||
Name: "FileBrowser Quantum",
|
||||
},
|
||||
UserDefaults: UserDefaults{
|
||||
StickySidebar: true,
|
||||
Scope: ".",
|
||||
|
||||
@@ -3,13 +3,12 @@ package settings
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -23,7 +22,7 @@ func (s *Settings) MakeUserDir(username, userScope, serverRoot string) (string,
|
||||
if userScope == "" && s.Server.CreateUserDir {
|
||||
username = cleanUsername(username)
|
||||
if username == "" || username == "-" || username == "." {
|
||||
logger.Error(fmt.Sprintf("create user: invalid user for home dir creation: [%s]", username))
|
||||
log.Printf("create user: invalid user for home dir creation: [%s]", username)
|
||||
return "", errors.New("invalid user for home dir creation")
|
||||
}
|
||||
userScope = path.Join(s.Server.UserHomeBasePath, username)
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
)
|
||||
|
||||
func TestConfigLoadChanged(t *testing.T) {
|
||||
@@ -17,7 +16,7 @@ func TestConfigLoadChanged(t *testing.T) {
|
||||
|
||||
err := yaml.Unmarshal(yamlData, &newConfig)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error unmarshaling YAML data: %v", err))
|
||||
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
||||
}
|
||||
// Use go-cmp to compare the two structs
|
||||
if diff := cmp.Diff(newConfig, Config); diff == "" {
|
||||
@@ -33,7 +32,7 @@ func TestConfigLoadSpecificValues(t *testing.T) {
|
||||
|
||||
err := yaml.Unmarshal(yamlData, &newConfig)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error unmarshaling YAML data: %v", err))
|
||||
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
||||
}
|
||||
testCases := []struct {
|
||||
fieldName string
|
||||
|
||||
@@ -47,21 +47,12 @@ type Server struct {
|
||||
Port int `json:"port"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
Address string `json:"address"`
|
||||
Logging []LogConfig `json:"logging"`
|
||||
Log string `json:"log"`
|
||||
Database string `json:"database"`
|
||||
Root string `json:"root"`
|
||||
UserHomeBasePath string `json:"userHomeBasePath"`
|
||||
CreateUserDir bool `json:"createUserDir"`
|
||||
Sources map[string]Source `json:"sources"`
|
||||
ExternalUrl string `json:"externalUrl"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Levels string `json:"levels"`
|
||||
ApiLevels string `json:"apiLevels"`
|
||||
Output string `json:"output"`
|
||||
NoColors bool `json:"noColors"`
|
||||
Json bool `json:"json"`
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
|
||||
@@ -10,6 +10,7 @@ server:
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
address: ""
|
||||
log: "stdout"
|
||||
database: "mydb.db"
|
||||
root: "/srv"
|
||||
auth:
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/asdine/storm/v3/q"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/share"
|
||||
)
|
||||
|
||||
@@ -69,7 +68,7 @@ func (s shareBackend) Gets(path string, id uint) ([]*share.Link, error) {
|
||||
if v[i].Expire < time.Now().Unix() {
|
||||
err = s.Delete(v[i].PasswordHash)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("expired share could not be deleted: %v", err.Error()))
|
||||
log.Println("expired share could not be deleted: ", err.Error())
|
||||
}
|
||||
} else {
|
||||
filteredList = append(filteredList, v[i])
|
||||
|
||||
@@ -2,6 +2,7 @@ package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@@ -9,7 +10,6 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage/bolt"
|
||||
@@ -118,15 +118,15 @@ func CreateUser(userInfo users.User, asAdmin bool) error {
|
||||
// create new home directory
|
||||
userHome, err := settings.Config.MakeUserDir(newUser.Username, newUser.Scope, files.RootPaths["default"])
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("create user: failed to mkdir user home dir: [%s]", userHome))
|
||||
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
||||
return err
|
||||
}
|
||||
newUser.Scope = userHome
|
||||
logger.Debug(fmt.Sprintf("user: %s, home dir: [%s].", newUser.Username, userHome))
|
||||
log.Printf("user: %s, home dir: [%s].", newUser.Username, userHome)
|
||||
idx := files.GetIndex("default")
|
||||
_, _, err = idx.GetRealPath(newUser.Scope)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("user path is not valid: %v", newUser.Scope))
|
||||
log.Println("user path is not valid", newUser.Scope)
|
||||
return nil
|
||||
}
|
||||
err = store.Users.Save(&newUser)
|
||||
|
||||
Regular → Executable
File renamed without changes.
Regular → Executable
File renamed without changes.
Regular → Executable
File renamed without changes.
@@ -3,17 +3,16 @@ package utils
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"log"
|
||||
math "math/rand"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
)
|
||||
|
||||
func CheckErr(source string, err error) {
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("%s: %v", source, err))
|
||||
log.Fatalf("%s: %v", source, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +49,7 @@ func PrintStructFields(v interface{}) {
|
||||
|
||||
// Ensure the input is a struct
|
||||
if val.Kind() != reflect.Struct {
|
||||
logger.Debug("Provided value is not a struct")
|
||||
fmt.Println("Provided value is not a struct")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -67,7 +66,7 @@ func PrintStructFields(v interface{}) {
|
||||
fieldValue = fieldValue[:100] + "..."
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("Field: %s, %s\n", fieldType.Name, fieldValue))
|
||||
fmt.Printf("Field: %s, %s\n", fieldType.Name, fieldValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Browser, firefox, expect, Page } from "@playwright/test";
|
||||
|
||||
|
||||
async function globalSetup() {
|
||||
const browser: Browser = await firefox.launch();
|
||||
const context = await browser.newContext();
|
||||
const page: Page = await context.newPage();
|
||||
await page.goto("http://127.0.0.1/login");
|
||||
await page.getByPlaceholder("Username").fill("admin");
|
||||
await page.getByPlaceholder("Password").fill("admin");
|
||||
await page.getByRole("button", { name: "Login" }).click();
|
||||
await page.waitForURL("**/files/", { timeout: 100 });
|
||||
await expect(page).toHaveTitle('FileBrowser Quantum - Files');
|
||||
let cookies = await context.cookies();
|
||||
expect(cookies.find((c) => c.name == "auth")?.value).toBeDefined();
|
||||
await page.context().storageState({ path: "./loginAuth.json" });
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
export default globalSetup
|
||||
@@ -17,10 +17,10 @@
|
||||
"lint": "eslint --ext .js,.vue,ts src",
|
||||
"lint:fix": "eslint --fix src/",
|
||||
"format": "prettier --write .",
|
||||
"integration-test": "npx playwright test",
|
||||
"test": "vitest run "
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": "^1.49.1",
|
||||
"ace-builds": "^1.24.2",
|
||||
"clipboard": "^2.0.4",
|
||||
"css-vars-ponyfill": "^2.4.3",
|
||||
@@ -34,6 +34,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"eslint": "^8.57.0",
|
||||
|
||||
@@ -10,8 +10,6 @@ import { defineConfig, devices } from "@playwright/test";
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
globalSetup: "./global-setup",
|
||||
timeout: 3000,
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
@@ -20,13 +18,11 @@ export default defineConfig({
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : 1, // required for now! todo parallel some tests
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "line",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
actionTimeout: 500,
|
||||
storageState: "loginAuth.json",
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1",
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<script>
|
||||
window.FileBrowser = JSON.parse('{{ .globalVars }}');
|
||||
var dynamicManifest = {
|
||||
"name": window.FileBrowser.Name || 'FileBrowser Quantum',
|
||||
"name": window.FileBrowser.Name || '',
|
||||
"icons": [
|
||||
{
|
||||
"src": window.location.origin + "{{ .StaticURL }}/img/icons/android-chrome-256x256.png",
|
||||
|
||||
+10
-18
@@ -125,38 +125,30 @@ export async function post(url, content = "", overwrite = false, onupload) {
|
||||
}
|
||||
|
||||
export async function moveCopy(items, action = "copy", overwrite = false, rename = false) {
|
||||
let promises = [];
|
||||
let params = {
|
||||
overwrite: overwrite,
|
||||
action: action,
|
||||
rename: rename,
|
||||
};
|
||||
}
|
||||
try {
|
||||
// Create an array of fetch calls
|
||||
let promises = items.map((item) => {
|
||||
for (let item of items) {
|
||||
let toPath = encodeURIComponent(removePrefix(decodeURI(item.to), "files"));
|
||||
let fromPath = encodeURIComponent(removePrefix(decodeURI(item.from), "files"));
|
||||
let localParams = { ...params, destination: toPath, from: fromPath };
|
||||
let localParams = { ...params };
|
||||
localParams.destination = toPath;
|
||||
localParams.from = fromPath;
|
||||
const apiPath = getApiPath("api/resources", localParams);
|
||||
return fetch(apiPath, { method: "PATCH" }).then((response) => {
|
||||
if (!response.ok) {
|
||||
// Throw an error if the fetch fails
|
||||
return response.text().then((text) => {
|
||||
throw new Error(`Failed to move/copy: ${text || response.statusText}`);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
});
|
||||
});
|
||||
promises.push(fetch(apiPath, { method: "PATCH" }));
|
||||
}
|
||||
return promises;
|
||||
|
||||
// Await all promises and ensure errors propagate
|
||||
await Promise.all(promises);
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error moving/copying resources");
|
||||
throw err; // Re-throw the error to propagate it back to the caller
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function checksum(url, algo) {
|
||||
try {
|
||||
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { fetchURL, fetchJSON, adjustedData } from "./utils";
|
||||
import { notify } from "@/notify";
|
||||
import { getApiPath } from "@/utils/url.js";
|
||||
import { externalUrl } from "@/utils/constants";
|
||||
|
||||
export async function list() {
|
||||
const apiPath = getApiPath("api/shares");
|
||||
@@ -42,8 +41,5 @@ export async function create(path, password = "", expires = "", unit = "hours")
|
||||
}
|
||||
|
||||
export function getShareURL(share) {
|
||||
if (externalUrl) {
|
||||
return externalUrl+getApiPath(`share/${share.hash}`);
|
||||
}
|
||||
return window.origin+getApiPath(`share/${share.hash}`);
|
||||
}
|
||||
@@ -67,7 +67,8 @@ export function adjustedData(data, url) {
|
||||
// Combine folders and files into items
|
||||
data.items = [...(data.folders || []), ...(data.files || [])];
|
||||
|
||||
data.items = data.items.map((item) => {
|
||||
data.items = data.items.map((item, index) => {
|
||||
item.index = index;
|
||||
item.url = `${data.url}${item.name}`;
|
||||
if (item.type === "directory") {
|
||||
item.url += "/";
|
||||
|
||||
@@ -23,10 +23,10 @@ describe('adjustedData', () => {
|
||||
folders: [],
|
||||
files: [],
|
||||
items: [
|
||||
{ name: "folder1", type: "directory", url: "http://example.com/unit-testing/files/path/to/directory/folder1/" },
|
||||
{ name: "folder2", type: "directory", url: "http://example.com/unit-testing/files/path/to/directory/folder2/" },
|
||||
{ name: "file1.txt", type: "file", url: "http://example.com/unit-testing/files/path/to/directory/file1.txt" },
|
||||
{ name: "file2.txt", type: "file", url: "http://example.com/unit-testing/files/path/to/directory/file2.txt" },
|
||||
{ name: "folder1", type: "directory", index: 0, url: "http://example.com/unit-testing/files/path/to/directory/folder1/" },
|
||||
{ name: "folder2", type: "directory", index: 1, url: "http://example.com/unit-testing/files/path/to/directory/folder2/" },
|
||||
{ name: "file1.txt", type: "file", index: 2, url: "http://example.com/unit-testing/files/path/to/directory/file1.txt" },
|
||||
{ name: "file2.txt", type: "file", index: 3, url: "http://example.com/unit-testing/files/path/to/directory/file2.txt" },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ export default {
|
||||
return state.user;
|
||||
},
|
||||
centered() {
|
||||
return getters.isMobile() || !this.posX || !this.posY;
|
||||
return getters.isMobile() || ( !this.posX || !this.posY );
|
||||
},
|
||||
showContext() {
|
||||
if (getters.currentPromptName() == "ContextMenu" && state.prompts != []) {
|
||||
|
||||
@@ -179,7 +179,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
handleTouchMove(event) {
|
||||
if (!state.isSafari) return;
|
||||
if (!state.isSafari) return
|
||||
const touch = event.touches[0];
|
||||
const deltaX = Math.abs(touch.clientX - this.touchStartX);
|
||||
const deltaY = Math.abs(touch.clientY - this.touchStartY);
|
||||
@@ -191,7 +191,7 @@ export default {
|
||||
}
|
||||
},
|
||||
handleTouchEnd() {
|
||||
if (!state.isSafari) return;
|
||||
if (!state.isSafari) return
|
||||
this.cancelContext(); // Clear timeout
|
||||
this.isSwipe = false; // Reset swipe state
|
||||
},
|
||||
@@ -214,8 +214,8 @@ export default {
|
||||
},
|
||||
onRightClick(event) {
|
||||
event.preventDefault(); // Prevent default context menu
|
||||
// If one or fewer items are selected, reset the selection
|
||||
if (!state.multiple && getters.selectedCount() < 2) {
|
||||
// If no items are selected, select the right-clicked item
|
||||
if (!state.multiple) {
|
||||
mutations.resetSelected();
|
||||
mutations.addSelected(this.index);
|
||||
}
|
||||
@@ -247,9 +247,7 @@ export default {
|
||||
getTime() {
|
||||
if (state.user.dateFormat) {
|
||||
// Truncate the fractional seconds to 3 digits (milliseconds)
|
||||
const sanitizedString = this.modified.replace(/\.\d+/, (match) =>
|
||||
match.slice(0, 4)
|
||||
);
|
||||
const sanitizedString = this.modified.replace(/\.\d+/, (match) => match.slice(0, 4));
|
||||
// Parse the sanitized string into a Date object
|
||||
const date = new Date(sanitizedString);
|
||||
return date.toLocaleString();
|
||||
@@ -335,7 +333,7 @@ export default {
|
||||
action(overwrite, rename);
|
||||
},
|
||||
addSelected(event) {
|
||||
if (!state.isSafari) return;
|
||||
if (!state.isSafari) return
|
||||
const touch = event.touches[0];
|
||||
this.touchStartX = touch.clientX;
|
||||
this.touchStartY = touch.clientY;
|
||||
@@ -359,11 +357,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!state.user.singleClick &&
|
||||
getters.selectedCount() !== 0 &&
|
||||
event.button === 0
|
||||
) {
|
||||
if (!state.user.singleClick && getters.selectedCount() !== 0 && event.button === 0) {
|
||||
event.preventDefault();
|
||||
}
|
||||
setTimeout(() => {
|
||||
@@ -399,12 +393,7 @@ export default {
|
||||
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!state.user.singleClick &&
|
||||
!event.ctrlKey &&
|
||||
!event.metaKey &&
|
||||
!state.multiple
|
||||
) {
|
||||
if (!state.user.singleClick && !event.ctrlKey && !event.metaKey && !state.multiple) {
|
||||
mutations.resetSelected();
|
||||
}
|
||||
mutations.addSelected(this.index);
|
||||
@@ -422,4 +411,4 @@ export default {
|
||||
.item {
|
||||
-webkit-touch-callout: none; /* Disable the default long press preview */
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -52,8 +52,7 @@ import FileList from "./FileList.vue";
|
||||
import { filesApi } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
import { removePrefix } from "@/utils/url";
|
||||
import { notify } from "@/notify";
|
||||
//import { notify } from "@/notify";
|
||||
|
||||
export default {
|
||||
name: "copy",
|
||||
@@ -62,7 +61,6 @@ export default {
|
||||
return {
|
||||
current: window.location.pathname,
|
||||
dest: null,
|
||||
items: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -73,63 +71,57 @@ export default {
|
||||
return mutations.closeHovers();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
for (let item of state.selected) {
|
||||
this.items.push({
|
||||
from: state.req.items[item].url,
|
||||
// add to: dest
|
||||
name: state.req.items[item].name,
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copy: async function (event) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
// Define the action function
|
||||
let action = async (overwrite, rename) => {
|
||||
const loc = removePrefix(this.dest, "files");
|
||||
for (let item of this.items) {
|
||||
item.to = loc + "/" + item.name;
|
||||
}
|
||||
buttons.loading("copy");
|
||||
await filesApi.moveCopy(this.items, "copy", overwrite, rename);
|
||||
};
|
||||
// Fetch destination files
|
||||
let dstResp = await filesApi.fetchFiles(this.dest);
|
||||
let conflict = upload.checkConflict(this.items, dstResp.items);
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
let items = [];
|
||||
|
||||
if (conflict) {
|
||||
await new Promise((resolve, reject) => {
|
||||
mutations.showHover({
|
||||
name: "replace-rename",
|
||||
confirm: async (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
event.preventDefault();
|
||||
try {
|
||||
await action(overwrite, rename);
|
||||
resolve(); // Resolve the promise if action succeeds
|
||||
} catch (e) {
|
||||
reject(e); // Reject the promise if an error occurs
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Await the action call for non-conflicting cases
|
||||
await action(overwrite, rename);
|
||||
}
|
||||
mutations.closeHovers();
|
||||
notify.showSuccess("Successfully copied file/folder, redirecting...");
|
||||
setTimeout(() => {
|
||||
this.$router.push(this.dest);
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
notify.error(error);
|
||||
// Create a new promise for each file.
|
||||
for (let item of state.selected) {
|
||||
items.push({
|
||||
from: state.req.items[item].url,
|
||||
to: this.dest + encodeURIComponent(state.req.items[item].name),
|
||||
name: state.req.items[item].name,
|
||||
});
|
||||
}
|
||||
|
||||
let action = async (overwrite, rename) => {
|
||||
buttons.loading("copy");
|
||||
await filesApi.moveCopy(items, "copy", overwrite, rename);
|
||||
this.$router.push({ path: this.dest });
|
||||
mutations.setReload(true);
|
||||
};
|
||||
|
||||
if (state.route.path === this.dest) {
|
||||
mutations.closeHovers();
|
||||
action(false, true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let dstItems = (await filesApi.fetchFiles(this.dest)).items;
|
||||
let conflict = upload.checkConflict(items, dstItems);
|
||||
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
if (conflict) {
|
||||
mutations.showHover({
|
||||
name: "replace-rename",
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
|
||||
event.preventDefault();
|
||||
mutations.closeHovers();
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
action(overwrite, rename);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
<script>
|
||||
import { state, mutations } from "@/store";
|
||||
import url from "@/utils/url.js";
|
||||
import { removePrefix } from "@/utils/url.js";
|
||||
import { filesApi } from "@/api";
|
||||
|
||||
export default {
|
||||
@@ -41,7 +40,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
nav() {
|
||||
return removePrefix(decodeURIComponent(this.current), "files");
|
||||
return decodeURIComponent(this.current);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -53,7 +53,6 @@ import { filesApi } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
import { notify } from "@/notify";
|
||||
import { removePrefix } from "@/utils/url";
|
||||
|
||||
export default {
|
||||
name: "move",
|
||||
@@ -62,7 +61,6 @@ export default {
|
||||
return {
|
||||
current: window.location.pathname,
|
||||
dest: null,
|
||||
items: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -73,66 +71,50 @@ export default {
|
||||
return mutations.closeHovers();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
for (let item of state.selected) {
|
||||
this.items.push({
|
||||
from: state.req.items[item].url,
|
||||
// add to: dest
|
||||
name: state.req.items[item].name,
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
move: async function (event) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
// Define the action function
|
||||
let action = async (overwrite, rename) => {
|
||||
const loc = removePrefix(this.dest, "files");
|
||||
for (let item of this.items) {
|
||||
item.to = loc + "/" + item.name;
|
||||
}
|
||||
buttons.loading("move");
|
||||
await filesApi.moveCopy(this.items, "move", overwrite, rename);
|
||||
};
|
||||
let items = [];
|
||||
|
||||
// Fetch destination files
|
||||
let dstResp = await filesApi.fetchFiles(this.dest);
|
||||
let conflict = upload.checkConflict(this.items, dstResp.items);
|
||||
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
if (conflict) {
|
||||
await new Promise((resolve, reject) => {
|
||||
mutations.showHover({
|
||||
name: "replace-rename",
|
||||
confirm: async (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
event.preventDefault();
|
||||
try {
|
||||
await action(overwrite, rename);
|
||||
resolve(); // Resolve the promise if action succeeds
|
||||
} catch (e) {
|
||||
reject(e); // Reject the promise if an error occurs
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Await the action call for non-conflicting cases
|
||||
await action(overwrite, rename);
|
||||
}
|
||||
for (let item of state.selected) {
|
||||
items.push({
|
||||
from: state.req.items[item].url,
|
||||
to: this.dest + state.req.items[item].name,
|
||||
name: state.req.items[item].name,
|
||||
});
|
||||
}
|
||||
let action = async (overwrite, rename) => {
|
||||
buttons.loading("move");
|
||||
await filesApi.moveCopy(items, "move", overwrite, rename);
|
||||
buttons.success("move");
|
||||
this.$router.push({ path: this.dest });
|
||||
mutations.closeHovers();
|
||||
notify.showSuccess("Successfully moved file/folder, redirecting...");
|
||||
setTimeout(() => {
|
||||
this.$router.push(this.dest);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
let dstItems = (await filesApi.fetchFiles(this.dest)).items;
|
||||
let conflict = upload.checkConflict(items, dstItems);
|
||||
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
try {
|
||||
if (conflict) {
|
||||
mutations.showHover({
|
||||
name: "replace-rename",
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
event.preventDefault();
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
action(overwrite, rename);
|
||||
} catch (e) {
|
||||
// Catch any errors from action or other parts of the flow
|
||||
notify.showError(e);
|
||||
}
|
||||
return;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -93,13 +93,14 @@ export default {
|
||||
this.$router.push({ path: uri });
|
||||
} else if (!this.base) {
|
||||
const res = await filesApi.fetchFiles(url.removeLastDir(uri) + "/");
|
||||
mutations.replaceRequest(res);
|
||||
mutations.updateRequest(res);
|
||||
}
|
||||
|
||||
mutations.closeHovers();
|
||||
} catch (error) {
|
||||
notify.showError(error);
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -206,12 +206,7 @@ export default {
|
||||
if (isPermanent) {
|
||||
res = await shareApi.create(this.subpath, this.password);
|
||||
} else {
|
||||
res = await shareApi.create(
|
||||
this.subpath,
|
||||
this.password,
|
||||
this.time.toString(),
|
||||
this.unit
|
||||
);
|
||||
res = await shareApi.create(this.subpath, this.password, this.time.toString(), this.unit);
|
||||
}
|
||||
|
||||
this.links.push(res);
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<i class="material-icons">settings</i>
|
||||
</div>
|
||||
|
||||
<div class="inner-card logout-button" @click="logout" >
|
||||
<i v-if="canLogout" class="material-icons">exit_to_app</i>
|
||||
<div class="inner-card">
|
||||
<i v-if="canLogout" @click="logout" class="material-icons">exit_to_app</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -59,10 +59,10 @@ body.rtl #listingView {
|
||||
|
||||
#listingView .item i {
|
||||
font-size: 4em;
|
||||
margin-right: 0.1em;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
|
||||
#listingView .item img {
|
||||
width: 4em;
|
||||
height: 4em;
|
||||
@@ -177,6 +177,11 @@ body.rtl #listingView {
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
#listingView.gallery .size,
|
||||
#listingView.gallery .modified {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#listingView.compact {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
@@ -215,7 +220,15 @@ body.rtl #listingView {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#listingView .header .name {
|
||||
#listingView.compact .header .name,
|
||||
#listingView.list .header .name,
|
||||
#listingView.list .item .name,
|
||||
#listingView.compact .item .name {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#listingView.compact .header .name,
|
||||
#listingView.list .header .name {
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
|
||||
@@ -224,23 +237,14 @@ body.rtl #listingView {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#listingView .header .size,
|
||||
#listingView.compact .header .size,
|
||||
#listingView.list .header .size,
|
||||
#listingView.list .item .size,
|
||||
#listingView.compact .item .size {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
#listingView .header .name,
|
||||
#listingView.list .item .name,
|
||||
#listingView.compact .item .name {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#listingView.normal .item .text {
|
||||
padding-left: 0.3em;
|
||||
}
|
||||
|
||||
#listingView .header i {
|
||||
#listingView.compact .header i {
|
||||
font-size: 1.5em;
|
||||
vertical-align: middle;
|
||||
margin-left: .2em;
|
||||
@@ -271,21 +275,25 @@ body.rtl #listingView {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
#listingView.compact .name {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#listingView.compact .header span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#listingView .header i {
|
||||
#listingView.compact .header i {
|
||||
opacity: 0;
|
||||
transition: .1s ease all;
|
||||
}
|
||||
|
||||
#listingView .header p:hover i,
|
||||
#listingView .header .active i {
|
||||
#listingView.compact .header p:hover i,
|
||||
#listingView.compact .header .active i {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#listingView .header .active {
|
||||
#listingView.compact .header .active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -304,28 +312,11 @@ body.rtl #listingView {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
#listingView.compact .item[aria-selected=true],
|
||||
#listingView.normal .item[aria-selected=true],
|
||||
#listingView.list .item[aria-selected=true] {
|
||||
#listingView .item[aria-selected=true] {
|
||||
background: var(--primaryColor) !important;
|
||||
color: var(--item-selected) !important;
|
||||
}
|
||||
|
||||
#listingView.gallery .item[aria-selected=true] {
|
||||
border: 1em solid var(--primaryColor) !important;
|
||||
border-radius: 1em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#listingView.gallery .item[aria-selected=true] .text {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#listingView.gallery .item .size,
|
||||
#listingView.gallery .item .modified {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#listingView.list .item div:first-of-type {
|
||||
width: 3em;
|
||||
}
|
||||
@@ -346,18 +337,13 @@ body.rtl #listingView {
|
||||
}
|
||||
|
||||
#listingView .header {
|
||||
display: flex !important;
|
||||
display: none;
|
||||
background: white;
|
||||
border-radius: 1em;
|
||||
border: 1px solid rgba(0, 0, 0, .1);
|
||||
z-index: 999;
|
||||
padding: .85em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#listingView.list .header i,
|
||||
#listingView.normal .header i,
|
||||
#listingView.gallary .header i {
|
||||
#listingView.list .header i {
|
||||
font-size: 1.5em;
|
||||
vertical-align: middle;
|
||||
margin-left: .2em;
|
||||
@@ -365,8 +351,12 @@ body.rtl #listingView {
|
||||
|
||||
#listingView.compact .header,
|
||||
#listingView.list .header {
|
||||
display: flex !important;
|
||||
border-top-left-radius: 1em;
|
||||
border-top-right-radius: 1em;
|
||||
z-index: 999;
|
||||
padding: .85em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#listingView.list .item:first-child {
|
||||
@@ -393,9 +383,7 @@ body.rtl #listingView {
|
||||
display:flex;
|
||||
width: 100%;
|
||||
}
|
||||
#listingView.list .header,
|
||||
#listingView.normal .header,
|
||||
#listingView.gallery .header {
|
||||
#listingView.list .header {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
@@ -403,11 +391,11 @@ body.rtl #listingView {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#listingView .name {
|
||||
#listingView.list .name {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#listingView .header span {
|
||||
#listingView.list .header span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
@@ -130,20 +130,19 @@ router.beforeResolve(async (to, from, next) => {
|
||||
return next(false);
|
||||
}
|
||||
|
||||
if (state != null && state.user != null && !('username' in state.user)) {
|
||||
await validateLogin();
|
||||
}
|
||||
|
||||
// Set the page title using i18n
|
||||
const title = i18n.global.t(titles[to.name as keyof typeof titles]);
|
||||
document.title = name + " - " + title ;
|
||||
document.title = title + " - " + name;
|
||||
|
||||
// Update store with the current route
|
||||
mutations.setRoute(to);
|
||||
|
||||
// Handle auth requirements
|
||||
if (to.matched.some((record) => record.meta.requiresAuth)) {
|
||||
|
||||
if (state != null && state.user != null && !('username' in state.user)) {
|
||||
await validateLogin();
|
||||
}
|
||||
|
||||
if (!getters.isLoggedIn()) {
|
||||
next({
|
||||
path: "/login",
|
||||
|
||||
@@ -53,6 +53,10 @@ export const getters = {
|
||||
state.req.items.forEach((item) => {
|
||||
// Check if the item is a directory
|
||||
if (item.type == "directory") {
|
||||
// If hideDotfiles is enabled and the item is a dotfile, skip it
|
||||
if (state.user.hideDotfiles && item.name.startsWith(".")) {
|
||||
return;
|
||||
}
|
||||
// Otherwise, count this directory
|
||||
dirCount++;
|
||||
}
|
||||
@@ -65,6 +69,10 @@ export const getters = {
|
||||
state.req.items.forEach((item) => {
|
||||
// Check if the item is a directory
|
||||
if (item.type != "directory") {
|
||||
// If hideDotfiles is enabled and the item is a dotfile, skip it
|
||||
if (state.user.hideDotfiles && item.name.startsWith(".")) {
|
||||
return;
|
||||
}
|
||||
// Otherwise, count this directory
|
||||
fileCount++;
|
||||
}
|
||||
@@ -80,6 +88,9 @@ export const getters = {
|
||||
const files = [];
|
||||
|
||||
state.req.items.forEach((item) => {
|
||||
if (state.user.hideDotfiles && item.name.startsWith(".")) {
|
||||
return;
|
||||
}
|
||||
if (item.type == "directory") {
|
||||
dirs.push(item);
|
||||
} else {
|
||||
|
||||
@@ -140,21 +140,16 @@ export const mutations = {
|
||||
emitStateChanged();
|
||||
},
|
||||
addSelected: (value) => {
|
||||
console.log("addSelected", value)
|
||||
state.selected.push(value);
|
||||
emitStateChanged();
|
||||
},
|
||||
removeSelected: (value) => {
|
||||
console.log("removeSelected", value)
|
||||
|
||||
let i = state.selected.indexOf(value);
|
||||
if (i === -1) return;
|
||||
state.selected.splice(i, 1);
|
||||
emitStateChanged();
|
||||
},
|
||||
resetSelected: () => {
|
||||
console.log("resetSelected")
|
||||
|
||||
state.selected = [];
|
||||
mutations.setMultiple(false);
|
||||
emitStateChanged();
|
||||
@@ -201,20 +196,17 @@ export const mutations = {
|
||||
// Emit state change event
|
||||
emitStateChanged();
|
||||
},
|
||||
replaceRequest: (value) => {
|
||||
updateRequest: (value) => {
|
||||
const selectedItems = state.selected.map((i) => state.req.items[i]);
|
||||
state.oldReq = state.req;
|
||||
state.req = value;
|
||||
state.selected = [];
|
||||
if (!value?.items) {
|
||||
state.req = value;
|
||||
emitStateChanged();
|
||||
return
|
||||
}
|
||||
if (state.user.hideDotfiles) {
|
||||
value.items = value.items.filter((item) => !item.name.startsWith("."));
|
||||
}
|
||||
value.items.map((item, index) => {
|
||||
item.index = index;
|
||||
return item;
|
||||
})
|
||||
if (!state.req?.items) return;
|
||||
state.selected = state.req.items
|
||||
.filter((item) => selectedItems.some((rItem) => rItem.url === item.url))
|
||||
.map((item) => item.index);
|
||||
},
|
||||
replaceRequest: (value) => {
|
||||
state.req = value;
|
||||
emitStateChanged();
|
||||
},
|
||||
@@ -228,8 +220,7 @@ export const mutations = {
|
||||
emitStateChanged();
|
||||
},
|
||||
updateListingItems: () => {
|
||||
state.req.items = sortedItems(state.req.items, state.user.sorting.by)
|
||||
mutations.replaceRequest(state.req);
|
||||
state.req.items = sortedItems(state.req.items,state.user.sorting.by)
|
||||
emitStateChanged();
|
||||
},
|
||||
updateClipboard: (value) => {
|
||||
|
||||
@@ -17,7 +17,6 @@ const loginPage = window.FileBrowser.LoginPage;
|
||||
const enableThumbs = window.FileBrowser.EnableThumbs;
|
||||
const resizePreview = window.FileBrowser.ResizePreview;
|
||||
const enableExec = window.FileBrowser.EnableExec;
|
||||
const externalUrl = window.FileBrowser.ExternalUrl
|
||||
const origin = window.location.origin;
|
||||
|
||||
const settings = [
|
||||
@@ -30,7 +29,6 @@ const settings = [
|
||||
|
||||
export {
|
||||
name,
|
||||
externalUrl,
|
||||
disableExternal,
|
||||
externalLinks,
|
||||
disableUsedPercentage,
|
||||
|
||||
@@ -1,32 +1,8 @@
|
||||
import { state, mutations, getters } from "@/store"
|
||||
import { filesApi } from "@/api";
|
||||
import { notify } from "@/notify"
|
||||
import { removePrefix } from "@/utils/url.js";
|
||||
import { publicApi } from "@/api";
|
||||
|
||||
export default function download() {
|
||||
if (getters.currentView() === "share") {
|
||||
let urlPath = getters.routePath("share");
|
||||
// Step 1: Split the path by '/'
|
||||
let parts = urlPath.split("/");
|
||||
// Step 2: Assign hash to the second part (index 2) and join the rest for subPath
|
||||
const hash = parts[1];
|
||||
const subPath = "/" + parts.slice(2).join("/");
|
||||
let files = [];
|
||||
for (let i of state.selected) {
|
||||
const dlfile = removePrefix(state.req.items[i].url, "share/"+hash);
|
||||
files.push(dlfile);
|
||||
}
|
||||
const share = {
|
||||
path: subPath,
|
||||
hash: hash,
|
||||
token: "",
|
||||
format: files.length ? "zip" : null,
|
||||
};
|
||||
publicApi.download(share, ...files);
|
||||
return
|
||||
}
|
||||
|
||||
if (getters.isSingleFileSelected()) {
|
||||
filesApi.download(null, [getters.selectedDownloadUrl()]);
|
||||
return;
|
||||
|
||||
@@ -3,8 +3,6 @@ import url from "@/utils/url.js";
|
||||
import { filesApi } from "@/api";
|
||||
|
||||
export function checkConflict(files, items) {
|
||||
console.log("testing",files)
|
||||
|
||||
if (typeof items === "undefined" || items === null) {
|
||||
items = [];
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export default {
|
||||
methods: {
|
||||
scrollToHash() {
|
||||
if (window.location.hash === this.lastHash) return;
|
||||
this.lastHash = window.location.hash;
|
||||
this.lastHash = window.location.hash
|
||||
if (window.location.hash) {
|
||||
const id = url.base64Encode(window.location.hash.slice(1));
|
||||
const element = document.getElementById(id);
|
||||
@@ -94,12 +94,13 @@ export default {
|
||||
},
|
||||
async fetchData() {
|
||||
if (state.route.path === this.lastPath) return;
|
||||
this.lastHash = "";
|
||||
this.lastHash = ""
|
||||
// Set loading to true and reset the error.
|
||||
mutations.setLoading("files", true);
|
||||
this.error = null;
|
||||
// Reset view information using mutations
|
||||
mutations.setReload(false);
|
||||
mutations.resetSelected();
|
||||
mutations.setMultiple(false);
|
||||
mutations.closeHovers();
|
||||
|
||||
@@ -124,7 +125,7 @@ export default {
|
||||
} catch (e) {
|
||||
notify.showError(e);
|
||||
this.error = e;
|
||||
mutations.replaceRequest({});
|
||||
mutations.replaceRequest(null);
|
||||
} finally {
|
||||
mutations.replaceRequest(data);
|
||||
mutations.setLoading("files", false);
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
<template>
|
||||
<template v-if="isLoggedIn">
|
||||
<div>
|
||||
<div
|
||||
v-show="showOverlay"
|
||||
@contextmenu.prevent="onOverlayRightClick"
|
||||
@click="resetPrompts"
|
||||
class="overlay"
|
||||
></div>
|
||||
<div v-show="showOverlay" @contextmenu.prevent="onOverlayRightClick" @click="resetPrompts" class="overlay"></div>
|
||||
<div v-if="progress" class="progress">
|
||||
<div v-bind:style="{ width: this.progress + '%' }"></div>
|
||||
</div>
|
||||
<listingBar
|
||||
:class="{ 'dark-mode-header': isDarkMode }"
|
||||
v-if="currentView == 'listingView'"
|
||||
></listingBar>
|
||||
<editorBar
|
||||
:class="{ 'dark-mode-header': isDarkMode }"
|
||||
v-else-if="currentView == 'editor'"
|
||||
></editorBar>
|
||||
<defaultBar v-else :class="{ 'dark-mode-header': isDarkMode }"></defaultBar>
|
||||
<listingBar :class="{ 'dark-mode-header': isDarkMode }" v-if="currentView == 'listingView'"></listingBar>
|
||||
<editorBar :class="{ 'dark-mode-header': isDarkMode }" v-else-if="currentView == 'editor'"></editorBar>
|
||||
<defaultBar :class="{ 'dark-mode-header': isDarkMode }" v-else></defaultBar>
|
||||
<sidebar></sidebar>
|
||||
<search v-if="showSearch"></search>
|
||||
<main :class="{ 'dark-mode': isDarkMode, moveWithSidebar: moveWithSidebar }">
|
||||
@@ -25,7 +14,6 @@
|
||||
</main>
|
||||
<prompts :class="{ 'dark-mode': isDarkMode }"></prompts>
|
||||
</div>
|
||||
|
||||
<div class="card" id="popup-notification">
|
||||
<i v-on:click="closePopUp" class="material-icons">close</i>
|
||||
<div id="popup-notification-content">no info</div>
|
||||
@@ -67,7 +55,7 @@ export default {
|
||||
mounted() {
|
||||
window.addEventListener("resize", this.updateIsMobile);
|
||||
if (state.user.themeColor) {
|
||||
document.documentElement.style.setProperty("--primaryColor", state.user.themeColor);
|
||||
document.documentElement.style.setProperty('--primaryColor', state.user.themeColor);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -126,6 +114,16 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onOverlayRightClick(event) {
|
||||
// Example: Show a custom context menu
|
||||
mutations.showHover({
|
||||
name: "ContextMenu", // Assuming ContextMenu is a component you've already imported
|
||||
props: {
|
||||
posX: event.clientX,
|
||||
posY: event.clientY,
|
||||
},
|
||||
});
|
||||
},
|
||||
updateIsMobile() {
|
||||
mutations.setMobile();
|
||||
},
|
||||
|
||||
@@ -67,13 +67,13 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
signup: () => signup,
|
||||
name: () => name || "FileBrowser Quantum",
|
||||
name: () => name,
|
||||
logoURL: () => logoURL,
|
||||
isDarkMode() {
|
||||
return darkMode === true;
|
||||
},
|
||||
loginName() {
|
||||
return name
|
||||
return name || "FileBrowser Quantum"
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
|
||||
@@ -279,7 +279,7 @@ export default {
|
||||
let file = await publicApi.fetchPub(this.subPath, this.hash, this.password);
|
||||
file.hash = this.hash;
|
||||
this.token = file.token;
|
||||
mutations.replaceRequest(file);
|
||||
mutations.updateRequest(file);
|
||||
document.title = `${file.name} - ${document.title}`;
|
||||
} catch (error) {
|
||||
this.error = error;
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
<template>
|
||||
<header :class="{ 'dark-mode-header': isDarkMode }">
|
||||
<header>
|
||||
<action v-if="notShare" icon="close" :label="$t('buttons.close')" @action="close()" />
|
||||
<title v-if="isSettings" class="topTitle">Settings</title>
|
||||
<title v-else class="topTitle">{{ req.name }}</title>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.flexbar {
|
||||
display: flex;
|
||||
flex-direction: block;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { url } from "@/utils";
|
||||
import router from "@/router";
|
||||
import { getters, state, mutations } from "@/store";
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import { filesApi } from "@/api";
|
||||
import Action from "@/components/Action.vue";
|
||||
|
||||
export default {
|
||||
@@ -17,18 +26,182 @@ export default {
|
||||
components: {
|
||||
Action,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
columnWidth: 350,
|
||||
width: window.innerWidth,
|
||||
itemWeight: 0,
|
||||
viewModes: ["list", "compact", "normal", "gallery"],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
notShare() {
|
||||
return getters.currentView() != "share";
|
||||
},
|
||||
isSettings() {
|
||||
return getters.isSettings();
|
||||
},
|
||||
// Map state and getters
|
||||
req() {
|
||||
return state.req;
|
||||
},
|
||||
isDarkMode() {
|
||||
return getters.isDarkMode();
|
||||
user() {
|
||||
return state.user;
|
||||
},
|
||||
selected() {
|
||||
return state.selected;
|
||||
},
|
||||
nameSorted() {
|
||||
return state.user.sorting.by === "name";
|
||||
},
|
||||
sizeSorted() {
|
||||
return state.user.sorting.by === "size";
|
||||
},
|
||||
modifiedSorted() {
|
||||
return state.user.sorting.by === "modified";
|
||||
},
|
||||
ascOrdered() {
|
||||
return state.req.sorting.asc;
|
||||
},
|
||||
items() {
|
||||
const dirs = [];
|
||||
const files = [];
|
||||
|
||||
state.req.items.forEach((item) => {
|
||||
if (item.type == "directory") {
|
||||
dirs.push(item);
|
||||
} else {
|
||||
files.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return { dirs, files };
|
||||
},
|
||||
dirs() {
|
||||
return this.items.dirs.slice(0, this.showLimit);
|
||||
},
|
||||
files() {
|
||||
let showLimit = this.showLimit - this.items.dirs.length;
|
||||
|
||||
if (showLimit < 0) showLimit = 0;
|
||||
|
||||
return this.items.files.slice(0, showLimit);
|
||||
},
|
||||
nameIcon() {
|
||||
if (this.nameSorted && !this.ascOrdered) {
|
||||
return "arrow_upward";
|
||||
}
|
||||
|
||||
return "arrow_downward";
|
||||
},
|
||||
sizeIcon() {
|
||||
if (this.sizeSorted && this.ascOrdered) {
|
||||
return "arrow_downward";
|
||||
}
|
||||
|
||||
return "arrow_upward";
|
||||
},
|
||||
modifiedIcon() {
|
||||
if (this.modifiedSorted && this.ascOrdered) {
|
||||
return "arrow_downward";
|
||||
}
|
||||
|
||||
return "arrow_upward";
|
||||
},
|
||||
viewIcon() {
|
||||
const icons = {
|
||||
list: "view_module",
|
||||
compact: "view_module",
|
||||
normal: "grid_view",
|
||||
gallery: "view_list",
|
||||
};
|
||||
return icons[state.user.viewMode];
|
||||
},
|
||||
headerButtons() {
|
||||
return {
|
||||
select: getters.selectedCount() > 0,
|
||||
upload: state.user.perm?.create && getters.selectedCount() > 0,
|
||||
download: state.user.perm?.download && getters.selectedCount() > 0,
|
||||
delete: getters.selectedCount() > 0 && state.user.perm.delete,
|
||||
rename: getters.selectedCount() === 1 && state.user.perm.rename,
|
||||
share: getters.selectedCount() === 1 && state.user.perm.share,
|
||||
move: getters.selectedCount() > 0 && state.user.perm.rename,
|
||||
copy: getters.selectedCount() > 0 && state.user.perm?.create,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
||||
// How much every listing item affects the window height
|
||||
this.setItemWeight();
|
||||
|
||||
// Fill and fit the window with listing items
|
||||
this.fillWindow(true);
|
||||
|
||||
// Add the needed event listeners to the window and document.
|
||||
window.addEventListener("keydown", this.keyEvent);
|
||||
window.addEventListener("scroll", this.scrollEvent);
|
||||
window.addEventListener("resize", this.windowsResize);
|
||||
if (state.route.path.startsWith("/share")) {
|
||||
return;
|
||||
}
|
||||
if (!state.user.perm?.create) return;
|
||||
document.addEventListener("dragover", this.preventDefault);
|
||||
document.addEventListener("dragenter", this.dragEnter);
|
||||
document.addEventListener("dragleave", this.dragLeave);
|
||||
document.addEventListener("drop", this.drop);
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
// Remove event listeners before destroying this page.
|
||||
window.removeEventListener("keydown", this.keyEvent);
|
||||
window.removeEventListener("scroll", this.scrollEvent);
|
||||
window.removeEventListener("resize", this.windowsResize);
|
||||
|
||||
if (state.user && !state.user.perm?.create) return;
|
||||
document.removeEventListener("dragover", this.preventDefault);
|
||||
document.removeEventListener("dragenter", this.dragEnter);
|
||||
document.removeEventListener("dragleave", this.dragLeave);
|
||||
document.removeEventListener("drop", this.drop);
|
||||
},
|
||||
|
||||
methods: {
|
||||
fillWindow(fit = false) {
|
||||
const totalItems = state.req.numDirs + state.req.numFiles;
|
||||
|
||||
// More items are displayed than the total
|
||||
if (this.showLimit >= totalItems && !fit) return;
|
||||
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
// Quantity of items needed to fill 2x of the window height
|
||||
const showQuantity = Math.ceil((windowHeight + windowHeight * 2) / this.itemWeight);
|
||||
|
||||
// Less items to display than current
|
||||
if (this.showLimit > showQuantity && !fit) return;
|
||||
|
||||
// Set the number of displayed items
|
||||
this.showLimit = showQuantity > totalItems ? totalItems : showQuantity;
|
||||
},
|
||||
setItemWeight() {
|
||||
// Listing element is not displayed
|
||||
if (this.$refs.listingView == null) return;
|
||||
|
||||
let itemQuantity = state.req.numDirs + state.req.numFiles;
|
||||
if (itemQuantity > this.showLimit) itemQuantity = this.showLimit;
|
||||
|
||||
// How much every listing item affects the window height
|
||||
this.itemWeight = this.$refs.listingView.offsetHeight / itemQuantity;
|
||||
},
|
||||
action() {
|
||||
if (this.show) {
|
||||
mutations.showHover(this.show);
|
||||
}
|
||||
|
||||
this.$emit("action");
|
||||
},
|
||||
close() {
|
||||
if (getters.isSettings()) {
|
||||
// Use this.isSettings to access the computed property
|
||||
@@ -41,6 +214,146 @@ export default {
|
||||
router.push({ path: uri });
|
||||
mutations.closeHovers();
|
||||
},
|
||||
base64(name) {
|
||||
return url.base64Encode(name);
|
||||
},
|
||||
keyEvent(event) {
|
||||
// No prompts are shown
|
||||
if (this.show !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Esc!
|
||||
if (event.keyCode === 27) {
|
||||
// Reset files selection.
|
||||
mutations.resetSelected();
|
||||
}
|
||||
|
||||
// Del!
|
||||
if (event.keyCode === 46) {
|
||||
if (!state.user.perm.delete || getters.selectedCount() == 0) return;
|
||||
|
||||
// Show delete prompt.
|
||||
mutations.showHover("delete");
|
||||
}
|
||||
|
||||
// F2!
|
||||
if (event.keyCode === 113) {
|
||||
if (!state.user.perm.rename || getters.selectedCount() !== 1) return;
|
||||
|
||||
// Show rename prompt.
|
||||
mutations.showHover("rename");
|
||||
}
|
||||
|
||||
// Ctrl is pressed
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
let key = String.fromCharCode(event.which).toLowerCase();
|
||||
|
||||
switch (key) {
|
||||
case "f":
|
||||
event.preventDefault();
|
||||
mutations.showHover("search");
|
||||
break;
|
||||
case "c":
|
||||
case "x":
|
||||
this.copyCut(event, key);
|
||||
break;
|
||||
case "v":
|
||||
this.paste(event);
|
||||
break;
|
||||
case "a":
|
||||
event.preventDefault();
|
||||
for (let file of this.items.files) {
|
||||
if (state.selected.indexOf(file.index) === -1) {
|
||||
this.addSelected(file.index);
|
||||
}
|
||||
}
|
||||
for (let dir of this.items.dirs) {
|
||||
if (state.selected.indexOf(dir.index) === -1) {
|
||||
this.addSelected(dir.index);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "s":
|
||||
event.preventDefault();
|
||||
document.getElementById("download-button").click();
|
||||
break;
|
||||
}
|
||||
},
|
||||
switchView: async function () {
|
||||
mutations.closeHovers();
|
||||
const currentIndex = this.viewModes.indexOf(state.user.viewMode);
|
||||
const nextIndex = (currentIndex + 1) % this.viewModes.length;
|
||||
const newView = this.viewModes[nextIndex];
|
||||
mutations.updateCurrentUser({ viewMode: newView });
|
||||
},
|
||||
preventDefault(event) {
|
||||
// Wrapper around prevent default.
|
||||
event.preventDefault();
|
||||
},
|
||||
copyCut(event, key) {
|
||||
if (event.target.tagName.toLowerCase() === "input") {
|
||||
return;
|
||||
}
|
||||
|
||||
let items = [];
|
||||
|
||||
for (let i of state.selected) {
|
||||
items.push({
|
||||
from: state.req.items[i].url,
|
||||
name: state.req.items[i].name,
|
||||
});
|
||||
}
|
||||
|
||||
if (items.length == 0) {
|
||||
return;
|
||||
}
|
||||
mutations.updateClipboard({
|
||||
key: key,
|
||||
items: items,
|
||||
path: state.route.path,
|
||||
});
|
||||
},
|
||||
async paste(event) {
|
||||
if (event.target.tagName.toLowerCase() === "input") {
|
||||
return;
|
||||
}
|
||||
|
||||
let items = [];
|
||||
|
||||
for (let item of state.clipboard.items) {
|
||||
const from = item.from.endsWith("/") ? item.from.slice(0, -1) : item.from;
|
||||
const to = state.route.path + encodeURIComponent(item.name);
|
||||
items.push({ from, to, name: item.name });
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let action = async (overwrite, rename) => {
|
||||
await filesApi.moveCopy(items, "copy", overwrite, rename);
|
||||
notify.showSuccess("Items pasted successfully.");
|
||||
mutations.setReload(true);
|
||||
};
|
||||
|
||||
this.$confirm(
|
||||
"Are you sure you want to copy these items?",
|
||||
"Copy",
|
||||
() => {
|
||||
action(false, false);
|
||||
},
|
||||
() => {
|
||||
action(true, false);
|
||||
},
|
||||
() => {
|
||||
action(true, true);
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -16,64 +16,26 @@
|
||||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>{{ $t("files.lonely") }}</span>
|
||||
</h2>
|
||||
<input
|
||||
style="display: none"
|
||||
type="file"
|
||||
id="upload-input"
|
||||
@change="uploadInput($event)"
|
||||
multiple
|
||||
/>
|
||||
<input
|
||||
style="display: none"
|
||||
type="file"
|
||||
id="upload-folder-input"
|
||||
@change="uploadInput($event)"
|
||||
webkitdirectory
|
||||
multiple
|
||||
/>
|
||||
<input style="display: none" type="file" id="upload-input" @change="uploadInput($event)" multiple />
|
||||
<input style="display: none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory
|
||||
multiple />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
id="listingView"
|
||||
ref="listingView"
|
||||
:class="listingViewMode + ' file-icons'"
|
||||
>
|
||||
<div v-else id="listingView" ref="listingView" :class="listingViewMode + ' file-icons'">
|
||||
<div>
|
||||
<div class="header" :class="{ 'dark-mode-item-header': isDarkMode }">
|
||||
<p
|
||||
:class="{ active: nameSorted }"
|
||||
class="name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('name')"
|
||||
:title="$t('files.sortByName')"
|
||||
:aria-label="$t('files.sortByName')"
|
||||
>
|
||||
<p :class="{ active: nameSorted }" class="name" role="button" tabindex="0" @click="sort('name')"
|
||||
:title="$t('files.sortByName')" :aria-label="$t('files.sortByName')">
|
||||
<span>{{ $t("files.name") }}</span>
|
||||
<i class="material-icons">{{ nameIcon }}</i>
|
||||
</p>
|
||||
|
||||
<p
|
||||
:class="{ active: sizeSorted }"
|
||||
class="size"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('size')"
|
||||
:title="$t('files.sortBySize')"
|
||||
:aria-label="$t('files.sortBySize')"
|
||||
>
|
||||
<p :class="{ active: sizeSorted }" class="size" role="button" tabindex="0" @click="sort('size')"
|
||||
:title="$t('files.sortBySize')" :aria-label="$t('files.sortBySize')">
|
||||
<span>{{ $t("files.size") }}</span>
|
||||
<i class="material-icons">{{ sizeIcon }}</i>
|
||||
</p>
|
||||
<p
|
||||
:class="{ active: modifiedSorted }"
|
||||
class="modified"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('modified')"
|
||||
:title="$t('files.sortByLastModified')"
|
||||
:aria-label="$t('files.sortByLastModified')"
|
||||
>
|
||||
<p :class="{ active: modifiedSorted }" class="modified" role="button" tabindex="0" @click="sort('modified')"
|
||||
:title="$t('files.sortByLastModified')" :aria-label="$t('files.sortByLastModified')">
|
||||
<span>{{ $t("files.lastModified") }}</span>
|
||||
<i class="material-icons">{{ modifiedIcon }}</i>
|
||||
</p>
|
||||
@@ -84,23 +46,10 @@
|
||||
<h2>{{ $t("files.folders") }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="numDirs > 0"
|
||||
class="folder-items"
|
||||
:class="{ lastGroup: numFiles === 0 }"
|
||||
>
|
||||
<item
|
||||
v-for="item in dirs"
|
||||
:key="base64(item.name)"
|
||||
v-bind:index="item.index"
|
||||
v-bind:name="item.name"
|
||||
v-bind:isDir="item.type == 'directory'"
|
||||
v-bind:url="item.url"
|
||||
v-bind:modified="item.modified"
|
||||
v-bind:type="item.type"
|
||||
v-bind:size="item.size"
|
||||
v-bind:path="item.path"
|
||||
/>
|
||||
<div v-if="numDirs > 0" class="folder-items" :class="{ lastGroup: numFiles === 0 }">
|
||||
<item v-for="item in dirs" :key="base64(item.name)" v-bind:index="item.index" v-bind:name="item.name"
|
||||
v-bind:isDir="item.type == 'directory'" v-bind:url="item.url" v-bind:modified="item.modified"
|
||||
v-bind:type="item.type" v-bind:size="item.size" v-bind:path="item.path" />
|
||||
</div>
|
||||
<div v-if="numFiles > 0">
|
||||
<div class="header-items">
|
||||
@@ -108,35 +57,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="numFiles > 0" class="file-items" :class="{ lastGroup: numFiles > 0 }">
|
||||
<item
|
||||
v-for="item in files"
|
||||
:key="base64(item.name)"
|
||||
v-bind:index="item.index"
|
||||
v-bind:name="item.name"
|
||||
v-bind:isDir="item.type == 'directory'"
|
||||
v-bind:url="item.url"
|
||||
v-bind:modified="item.modified"
|
||||
v-bind:type="item.type"
|
||||
v-bind:size="item.size"
|
||||
v-bind:path="item.path"
|
||||
/>
|
||||
<item v-for="item in files" :key="base64(item.name)" v-bind:index="item.index" v-bind:name="item.name"
|
||||
v-bind:isDir="item.type == 'directory'" v-bind:url="item.url" v-bind:modified="item.modified"
|
||||
v-bind:type="item.type" v-bind:size="item.size" v-bind:path="item.path" />
|
||||
</div>
|
||||
|
||||
<input
|
||||
style="display: none"
|
||||
type="file"
|
||||
id="upload-input"
|
||||
@change="uploadInput($event)"
|
||||
multiple
|
||||
/>
|
||||
<input
|
||||
style="display: none"
|
||||
type="file"
|
||||
id="upload-folder-input"
|
||||
@change="uploadInput($event)"
|
||||
webkitdirectory
|
||||
multiple
|
||||
/>
|
||||
<input style="display: none" type="file" id="upload-input" @change="uploadInput($event)" multiple />
|
||||
<input style="display: none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory
|
||||
multiple />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,7 +93,6 @@ export default {
|
||||
width: window.innerWidth,
|
||||
lastSelected: {}, // Add this to track the currently focused item
|
||||
contextTimeout: null, // added for safari context menu
|
||||
ctrKeyPressed: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -269,6 +196,7 @@ export default {
|
||||
this.colunmsResize();
|
||||
return state.user.viewMode;
|
||||
},
|
||||
|
||||
selectedCount() {
|
||||
return state.selected.length;
|
||||
},
|
||||
@@ -287,8 +215,7 @@ export default {
|
||||
window.addEventListener("keydown", this.keyEvent);
|
||||
window.addEventListener("scroll", this.scrollEvent);
|
||||
window.addEventListener("resize", this.windowsResize);
|
||||
window.addEventListener("click", this.clickClear);
|
||||
window.addEventListener("keyup", this.clearCtrKey);
|
||||
this.$el.addEventListener("click", this.clickClear);
|
||||
|
||||
// Adjust contextmenu listener based on browser
|
||||
if (state.isSafari) {
|
||||
@@ -323,6 +250,7 @@ export default {
|
||||
this.$el.removeEventListener("touchend", this.cancelContext);
|
||||
this.$el.removeEventListener("mouseup", this.cancelContext);
|
||||
this.$el.removeEventListener("touchmove", this.handleTouchMove);
|
||||
|
||||
} else {
|
||||
window.removeEventListener("contextmenu", this.openContext);
|
||||
}
|
||||
@@ -497,22 +425,17 @@ export default {
|
||||
}, 50);
|
||||
}
|
||||
},
|
||||
clearCtrKey(event) {
|
||||
const { ctrlKey } = event;
|
||||
if (!ctrlKey) {
|
||||
this.ctrKeyPressed = false;
|
||||
}
|
||||
},
|
||||
keyEvent(event) {
|
||||
const { key, ctrlKey, metaKey, which } = event;
|
||||
// Check if the key is alphanumeric
|
||||
const isAlphanumeric = /^[a-z0-9]$/i.test(key);
|
||||
const modifierKeys = ctrlKey || metaKey;
|
||||
if (isAlphanumeric && !modifierKeys && getters.currentPromptName() == null) {
|
||||
const noModifierKeys = !ctrlKey && !metaKey;
|
||||
|
||||
if (isAlphanumeric && noModifierKeys && getters.currentPromptName() == null) {
|
||||
this.alphanumericKeyPress(key); // Call the alphanumeric key press function
|
||||
return;
|
||||
}
|
||||
if (!modifierKeys && getters.currentPromptName() != null) {
|
||||
if (noModifierKeys && getters.currentPromptName() != null) {
|
||||
return;
|
||||
}
|
||||
// Handle the space bar key
|
||||
@@ -529,13 +452,6 @@ export default {
|
||||
}
|
||||
let currentPath = state.route.path.replace(/\/+$/, ""); // Remove trailing slashes
|
||||
let newPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
|
||||
|
||||
if (modifierKeys) {
|
||||
if (!ctrlKey) {
|
||||
this.ctrKeyPressed = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Handle key events using a switch statement
|
||||
switch (key) {
|
||||
case "Enter":
|
||||
@@ -570,6 +486,11 @@ export default {
|
||||
event.preventDefault();
|
||||
this.navigateKeboardArrows(key);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Handle keys with ctrl or meta keys
|
||||
if (!ctrlKey && !metaKey) return;
|
||||
break;
|
||||
}
|
||||
|
||||
const charKey = String.fromCharCode(which).toLowerCase();
|
||||
@@ -592,6 +513,8 @@ export default {
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// Helper method to select all files and directories
|
||||
selectAll() {
|
||||
for (let file of this.items.files) {
|
||||
if (state.selected.indexOf(file.index) === -1) {
|
||||
@@ -727,18 +650,12 @@ export default {
|
||||
action(false, false);
|
||||
},
|
||||
colunmsResize() {
|
||||
document.documentElement.style.setProperty(
|
||||
"--item-width",
|
||||
`calc(${100 / this.numColumns}% - 1em)`
|
||||
);
|
||||
document.documentElement.style.setProperty('--item-width', `calc(${100 / this.numColumns}% - 1em)`);
|
||||
|
||||
if (state.user.viewMode == "gallery") {
|
||||
document.documentElement.style.setProperty(
|
||||
"--item-height",
|
||||
`calc(${this.columnWidth / 25}em)`
|
||||
);
|
||||
document.documentElement.style.setProperty('--item-height', `calc(${this.columnWidth / 25}em)`);
|
||||
} else {
|
||||
document.documentElement.style.setProperty("--item-height", `auto`);
|
||||
document.documentElement.style.setProperty('--item-height', `auto`);
|
||||
}
|
||||
},
|
||||
dragEnter() {
|
||||
@@ -897,11 +814,9 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
clickClear(event) {
|
||||
// if control or shift is pressed, do not clear the selection
|
||||
if (this.ctrKeyPressed || event.shiftKey) return;
|
||||
clickClear() {
|
||||
const sameAsBefore = state.selected == this.lastSelected;
|
||||
if (sameAsBefore && !state.multiple && getters.currentPromptName == null) {
|
||||
if (sameAsBefore && !state.multiple) {
|
||||
mutations.resetSelected();
|
||||
}
|
||||
this.lastSelected = state.selected;
|
||||
|
||||
@@ -110,7 +110,7 @@ export default {
|
||||
event.preventDefault();
|
||||
try {
|
||||
if (this.isNew) {
|
||||
await usersApi.create(this.userPayload); // Use the computed property
|
||||
const loc = await usersApi.create(this.userPayload); // Use the computed property
|
||||
this.$router.push({ path: "/settings", hash: "#users-main" });
|
||||
notify.showSuccess(this.$t("settings.userCreated"));
|
||||
} else {
|
||||
|
||||
+20
-14
@@ -1,8 +1,6 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("redirect to login", async ({ page, context }) => {
|
||||
await context.clearCookies();
|
||||
import { test, expect } from "./fixtures/auth";
|
||||
|
||||
test("redirect to login", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
|
||||
@@ -10,16 +8,24 @@ test("redirect to login", async ({ page, context }) => {
|
||||
await expect(page).toHaveURL(/\/login\?redirect=\/files\//);
|
||||
});
|
||||
|
||||
test("logout", async ({ page, context }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator("div.wrong")).toBeHidden();
|
||||
await page.waitForURL("**/files/", { timeout: 100 });
|
||||
await expect(page).toHaveTitle('playwright-files - FileBrowser Quantum - Files');
|
||||
test("login", async ({ authPage, page, context }) => {
|
||||
await authPage.goto();
|
||||
await expect(page).toHaveTitle(/Login - FileBrowser Quantum$/);
|
||||
|
||||
await authPage.loginAs("fake", "fake");
|
||||
await expect(authPage.wrongCredentials).toBeVisible();
|
||||
|
||||
await authPage.loginAs();
|
||||
await expect(authPage.wrongCredentials).toBeHidden();
|
||||
// await page.waitForURL("**/files/", { timeout: 5000 });
|
||||
await expect(page).toHaveTitle(/.*Files - FileBrowser Quantum$/);
|
||||
|
||||
let cookies = await context.cookies();
|
||||
expect(cookies.find((c) => c.name == "auth")?.value).toBeDefined();
|
||||
await page.locator('div.inner-card.logout-button').click();
|
||||
await page.waitForURL("**/login", { timeout: 100 });
|
||||
await expect(page).toHaveTitle('FileBrowser Quantum - Login');
|
||||
cookies = await context.cookies();
|
||||
expect(cookies.find((c) => c.name == "auth")?.value).toBeUndefined();
|
||||
|
||||
// await authPage.logout();
|
||||
// await page.waitForURL("**/login", { timeout: 5000 });
|
||||
// await expect(page).toHaveTitle(/Login - FileBrowser Quantum$/);
|
||||
// cookies = await context.cookies();
|
||||
// expect(cookies.find((c) => c.name == "auth")?.value).toBeUndefined();
|
||||
});
|
||||
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
type Page,
|
||||
type Locator,
|
||||
test as base,
|
||||
expect,
|
||||
} from "@playwright/test";
|
||||
|
||||
export class AuthPage {
|
||||
public readonly wrongCredentials: Locator;
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.wrongCredentials = this.page.locator("div.wrong");
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto("/login");
|
||||
}
|
||||
|
||||
async loginAs(username = "admin", password = "admin") {
|
||||
await this.page.getByPlaceholder("Username").fill(username);
|
||||
await this.page.getByPlaceholder("Password").fill(password);
|
||||
await this.page.getByRole("button", { name: "Login" }).click();
|
||||
}
|
||||
|
||||
//async logout() {
|
||||
// await this.page.getByRole("button", { name: "Logout" }).click();
|
||||
//}
|
||||
}
|
||||
|
||||
const test = base.extend<{ authPage: AuthPage }>({
|
||||
authPage: async ({ page }, use) => {
|
||||
const authPage = new AuthPage(page);
|
||||
await authPage.goto();
|
||||
await authPage.loginAs();
|
||||
await use(authPage);
|
||||
// await authPage.logout();
|
||||
},
|
||||
});
|
||||
|
||||
export { test, expect };
|
||||
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user