Compare commits

...
20 Commits
Author SHA1 Message Date
Graham Steffaniak 3ac9802e5c updated search (#219) 2024-10-07 17:36:02 -05:00
Graham Steffaniak 742776c492 updated minor things 2024-10-07 12:46:54 -05:00
Graham Steffaniak 3ceaf43b86 fixed issue with dir sizes 2024-10-07 12:34:40 -05:00
Graham Steffaniak bb420e9939 updated benchmark results 2024-10-07 11:36:32 -05:00
Graham Steffaniak 710d9c523b Show folder sizes (#218) 2024-10-07 11:33:15 -05:00
Graham Steffaniak ac75b5344d updated gallery resizing persistence 2024-10-05 13:55:20 -05:00
Graham Steffaniak 64c5a24d1f updated listeners 2024-10-05 13:46:44 -05:00
Graham Steffaniak f3567c01e8 fixed multiple selection clicking (#217) 2024-10-05 13:01:51 -05:00
Graham Steffaniak 69de3d712b updated main screenshot 2024-10-05 12:46:18 -05:00
Graham Steffaniak 4c8e35ebe0 Comandline users (#216) 2024-10-05 12:44:36 -05:00
Graham Steffaniak 9983cde9b6 updated docs 2024-09-23 10:39:27 -05:00
Graham Steffaniak b4b92bf852 Adjusted path tweak (#213) 2024-09-17 12:15:29 -05:00
Graham Steffaniak 2ec0c457cf hotfix: adjusted paths (#211) 2024-09-16 20:30:55 -05:00
Graham Steffaniak c272eb1e61 hotfix: search path adjustment 2024-09-16 20:19:25 -05:00
Graham Steffaniak bae39c8586 Update main.yaml workflow
add fetch depth
2024-09-16 17:26:57 -05:00
Graham Steffaniak 62d1cd88a1 V0.2.9 release (#205) 2024-09-16 16:01:16 -05:00
Graham Steffaniak 0bad14b51e workflow 2024-08-24 19:20:54 -05:00
Graham Steffaniak 74ccfab070 Update search behavior (#195) 2024-08-24 19:12:47 -05:00
Graham Steffaniak 5f185597c9 fix main workflow 2024-08-24 17:06:55 -05:00
Graham Steffaniak 9e9109984d v0.2.8 release (#193) 2024-08-24 17:02:33 -05:00
154 changed files with 5091 additions and 3501 deletions

No files matched your search

+10 -7
View File
@@ -1,4 +1,4 @@
name: main job
name: main release
on:
push:
@@ -12,16 +12,16 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Find latest tag
run: |
echo "LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV
echo "latest tag is $LATEST_TAG"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
# Workaround to fix error:
# failed to push: failed to copy: io: read/write on closed pipe
# See https://github.com/docker/build-push-action/issues/761
with:
driver-opts: |
image=moby/buildkit:v0.10.6
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
@@ -36,6 +36,9 @@ jobs:
uses: docker/build-push-action@v5
with:
context: .
build-args: |
VERSION=${{ env.LATEST_TAG }}
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
file: ./Dockerfile
push: true
+22 -10
View File
@@ -1,13 +1,28 @@
name: pr-merge
name: pr-request
on:
pull_request:
branches:
- "main"
- "v[0-9]+.[0-9]+.[0-9]+"
- "dev_*"
jobs:
test_frontend:
name: Push release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- name: Build
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile.playwright
push: false
push_pr_to_registry:
name: Push PR
runs-on: ubuntu-latest
@@ -18,12 +33,6 @@ jobs:
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
# Workaround to fix error:
# failed to push: failed to copy: io: read/write on closed pipe
# See https://github.com/docker/build-push-action/issues/761
with:
driver-opts: |
image=moby/buildkit:v0.10.6
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
@@ -35,10 +44,13 @@ jobs:
with:
images: gtstef/filebrowser
- name: Build and push
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=${{ steps.meta.outputs.version }}
REVISION=${{ steps.meta.outputs.revision }}
+4 -4
View File
@@ -1,4 +1,4 @@
name: dev
name: dev tests
on:
push:
@@ -21,10 +21,10 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- uses: golangci/golangci-lint-action@v6
go-version: '1.22.5'
- uses: golangci/golangci-lint-action@v5
with:
version: v1.59
version: v1.60
working-directory: backend
format-backend:
runs-on: ubuntu-latest
+7 -10
View File
@@ -1,4 +1,4 @@
name: release
name: version release
on:
push:
@@ -34,6 +34,7 @@ jobs:
draft: false
generate_release_notes: true
name: ${{ steps.extract_branch.outputs.branch }}
push_release_to_registry:
name: Push release
runs-on: ubuntu-latest
@@ -44,14 +45,7 @@ jobs:
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
# Workaround to fix error:
# failed to push: failed to copy: io: read/write on closed pipe
# See https://github.com/docker/build-push-action/issues/761
with:
driver-opts: |
image=moby/buildkit:v0.10.6
- name: Login to Docker Hub
# Only push to Docker Hub when making a release
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -69,11 +63,14 @@ jobs:
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
build-args: |
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
file: ./Dockerfile
push: true
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
labels: ${{ steps.meta.outputs.labels }}
labels: ${{ steps.meta.outputs.labels }}
+50
View File
@@ -0,0 +1,50 @@
name: dev release
on:
push:
branches:
- "dev_v[0-9]+.[0-9]+.[0-9]+"
permissions:
contents: write
jobs:
push_release_to_registry:
name: Push dev release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: gtstef/filebrowser
- name: Strip v from version number
id: modify-json
run: |
JSON="${{ steps.meta.outputs.tags }}"
# Use jq to remove 'v' from the version field
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
echo "CLEANED_TAG=$JSON" >> $GITHUB_ENV
- name: Build and push
uses: docker/build-push-action@v6
with:
build-args: |
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
context: .
platforms: linux/amd64
file: ./Dockerfile
push: true
tags: ${{ env.CLEANED_TAG }}
labels: ${{ steps.meta.outputs.labels }}
-24
View File
@@ -1,24 +0,0 @@
name: 'Close stale issues and PRs'
permissions:
issues: write
pull-requests: write
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v5
with:
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
close-issue-message: 'This issue was closed because it has been stalled for 30 days with no activity.'
days-before-stale: 30
days-before-close: 30
exempt-issue-labels: 'feature ☘,enhancement ⚙,bug 🐞'
exempt-pr-labels: 'need-help,wip'
operations-per-run: 100
+2
View File
@@ -7,10 +7,12 @@ rice-box.go
/filebrowser.exe
/frontend/dist
/frontend/pkg
/frontend/test-results
/frontend/package-lock.json
/backend/vendor
/backend/*.cov
/backend/test_config.yaml
/backend/srv
.DS_Store
node_modules
+46
View File
@@ -2,6 +2,52 @@
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
## v0.2.10
**New Features**:
- Allows user creation command line arguments https://github.com/gtsteffaniak/filebrowser/issues/196
- Folder sizes are always shown, leveraging the index. https://github.com/gtsteffaniak/filebrowser/issues/138
- Searching files based on filesize is no longer slower.
**Bugfixes**:
- fixes file selection usage when in single-click mode https://github.com/gtsteffaniak/filebrowser/issues/214
- Fixed displayed search context on root directory
- Fixed issue searching "smaller than" actually returned files "larger than"
**Notes**:
- Memory usage from index is reduced by ~40%
- Indexing time has increased 2x due to the extra processing time required to calculate directory sizes.
- File size calcuations use 1024 base vs previous 1000 base (matching windows explorer)
## v0.2.9
This release focused on UI navigation experience. Improving keyboard navigation and adds right click context menu.
**New Features**:
- listing view items are middle-clickable on selected listing or when in single-click mode.
- listing view items can be navigated via arrow keys.
- listing view can jump to items using letters and number keys to cycle through files that start with that character.
- You can use the enter key and backspace key to navigate backwards and forwards on selected items.
- ctr-space will open/close the search (leaving ctr-f to browser default find prompt)
- Added right-click context menu to replace the file selection prompt.
**Bugfixes**:
- Fixed drag to upload not working.
- Fixed shared video link issues.
- Fixed user edit bug related to other user.
- Fixed password reset bug.
- Fixed loading state getting stuck.
## v0.2.8
- **Feature**: New gallary view scaling options (closes [#141](https://github.com/gtsteffaniak/filebrowser/issues/141))
- **Change**: Refactored backend files functions
- **Change**: Improved UI response to filesystem changes
- **Change**: Added frontend tests for deployment integrity
- **Fix**: move/replace file prompt issue
- **Fix**: opening files from search
- **Fix**: Display count issue when hideDotFile is enabled.
## v0.2.7
- **Change**: New sidebar style and behavior
+12 -8
View File
@@ -1,18 +1,22 @@
FROM node:slim as nbuild
FROM golang:1.22-alpine AS base
ARG VERSION
ARG REVISION
WORKDIR /app
COPY ./frontend/package*.json ./
COPY ./backend ./
RUN go build -ldflags="-w -s \
-X 'github.com/gtsteffaniak/filebrowser/version.Version=${VERSION}' \
-X 'github.com/gtsteffaniak/filebrowser/version.CommitSHA=${REVISION}'" \
-o filebrowser .
FROM node:slim AS nbuild
WORKDIR /app
COPY ./frontend/package.json ./
RUN npm i --maxsockets 1
COPY ./frontend/ ./
RUN npm run build-docker
FROM golang:1.22-alpine as base
WORKDIR /app
COPY ./backend ./
RUN go build -ldflags="-w -s" -o filebrowser .
FROM alpine:latest
ENV FILEBROWSER_NO_EMBEDED="true"
ARG app="/app/filebrowser"
RUN apk --no-cache add ca-certificates mailcap
COPY --from=base /app/filebrowser* ./
COPY --from=nbuild /app/dist/ ./frontend/dist/
+19
View File
@@ -0,0 +1,19 @@
FROM golang:1.22-alpine AS base
WORKDIR /app
COPY ./backend ./
RUN go build -ldflags="-w -s" -o filebrowser .
FROM node:slim
WORKDIR /app
COPY ./frontend/package.json ./
RUN npm i --maxsockets 1
RUN npx playwright install --with-deps firefox
COPY [ "backend/filebrowser.yaml", "./" ]
COPY ./frontend/ ./frontend
WORKDIR /app/frontend
RUN npm run build-docker
WORKDIR /app
COPY --from=base /app/filebrowser* ./
RUN cp -R frontend/tests/ srv
ENV FILEBROWSER_NO_EMBEDED="true"
RUN ./filebrowser & sleep 2 && cd frontend && npx playwright test
+83 -38
View File
@@ -4,9 +4,9 @@
<p align="center">
<img src="frontend/public/img/icons/favicon-256x256.png" width="100" title="Login With Custom URL">
</p>
<h3 align="center">Filebrowser - A modern web-based file manager</h3>
<h3 align="center">FileBrowser Quantum - A modern web-based file manager</h3>
<p align="center">
<img width="800" src="https://github.com/user-attachments/assets/8ba93582-aba2-4996-8ac3-25f763a2e596" title="Main Screenshot">
<img width="800" src="https://private-user-images.githubusercontent.com/42989099/367975355-3d6f4619-4985-4ce3-952f-286510dff4f1.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjgxNTA2MjEsIm5iZiI6MTcyODE1MDMyMSwicGF0aCI6Ii80Mjk4OTA5OS8zNjc5NzUzNTUtM2Q2ZjQ2MTktNDk4NS00Y2UzLTk1MmYtMjg2NTEwZGZmNGYxLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEwMDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMDA1VDE3NDUyMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTg1OGNlMWM3M2I1ZmY3MDcxMGU1ODc3N2ZkMjI5YWQ3YzEyODRmNDU0ZDkxMjJhNTU0ZGY1MDQ2YmIwOWRmMTgmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.mOl0Hm70XmQEk-DPzx1FbwrpxNMDAqb-WDprs1HK-mc" title="Main Screenshot">
</p>
> [!WARNING]
@@ -15,38 +15,41 @@
> Starting with v0.2.4 *ALL* share links need to be re-created (due to
> security fix).
This fork makes the following significant changes to filebrowser for
origin:
FileBrowser Quantum is a fork of the filebrowser opensource project with the
following changes:
1. [x] Better search
- Lightning fast
- real-time results as you type
- Works with more type filters
- interactive results page.
1. [x] Efficiently indexed files
- Real-time search results as you type
- Search Works with more type filters
- Enhanced interactive results page.
2. [x] Revamped and simplified GUI navbar and sidebar menu.
- Additional compact view mode as well as refreshed view mode
styles.
3. [x] Revamped configuration via `filebrowser.yml` config file.
- More configurations possible at a per-user level
3. [x] Revamped and simplified configuration via `filebrowser.yml` config file.
4. [x] Faster listing browsing
- Switching view modes is instant
- Changing Sort order is instant
- The entire directory is loaded in 1/3 the time
## About
Filebrowser provides a file managing interface within a specified directory
FileBrowser Quantum provides a file managing interface within a specified directory
and can be used to upload, delete, preview, rename, and edit your files.
It allows the creation of multiple users and each user can have its
directory.
This repository is a fork, a collection of changes that make this program
work better in terms of aesthetics and performance. Improved search,
simplified ui (without removing features) and more secure and up-to-date
This repository is a fork of the original [filebrowser](https://github.com/filebrowser/filebrowser)
with a collection of changes that make this program work better in terms of
aesthetics and performance. Improved search, simplified ui
(without removing features) and more secure and up-to-date
build are just a few examples.
This Implementation of filebrowser differs significantly to the original.
FileBrowser Quantum differs significantly to the original.
There are hundreds of thousands of lines changed and they are generally
no longer compatible with each other. This has been intentional -- the
focus of this fork is on a few key principles:
- Simplicity and improved user experience
- Efficiency of operations and performance
- Improving performance and faster feedback when making changes.
- Minimize external dependencies and standard library usage.
- Of course -- adding much-needed features.
@@ -81,7 +84,7 @@ Using docker:
docker run -it -v /path/to/folder:/srv -p 80:80 gtstef/filebrowser
```
1. docker-compose:
1. docker compose:
- with local storage
@@ -128,39 +131,81 @@ Not using docker (not recommended), download your binary from releases and run w
./filebrowser -c <filebrowser.yml or other /path/to/config.yaml>
```
## Command Line Usage
There are very few commands available. There are 3 actions done via command line:
1. Running the program, as shown on install step. Only argument used is the config file, if you choose to override default "filebrowser.yaml"
2. Checking the version info via `./filebrowser version`
3. Updating the DB, which currently only supports adding users via `./filebrowser set -u username,password [-a] [-s "example/scope"]`
## Configuration
All configuration is now done via a single configuration file:
`filebrowser.yaml`, here is an example of minimal [configuration
file](./backend/filebrowser.yaml).
View the [Configuration Help Page](./configuration.md) for available
View the [Configuration Help Page](./docs/configuration.md) for available
configuration options and other help.
## Migration from filebrowser/filebrowser
If you currently use filebrowser from the filebrowser/filebrowser
repo but want to try using this. I recommend you start fresh without
reusing the database, but there are a few things you'll need to do if you
must migrate:
If you currently use the original filebrowser but want to try using this.
I recommend you start fresh without reusing the database. If you want to
migrate your existing database to FileBrowser Quantum, visit the [migration
readme](./docs/migration.md)
1. Create a configuration file as mentioned above.
2. Copy your database file from the original filebrowser to the path of
the new one.
3. Update the configuration file to use the database (under server in
filebrowser.yml)
4. If you are using docker, update the docker-compose file or docker run
command to use the config file as described in the install section
above.
5. If you are not using docker, just make sure you run filebrowser -c
filebrowser.yml and have a valid filebrowser config.
## Comparison Chart
The filebrowser application should run with the same user and rules that
you have from the original. But keep in mind the differences that are
mentioned at the top of this readme.
Application Name | <img width="48" src="frontend/public/img/icons/favicon-256x256.png" > Quantum | <img width="48" src="https://github.com/filebrowser/filebrowser/blob/master/frontend/public/img/logo.svg" > Filebrowser | <img width="48" src="https://github.com/mickael-kerjean/filestash/blob/master/public/assets/logo/app_icon.png?raw=true" > Filestash | <img width="48" src="https://avatars.githubusercontent.com/u/19211038?s=200&v=4" > Nextcloud | <img width="48" src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/da/Google_Drive_logo.png/480px-Google_Drive_logo.png" > Google_Drive | <img width="48" src="https://avatars.githubusercontent.com/u/6422152?v=4" > FileRun
--- | --- | --- | --- | --- | --- | --- |
Filesystem support | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
Linux | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
Windows | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
Mac | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
Self hostable | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
Has Stable Release? | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
S3 support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
webdav support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
ftp support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
Dedicated docs site? | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ |
Multiple sources at once | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
Docker image size | 22 MB | 31 MB | 240 MB (main image) | 250 MB | ❌ | > 2 GB |
Min. Memory Requirements | 128 MB | 128 MB | 128 MB (main image) | 128 MB | ❌ | 4 GB |
has standalone binary | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
price | free | free | free | free tier | free tier | $99+ |
rich media preview | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
upload files from the web? | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
Advanced Search? | ✅ | ❌ | ❌ | configurable | ✅ | ✅ |
Indexed Search? | ✅ | ❌ | ❌ | configurable | ✅ | ✅ |
Content-aware search? | ❌ | ❌ | ❌ | configurable | ✅ | ✅ |
Custom job support | ❌ | ✅ | ❌ | ✅ | ❌ | ✅ |
Multiple users | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Single sign-on support | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
LDAP sign-on support | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
2FA sign-on support | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
Long-live API key support | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
Mobile App | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
open source? | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
tags support | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ |
sharable web links? | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Event-based notifications | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
Metrics | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
file space quotas | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
text-based files editor | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
office file support | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
Themes | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
Branding support | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
activity log | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
Comments support | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
collaboration on same file | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
trash support | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
Starred/pinned files | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
Content preview icons | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ |
Plugins support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
Chromecast support | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
## Roadmap
see [Roadmap Page](./roadmap.md)
see [Roadmap Page](./docs/roadmap.md)
+232
View File
@@ -0,0 +1,232 @@
# This code is licensed under the terms of the MIT license https://opensource.org/license/mit
# Copyright (c) 2021 Marat Reymers
## Golden config for golangci-lint v1.59.1
#
# This is the best config for golangci-lint based on my experience and opinion.
# It is very strict, but not extremely strict.
# Feel free to adapt and change it for your needs.
run:
# Timeout for analysis, e.g. 30s, 5m.
# Default: 1m
timeout: 3m
# This file contains only configs which differ from defaults.
# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml
linters-settings:
cyclop:
# The maximal code complexity to report.
# Default: 10
max-complexity: 30
# The maximal average package complexity.
# If it's higher than 0.0 (float) the check is enabled
# Default: 0.0
package-average: 10.0
errcheck:
# Report about not checking of errors in type assertions: `a := b.(MyStruct)`.
# Such cases aren't reported by default.
# Default: false
check-type-assertions: true
exhaustive:
# Program elements to check for exhaustiveness.
# Default: [ switch ]
check:
- switch
- map
exhaustruct:
# List of regular expressions to exclude struct packages and their names from checks.
# Regular expressions must match complete canonical struct package/name/structname.
# Default: []
exclude:
# std libs
- "^net/http.Client$"
- "^net/http.Cookie$"
- "^net/http.Request$"
- "^net/http.Response$"
- "^net/http.Server$"
- "^net/http.Transport$"
- "^net/url.URL$"
- "^os/exec.Cmd$"
- "^reflect.StructField$"
# public libs
- "^github.com/Shopify/sarama.Config$"
- "^github.com/Shopify/sarama.ProducerMessage$"
- "^github.com/mitchellh/mapstructure.DecoderConfig$"
- "^github.com/prometheus/client_golang/.+Opts$"
- "^github.com/spf13/cobra.Command$"
- "^github.com/spf13/cobra.CompletionOptions$"
- "^github.com/stretchr/testify/mock.Mock$"
- "^github.com/testcontainers/testcontainers-go.+Request$"
- "^github.com/testcontainers/testcontainers-go.FromDockerfile$"
- "^golang.org/x/tools/go/analysis.Analyzer$"
- "^google.golang.org/protobuf/.+Options$"
- "^gopkg.in/yaml.v3.Node$"
funlen:
# Checks the number of lines in a function.
# If lower than 0, disable the check.
# Default: 60
lines: 100
# Checks the number of statements in a function.
# If lower than 0, disable the check.
# Default: 40
statements: 50
# Ignore comments when counting lines.
# Default false
ignore-comments: true
gocognit:
# Minimal code complexity to report.
# Default: 30 (but we recommend 10-20)
min-complexity: 20
gocritic:
# Settings passed to gocritic.
# The settings key is the name of a supported gocritic checker.
# The list of supported checkers can be find in https://go-critic.github.io/overview.
settings:
captLocal:
# Whether to restrict checker to params only.
# Default: true
paramsOnly: false
underef:
# Whether to skip (*x).method() calls where x is a pointer receiver.
# Default: true
skipRecvDeref: false
gomodguard:
blocked:
# List of blocked modules.
# Default: []
modules:
- github.com/golang/protobuf:
recommendations:
- google.golang.org/protobuf
reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules"
- github.com/satori/go.uuid:
recommendations:
- github.com/google/uuid
reason: "satori's package is not maintained"
- github.com/gofrs/uuid:
recommendations:
- github.com/gofrs/uuid/v5
reason: "gofrs' package was not go module before v5"
govet:
# Enable all analyzers.
# Default: false
enable-all: true
# Disable analyzers by name.
# Run `go tool vet help` to see all analyzers.
# Default: []
disable:
- fieldalignment # too strict
# Settings per analyzer.
settings:
shadow:
# Whether to be strict about shadowing; can be noisy.
# Default: false
strict: false
inamedparam:
# Skips check for interface methods with only a single parameter.
# Default: false
skip-single-param: true
mnd:
# List of function patterns to exclude from analysis.
# Values always ignored: `time.Date`,
# `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`,
# `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`.
# Default: []
ignored-functions:
- args.Error
- flag.Arg
- flag.Duration.*
- flag.Float.*
- flag.Int.*
- flag.Uint.*
- os.Chmod
- os.Mkdir.*
- os.OpenFile
- os.WriteFile
- prometheus.ExponentialBuckets.*
- prometheus.LinearBuckets
nakedret:
# Make an issue if func has more lines of code than this setting, and it has naked returns.
# Default: 30
max-func-lines: 0
nolintlint:
# Exclude following linters from requiring an explanation.
# Default: []
allow-no-explanation: [ funlen, gocognit, lll ]
# Enable to require an explanation of nonzero length after each nolint directive.
# Default: false
require-explanation: true
# Enable to require nolint directives to mention the specific linter being suppressed.
# Default: false
require-specific: true
perfsprint:
# Optimizes into strings concatenation.
# Default: true
strconcat: false
rowserrcheck:
# database/sql is always checked
# Default: []
packages:
- github.com/jmoiron/sqlx
sloglint:
# Enforce not using global loggers.
# Values:
# - "": disabled
# - "all": report all global loggers
# - "default": report only the default slog logger
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global
# Default: ""
no-global: "all"
# Enforce using methods that accept a context.
# Values:
# - "": disabled
# - "all": report all contextless calls
# - "scope": report only if a context exists in the scope of the outermost function
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#context-only
# Default: ""
context: "scope"
tenv:
# The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures.
# Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked.
# Default: false
all: true
issues:
# Maximum count of issues with the same text.
# Set to 0 to disable.
# Default: 3
max-same-issues: 50
exclude-rules:
- source: "(noinspection|TODO)"
linters: [ godot ]
- source: "//noinspection"
linters: [ gocritic ]
- path: "_test\\.go"
linters:
- bodyclose
- dupl
- funlen
- goconst
- gosec
- noctx
- wrapcheck
+3 -4
View File
@@ -5,8 +5,8 @@ version: 2
builds:
# Build configuration for darwin and linux
- id: default
ldflags:
- -s -w
ldflags: &ldflags
- -s -w -X github.com/gtsteffaniak/filebrowser/version.Version={{ .Version }} -X github.com/gtsteffaniak/filebrowser/version.CommitSHA={{ .ShortCommit }}
main: main.go
binary: filebrowser
goos:
@@ -25,8 +25,7 @@ builds:
# Build configuration for windows without arm
- id: windows
ldflags:
- -s -w
ldflags: *ldflags
main: main.go
binary: filebrowser
goos:
+1 -1
View File
@@ -187,7 +187,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
func (a *HookAuth) GetUser(d *users.User) *users.User {
// adds all permissions when user is admin
isAdmin := d.Perm.Admin
perms := users.Permissions{
perms := settings.Permissions{
Admin: isAdmin,
Execute: isAdmin || d.Perm.Execute,
Create: isAdmin || d.Perm.Create,
+16 -26
View File
@@ -5,49 +5,39 @@
? github.com/gtsteffaniak/filebrowser/auth [no test files]
? github.com/gtsteffaniak/filebrowser/cmd [no test files]
PASS
ok github.com/gtsteffaniak/filebrowser/diskcache 0.003s
ok github.com/gtsteffaniak/filebrowser/diskcache 0.004s
? github.com/gtsteffaniak/filebrowser/errors [no test files]
2024/10/07 12:46:34 could not update unknown type: unknown
goos: linux
goarch: amd64
pkg: github.com/gtsteffaniak/filebrowser/files
cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz
BenchmarkFillIndex-8 10 3587120 ns/op 273640 B/op 2013 allocs/op
BenchmarkSearchAllIndexes-8 10 31291180 ns/op 19500700 B/op 298636 allocs/op
BenchmarkFillIndex-8 10 3847878 ns/op 758424 B/op 5567 allocs/op
BenchmarkSearchAllIndexes-8 10 780431 ns/op 173444 B/op 2014 allocs/op
PASS
ok github.com/gtsteffaniak/filebrowser/files 0.408s
ok github.com/gtsteffaniak/filebrowser/files 0.073s
PASS
ok github.com/gtsteffaniak/filebrowser/fileutils 0.003s
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 h: 401 <nil>
2024/02/07 07:16:43 h: 401 <nil>
2024/02/07 07:16:43 h: 401 <nil>
2024/02/07 07:16:43 h: 401 <nil>
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 h: 401 <nil>
2024/02/07 07:16:43 h: 401 <nil>
2024/10/07 12:46:34 h: 401 <nil>
2024/10/07 12:46:34 h: 401 <nil>
2024/10/07 12:46:34 h: 401 <nil>
2024/10/07 12:46:34 h: 401 <nil>
2024/10/07 12:46:34 h: 401 <nil>
2024/10/07 12:46:34 h: 401 <nil>
PASS
ok github.com/gtsteffaniak/filebrowser/http 0.202s
ok github.com/gtsteffaniak/filebrowser/http 0.080s
PASS
ok github.com/gtsteffaniak/filebrowser/img 0.125s
ok github.com/gtsteffaniak/filebrowser/img 0.137s
PASS
ok github.com/gtsteffaniak/filebrowser/rules 0.002s
PASS
ok github.com/gtsteffaniak/filebrowser/runner 0.003s
PASS
ok github.com/gtsteffaniak/filebrowser/settings 0.005s
ok github.com/gtsteffaniak/filebrowser/settings 0.004s
? github.com/gtsteffaniak/filebrowser/share [no test files]
? github.com/gtsteffaniak/filebrowser/storage [no test files]
? github.com/gtsteffaniak/filebrowser/storage/bolt [no test files]
PASS
ok github.com/gtsteffaniak/filebrowser/users 0.003s
ok github.com/gtsteffaniak/filebrowser/users 0.002s
? github.com/gtsteffaniak/filebrowser/utils [no test files]
? github.com/gtsteffaniak/filebrowser/version [no test files]
+185 -122
View File
@@ -11,28 +11,28 @@ import (
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"embed"
"github.com/spf13/pflag"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/auth"
"github.com/gtsteffaniak/filebrowser/diskcache"
"github.com/gtsteffaniak/filebrowser/files"
fbhttp "github.com/gtsteffaniak/filebrowser/http"
"github.com/gtsteffaniak/filebrowser/img"
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/storage"
"github.com/gtsteffaniak/filebrowser/users"
"github.com/gtsteffaniak/filebrowser/utils"
"github.com/gtsteffaniak/filebrowser/version"
)
//go:embed dist/*
var assets embed.FS
var nonEmbededFS = os.Getenv("FILEBROWSER_NO_EMBEDED") == "true"
var (
nonEmbededFS = os.Getenv("FILEBROWSER_NO_EMBEDED") == "true"
)
type dirFS struct {
http.Dir
@@ -42,95 +42,119 @@ func (d dirFS) Open(name string) (fs.File, error) {
return d.Dir.Open(name)
}
func init() {
// Define a flag for the config option (-c or --config)
configFlag := pflag.StringP("config", "c", "filebrowser.yaml", "Path to the config file")
// Bind the flags to the pflag command line parser
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse()
log.Println("Initializing with config file:", *configFlag)
log.Println("Embeded Frontend:", !nonEmbededFS)
settings.Initialize(*configFlag)
func getStore(config string) (*storage.Storage, bool) {
// Use the config file (global flag)
log.Printf("Using Config file : %v", config)
settings.Initialize(config)
store, hasDB, err := storage.InitializeDb(settings.Config.Server.Database)
if err != nil {
log.Fatal("could not load db info: ", err)
}
return store, hasDB
}
var rootCmd = &cobra.Command{
Use: "filebrowser",
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
serverConfig := settings.Config.Server
if !d.hadDB {
quickSetup(d)
}
if serverConfig.NumImageProcessors < 1 {
log.Fatal("Image resize workers count could not be < 1")
}
imgSvc := img.New(serverConfig.NumImageProcessors)
var fileCache diskcache.Interface = diskcache.NewNoOp()
cacheDir := "/tmp"
if cacheDir != "" {
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet,gomnd
log.Fatalf("can't make directory %s: %s", cacheDir, err)
}
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
}
// initialize indexing and schedule indexing ever n minutes (default 5)
go files.InitializeIndex(serverConfig.IndexingInterval, serverConfig.Indexing)
_, err := os.Stat(serverConfig.Root)
checkErr(fmt.Sprint("cmd os.Stat ", serverConfig.Root), err)
var listener net.Listener
address := serverConfig.Address + ":" + strconv.Itoa(serverConfig.Port)
switch {
case serverConfig.Socket != "":
listener, err = net.Listen("unix", serverConfig.Socket)
checkErr("net.Listen", err)
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
checkErr("cmd.Flags().GetUint32", err)
err = os.Chmod(serverConfig.Socket, os.FileMode(socketPerm))
checkErr("os.Chmod", err)
case serverConfig.TLSKey != "" && serverConfig.TLSCert != "":
cer, err := tls.LoadX509KeyPair(serverConfig.TLSCert, serverConfig.TLSKey) //nolint:govet
checkErr("tls.LoadX509KeyPair", err)
listener, err = tls.Listen("tcp", address, &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cer}},
)
checkErr("tls.Listen", err)
default:
listener, err = net.Listen("tcp", address)
checkErr("net.Listen", err)
}
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
go cleanupHandler(listener, sigc)
if !nonEmbededFS {
assetsFs, err := fs.Sub(assets, "dist")
if err != nil {
log.Fatal("Could not embed frontend. Does backend/cmd/dist exist? Must be built and exist first")
}
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, &serverConfig, assetsFs)
checkErr("fbhttp.NewHandler", err)
defer listener.Close()
log.Println("Listening on", listener.Addr().String())
//nolint: gosec
if err := http.Serve(listener, handler); err != nil {
log.Fatalf("Could not start server on port %d: %v", serverConfig.Port, err)
}
} else {
assetsFs := dirFS{Dir: http.Dir("frontend/dist")}
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, &serverConfig, assetsFs)
checkErr("fbhttp.NewHandler", err)
defer listener.Close()
log.Println("Listening on", listener.Addr().String())
//nolint: gosec
if err := http.Serve(listener, handler); err != nil {
log.Fatalf("Could not start server on port %d: %v", serverConfig.Port, err)
}
}
}, pythonConfig{allowNoDB: true}),
func generalUsage() {
fmt.Printf(`usage: ./html-web-crawler <command> [options] --urls <urls>
commands:
collect Collect data from URLs
crawl Crawl URLs and collect data
install Install chrome browser for javascript enabled scraping.
Note: Consider instead to install via native package manager,
then set "CHROME_EXECUTABLE" in the environment
` + "\n")
}
func StartFilebrowser() {
if err := rootCmd.Execute(); err != nil {
// Global flags
var configPath string
var help bool
// Override the default usage output to use generalUsage()
flag.Usage = generalUsage
flag.StringVar(&configPath, "c", "filebrowser.yaml", "Path to the config file.")
flag.BoolVar(&help, "h", false, "Get help about commands")
// Parse global flags (before subcommands)
flag.Parse() // print generalUsage on error
// Show help if requested
if help {
generalUsage()
return
}
// Create a new FlagSet for the 'set' subcommand
setCmd := flag.NewFlagSet("set", flag.ExitOnError)
var user, scope, dbConfig string
var asAdmin bool
setCmd.StringVar(&user, "u", "", "Comma-separated username and password: \"set -u <username>,<password>\"")
setCmd.BoolVar(&asAdmin, "a", false, "Create user as admin user, used in combination with -u")
setCmd.StringVar(&scope, "s", "", "Specify a user scope, otherwise default user config scope is used")
setCmd.StringVar(&dbConfig, "c", "filebrowser.yaml", "Path to the config file.")
// Parse subcommand flags only if a subcommand is specified
if len(os.Args) > 1 {
switch os.Args[1] {
case "set":
err := setCmd.Parse(os.Args)
if err != nil {
setCmd.PrintDefaults()
os.Exit(1)
}
userInfo := strings.Split(user, ",")
if len(userInfo) < 2 {
fmt.Println("not enough info to create user: \"set -u username,password\"")
setCmd.PrintDefaults()
os.Exit(1)
}
username := userInfo[0]
password := userInfo[1]
getStore(dbConfig)
// Create the user logic
if asAdmin {
log.Printf("Creating user as admin: %s\n", username)
} else {
log.Printf("Creating user: %s\n", username)
}
newUser := users.User{
Username: username,
Password: password,
}
if scope != "" {
newUser.Scope = scope
}
err = storage.CreateUser(newUser, asAdmin)
if err != nil {
log.Fatal("Could not create user: ", err)
}
return
case "version":
fmt.Println("FileBrowser Quantum - A modern web-based file manager")
fmt.Printf("Version : %v\n", version.Version)
fmt.Printf("Commit : %v\n", version.CommitSHA)
fmt.Printf("Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v\n", version.Version)
return
}
}
store, dbExists := getStore(configPath)
indexingInterval := fmt.Sprint(settings.Config.Server.IndexingInterval, " minutes")
if !settings.Config.Server.Indexing {
indexingInterval = "disabled"
}
database := fmt.Sprintf("Using existing database : %v", settings.Config.Server.Database)
if !dbExists {
database = fmt.Sprintf("Creating new database : %v", settings.Config.Server.Database)
}
log.Printf("Initializing FileBrowser Quantum (%v)\n", version.Version)
log.Println("Embeded frontend :", !nonEmbededFS)
log.Println(database)
log.Println("Sources :", settings.Config.Server.Root)
log.Print("Indexing interval : ", indexingInterval)
serverConfig := settings.Config.Server
// initialize indexing and schedule indexing ever n minutes (default 5)
go files.InitializeIndex(serverConfig.IndexingInterval, serverConfig.Indexing)
if err := rootCMD(store, &serverConfig); err != nil {
log.Fatal("Error starting filebrowser:", err)
}
}
@@ -142,38 +166,77 @@ func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfac
os.Exit(0)
}
func quickSetup(d pythonData) {
settings.Config.Auth.Key = generateKey()
if settings.Config.Auth.Method == "noauth" {
err := d.store.Auth.Save(&auth.NoAuth{})
checkErr("d.store.Auth.Save", err)
func rootCMD(store *storage.Storage, serverConfig *settings.Server) error {
if serverConfig.NumImageProcessors < 1 {
log.Fatal("Image resize workers count could not be < 1")
}
imgSvc := img.New(serverConfig.NumImageProcessors)
cacheDir := "/tmp"
var fileCache diskcache.Interface
// Use file cache if cacheDir is specified
if cacheDir != "" {
var err error
fileCache, err = diskcache.NewFileCache(cacheDir)
if err != nil {
log.Fatalf("failed to create file cache: %v", err)
}
} else {
settings.Config.Auth.Method = "password"
err := d.store.Auth.Save(&auth.JSONAuth{})
checkErr("d.store.Auth.Save", err)
// No-op cache if no cacheDir is specified
fileCache = diskcache.NewNoOp()
}
err := d.store.Settings.Save(&settings.Config)
checkErr("d.store.Settings.Save", err)
err = d.store.Settings.SaveServer(&settings.Config.Server)
checkErr("d.store.Settings.SaveServer", err)
user := &users.User{}
settings.Config.UserDefaults.Apply(user)
user.Username = settings.Config.Auth.AdminUsername
user.Password = settings.Config.Auth.AdminPassword
user.Perm.Admin = true
user.Scope = "./"
user.DarkMode = true
user.ViewMode = "normal"
user.LockPassword = false
user.Perm = users.Permissions{
Create: true,
Rename: true,
Modify: true,
Delete: true,
Share: true,
Download: true,
Admin: true,
fbhttp.SetupEnv(store, serverConfig, fileCache)
_, err := os.Stat(serverConfig.Root)
utils.CheckErr(fmt.Sprint("cmd os.Stat ", serverConfig.Root), err)
var listener net.Listener
address := serverConfig.Address + ":" + strconv.Itoa(serverConfig.Port)
switch {
case serverConfig.Socket != "":
listener, err = net.Listen("unix", serverConfig.Socket)
utils.CheckErr("net.Listen", err)
err = os.Chmod(serverConfig.Socket, os.FileMode(0666)) // socket-perm
utils.CheckErr("os.Chmod", err)
case serverConfig.TLSKey != "" && serverConfig.TLSCert != "":
cer, err := tls.LoadX509KeyPair(serverConfig.TLSCert, serverConfig.TLSKey) //nolint:govet
utils.CheckErr("tls.LoadX509KeyPair", err)
listener, err = tls.Listen("tcp", address, &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cer}},
)
utils.CheckErr("tls.Listen", err)
default:
listener, err = net.Listen("tcp", address)
utils.CheckErr("net.Listen", err)
}
err = d.store.Users.Save(user)
checkErr("d.store.Users.Save", err)
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
go cleanupHandler(listener, sigc)
if !nonEmbededFS {
assetsFs, err := fs.Sub(assets, "dist")
if err != nil {
log.Fatal("Could not embed frontend. Does backend/cmd/dist exist? Must be built and exist first")
}
handler, err := fbhttp.NewHandler(imgSvc, assetsFs)
utils.CheckErr("fbhttp.NewHandler", err)
defer listener.Close()
log.Println("Listening on", listener.Addr().String())
//nolint: gosec
if err := http.Serve(listener, handler); err != nil {
log.Fatalf("Could not start server on port %d: %v", serverConfig.Port, err)
}
} else {
assetsFs := dirFS{Dir: http.Dir("frontend/dist")}
handler, err := fbhttp.NewHandler(imgSvc, assetsFs)
utils.CheckErr("fbhttp.NewHandler", err)
defer listener.Close()
log.Println("Listening on", listener.Addr().String())
//nolint: gosec
if err := http.Serve(listener, handler); err != nil {
log.Fatalf("Could not start server on port %d: %v", serverConfig.Port, err)
}
}
return nil
}
+11 -9
View File
@@ -6,7 +6,9 @@ import (
"github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/storage"
"github.com/gtsteffaniak/filebrowser/users"
"github.com/gtsteffaniak/filebrowser/utils"
)
func init() {
@@ -40,27 +42,27 @@ including 'index_end'.`,
return nil
},
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
i, err := strconv.Atoi(args[0])
checkErr("strconv.Atoi", err)
utils.CheckErr("strconv.Atoi", err)
f := i
if len(args) == 2 { //nolint:gomnd
f, err = strconv.Atoi(args[1])
checkErr("strconv.Atoi", err)
utils.CheckErr("strconv.Atoi", err)
}
user := func(u *users.User) {
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
err := d.store.Users.Save(u)
checkErr("d.store.Users.Save", err)
err := store.Users.Save(u)
utils.CheckErr("store.Users.Save", err)
}
global := func(s *settings.Settings) {
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
err := d.store.Settings.Save(s)
checkErr("d.store.Settings.Save", err)
err := store.Settings.Save(s)
utils.CheckErr("store.Settings.Save", err)
}
runRules(d.store, cmd, user, global)
}, pythonConfig{}),
runRules(store, cmd, user, global)
}),
}
+3 -8
View File
@@ -10,10 +10,10 @@ import (
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/storage"
"github.com/gtsteffaniak/filebrowser/users"
"github.com/gtsteffaniak/filebrowser/utils"
)
func init() {
rootCmd.AddCommand(rulesCmd)
rulesCmd.PersistentFlags().StringP("username", "u", "", "username of user to which the rules apply")
rulesCmd.PersistentFlags().UintP("id", "i", 0, "id of user to which the rules apply")
}
@@ -33,7 +33,7 @@ func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User)
id := getUserIdentifier(cmd.Flags())
if id != nil {
user, err := st.Users.Get("", id)
checkErr("st.Users.Get", err)
utils.CheckErr("st.Users.Get", err)
if usersFn != nil {
usersFn(user)
@@ -44,7 +44,7 @@ func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User)
}
s, err := st.Settings.Get()
checkErr("st.Settings.Get", err)
utils.CheckErr("st.Settings.Get", err)
if globalFn != nil {
globalFn(s)
@@ -67,11 +67,6 @@ func getUserIdentifier(flags *pflag.FlagSet) interface{} {
}
func printRules(rulez []rules.Rule, id interface{}) {
if id == nil {
fmt.Printf("Global Rules:\n\n")
} else {
fmt.Printf("Rules for user %v:\n\n", id)
}
for id, rule := range rulez {
fmt.Printf("(%d) ", id)
+9 -7
View File
@@ -7,7 +7,9 @@ import (
"github.com/gtsteffaniak/filebrowser/rules"
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/storage"
"github.com/gtsteffaniak/filebrowser/users"
"github.com/gtsteffaniak/filebrowser/utils"
)
func init() {
@@ -21,7 +23,7 @@ var rulesAddCmd = &cobra.Command{
Short: "Add a global rule or user rule",
Long: `Add a global rule or user rule.`,
Args: cobra.ExactArgs(1),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
allow := mustGetBool(cmd.Flags(), "allow")
regex := mustGetBool(cmd.Flags(), "regex")
exp := args[0]
@@ -43,16 +45,16 @@ var rulesAddCmd = &cobra.Command{
user := func(u *users.User) {
u.Rules = append(u.Rules, rule)
err := d.store.Users.Save(u)
checkErr("d.store.Users.Save", err)
err := store.Users.Save(u)
utils.CheckErr("store.Users.Save", err)
}
global := func(s *settings.Settings) {
s.Rules = append(s.Rules, rule)
err := d.store.Settings.Save(s)
checkErr("d.store.Settings.Save", err)
err := store.Settings.Save(s)
utils.CheckErr("store.Settings.Save", err)
}
runRules(d.store, cmd, user, global)
}, pythonConfig{}),
runRules(store, cmd, user, global)
}),
}
+4 -3
View File
@@ -1,6 +1,7 @@
package cmd
import (
"github.com/gtsteffaniak/filebrowser/storage"
"github.com/spf13/cobra"
)
@@ -13,7 +14,7 @@ var rulesLsCommand = &cobra.Command{
Short: "List global rules or user specific rules",
Long: `List global rules or user specific rules.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
runRules(d.store, cmd, nil, nil)
}, pythonConfig{}),
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
runRules(store, cmd, nil, nil)
}),
}
-4
View File
@@ -11,10 +11,6 @@ import (
"github.com/gtsteffaniak/filebrowser/users"
)
func init() {
rootCmd.AddCommand(usersCmd)
}
var usersCmd = &cobra.Command{
Use: "users",
Short: "Users management utility",
+11 -9
View File
@@ -3,7 +3,9 @@ package cmd
import (
"github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/storage"
"github.com/gtsteffaniak/filebrowser/users"
"github.com/gtsteffaniak/filebrowser/utils"
)
func init() {
@@ -15,26 +17,26 @@ var usersAddCmd = &cobra.Command{
Short: "Create a new user",
Long: `Create a new user and add it to the database.`,
Args: cobra.ExactArgs(2), //nolint:gomnd
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
user := &users.User{
Username: args[0],
Password: args[1],
LockPassword: mustGetBool(cmd.Flags(), "lockPassword"),
}
servSettings, err := d.store.Settings.GetServer()
checkErr("d.store.Settings.GetServer()", err)
servSettings, err := store.Settings.GetServer()
utils.CheckErr("store.Settings.GetServer()", err)
// since getUserDefaults() polluted s.Defaults.Scope
// which makes the Scope not the one saved in the db
// we need the right s.Defaults.Scope here
s2, err := d.store.Settings.Get()
checkErr("d.store.Settings.Get()", err)
s2, err := store.Settings.Get()
utils.CheckErr("store.Settings.Get()", err)
userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root)
checkErr("s2.MakeUserDir", err)
utils.CheckErr("s2.MakeUserDir", err)
user.Scope = userHome
err = d.store.Users.Save(user)
checkErr("d.store.Users.Save", err)
err = store.Users.Save(user)
utils.CheckErr("store.Users.Save", err)
printUsers([]*users.User{user})
}, pythonConfig{}),
}),
}
+7 -5
View File
@@ -1,6 +1,8 @@
package cmd
import (
"github.com/gtsteffaniak/filebrowser/storage"
"github.com/gtsteffaniak/filebrowser/utils"
"github.com/spf13/cobra"
)
@@ -14,11 +16,11 @@ var usersExportCmd = &cobra.Command{
Long: `Export all users to a json or yaml file. Please indicate the
path to the file where you want to write the users.`,
Args: jsonYamlArg,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
list, err := d.store.Users.Gets("")
checkErr("d.store.Users.Gets", err)
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
list, err := store.Users.Gets("")
utils.CheckErr("store.Users.Gets", err)
err = marshal(args[0], list)
checkErr("marshal", err)
}, pythonConfig{}),
utils.CheckErr("marshal", err)
}),
}
+8 -6
View File
@@ -3,7 +3,9 @@ package cmd
import (
"github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/storage"
"github.com/gtsteffaniak/filebrowser/users"
"github.com/gtsteffaniak/filebrowser/utils"
)
func init() {
@@ -26,7 +28,7 @@ var usersLsCmd = &cobra.Command{
Run: findUsers,
}
var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) {
var findUsers = cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
var (
list []*users.User
user *users.User
@@ -36,16 +38,16 @@ var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) {
if len(args) == 1 {
username, id := parseUsernameOrID(args[0])
if username != "" {
user, err = d.store.Users.Get("", username)
user, err = store.Users.Get("", username)
} else {
user, err = d.store.Users.Get("", id)
user, err = store.Users.Get("", id)
}
list = []*users.User{user}
} else {
list, err = d.store.Users.Gets("")
list, err = store.Users.Gets("")
}
checkErr("findUsers", err)
utils.CheckErr("findUsers", err)
printUsers(list)
}, pythonConfig{})
})
+17 -20
View File
@@ -8,7 +8,9 @@ import (
"github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/storage"
"github.com/gtsteffaniak/filebrowser/users"
"github.com/gtsteffaniak/filebrowser/utils"
)
func init() {
@@ -25,52 +27,47 @@ file. You can use this command to import new users to your
installation. For that, just don't place their ID on the files
list or set it to 0.`,
Args: jsonYamlArg,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
fd, err := os.Open(args[0])
checkErr("os.Open", err)
utils.CheckErr("os.Open", err)
defer fd.Close()
list := []*users.User{}
err = unmarshal(args[0], &list)
checkErr("unmarshal", err)
for _, user := range list {
err = user.Clean("")
checkErr("Clean", err)
}
utils.CheckErr("unmarshal", err)
if mustGetBool(cmd.Flags(), "replace") {
oldUsers, err := d.store.Users.Gets("")
checkErr("d.store.Users.Gets", err)
oldUsers, err := store.Users.Gets("")
utils.CheckErr("store.Users.Gets", err)
err = marshal("users.backup.json", list)
checkErr("marshal users.backup.json", err)
utils.CheckErr("marshal users.backup.json", err)
for _, user := range oldUsers {
err = d.store.Users.Delete(user.ID)
checkErr("d.store.Users.Delete", err)
err = store.Users.Delete(user.ID)
utils.CheckErr("store.Users.Delete", err)
}
}
overwrite := mustGetBool(cmd.Flags(), "overwrite")
for _, user := range list {
onDB, err := d.store.Users.Get("", user.ID)
onDB, err := store.Users.Get("", user.ID)
// User exists in DB.
if err == nil {
if !overwrite {
newErr := errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered")
checkErr("", newErr)
utils.CheckErr("", newErr)
}
// If the usernames mismatch, check if there is another one in the DB
// with the new username. If there is, print an error and cancel the
// operation
if user.Username != onDB.Username {
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
if conflictuous, err := store.Users.Get("", user.Username); err == nil { //nolint:govet
newErr := usernameConflictError(user.Username, conflictuous.ID, user.ID)
checkErr("usernameConflictError", newErr)
utils.CheckErr("usernameConflictError", newErr)
}
}
} else {
@@ -79,10 +76,10 @@ list or set it to 0.`,
user.ID = 0
}
err = d.store.Users.Save(user)
checkErr("d.store.Users.Save", err)
err = store.Users.Save(user)
utils.CheckErr("store.Users.Save", err)
}
}, pythonConfig{}),
}),
}
func usernameConflictError(username string, originalID, newID uint) error {
+9 -7
View File
@@ -1,8 +1,10 @@
package cmd
import (
"fmt"
"log"
"github.com/gtsteffaniak/filebrowser/storage"
"github.com/gtsteffaniak/filebrowser/utils"
"github.com/spf13/cobra"
)
@@ -15,17 +17,17 @@ var usersRmCmd = &cobra.Command{
Short: "Delete a user by username or id",
Long: `Delete a user by username or id`,
Args: cobra.ExactArgs(1),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
username, id := parseUsernameOrID(args[0])
var err error
if username != "" {
err = d.store.Users.Delete(username)
err = store.Users.Delete(username)
} else {
err = d.store.Users.Delete(id)
err = store.Users.Delete(id)
}
checkErr("usersRmCmd", err)
fmt.Println("user deleted successfully")
}, pythonConfig{}),
utils.CheckErr("usersRmCmd", err)
log.Println("user deleted successfully")
}),
}
+9 -7
View File
@@ -3,7 +3,9 @@ package cmd
import (
"github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/storage"
"github.com/gtsteffaniak/filebrowser/users"
"github.com/gtsteffaniak/filebrowser/utils"
)
func init() {
@@ -16,7 +18,7 @@ var usersUpdateCmd = &cobra.Command{
Long: `Updates an existing user. Set the flags for the
options you want to change.`,
Args: cobra.ExactArgs(1),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
Run: cobraCmd(func(cmd *cobra.Command, args []string, store *storage.Storage) {
username, id := parseUsernameOrID(args[0])
var (
@@ -25,14 +27,14 @@ options you want to change.`,
)
if id != 0 {
user, err = d.store.Users.Get("", id)
user, err = store.Users.Get("", id)
} else {
user, err = d.store.Users.Get("", username)
user, err = store.Users.Get("", username)
}
checkErr("d.store.Users.Get", err)
utils.CheckErr("store.Users.Get", err)
err = d.store.Users.Update(user)
checkErr("d.store.Users.Update", err)
err = store.Users.Update(user)
utils.CheckErr("store.Users.Update", err)
printUsers([]*users.User{user})
}, pythonConfig{}),
}),
}
+12 -78
View File
@@ -3,113 +3,42 @@ package cmd
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"github.com/asdine/storm/v3"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/storage"
"github.com/gtsteffaniak/filebrowser/storage/bolt"
"github.com/gtsteffaniak/filebrowser/utils"
)
func checkErr(source string, err error) {
if err != nil {
log.Fatalf("%s: %v", source, err)
}
}
func mustGetString(flags *pflag.FlagSet, flag string) string {
s, err := flags.GetString(flag)
checkErr("mustGetString", err)
utils.CheckErr("mustGetString", err)
return s
}
func mustGetBool(flags *pflag.FlagSet, flag string) bool {
b, err := flags.GetBool(flag)
checkErr("mustGetBool", err)
utils.CheckErr("mustGetBool", err)
return b
}
func mustGetUint(flags *pflag.FlagSet, flag string) uint {
b, err := flags.GetUint(flag)
checkErr("mustGetUint", err)
utils.CheckErr("mustGetUint", err)
return b
}
func generateKey() []byte {
k, err := settings.GenerateKey()
checkErr("generateKey", err)
return k
}
type cobraFunc func(cmd *cobra.Command, args []string)
type pythonFunc func(cmd *cobra.Command, args []string, data pythonData)
type pythonConfig struct {
noDB bool
allowNoDB bool
}
type pythonData struct {
hadDB bool
store *storage.Storage
}
func dbExists(path string) (bool, error) {
stat, err := os.Stat(path)
if err == nil {
return stat.Size() != 0, nil
}
if os.IsNotExist(err) {
d := filepath.Dir(path)
_, err = os.Stat(d)
if os.IsNotExist(err) {
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet,gomnd
return false, err
}
return false, nil
}
}
return false, err
}
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
return func(cmd *cobra.Command, args []string) {
data := pythonData{hadDB: true}
path := settings.Config.Server.Database
exists, err := dbExists(path)
if err != nil {
panic(err)
} else if exists && cfg.noDB {
log.Fatal(path + " already exists")
} else if !exists && !cfg.noDB && !cfg.allowNoDB {
log.Fatal(path + " does not exist. Please run 'filebrowser config init' first.")
}
data.hadDB = exists
db, err := storm.Open(path)
checkErr(fmt.Sprintf("storm.Open path %v", path), err)
defer db.Close()
data.store, err = bolt.NewStorage(db)
checkErr("bolt.NewStorage", err)
fn(cmd, args, data)
}
}
type pythonFunc func(cmd *cobra.Command, args []string, store *storage.Storage)
func marshal(filename string, data interface{}) error {
fd, err := os.Create(filename)
checkErr("os.Create", err)
utils.CheckErr("os.Create", err)
defer fd.Close()
switch ext := filepath.Ext(filename); ext {
@@ -127,7 +56,7 @@ func marshal(filename string, data interface{}) error {
func unmarshal(filename string, data interface{}) error {
fd, err := os.Open(filename)
checkErr("os.Open", err)
utils.CheckErr("os.Open", err)
defer fd.Close()
switch ext := filepath.Ext(filename); ext {
@@ -152,3 +81,8 @@ func jsonYamlArg(cmd *cobra.Command, args []string) error {
return errors.New("invalid format: " + ext)
}
}
func cobraCmd(fn pythonFunc) cobraFunc {
return func(cmd *cobra.Command, args []string) {
}
}
-21
View File
@@ -1,21 +0,0 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/version"
)
func init() {
rootCmd.AddCommand(versionCmd)
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA)
},
}
+19 -13
View File
@@ -10,13 +10,17 @@ import (
"os"
"path/filepath"
"sync"
"github.com/spf13/afero"
)
type FileCache struct {
fs afero.Fs
// Cache interface for caching operations
type Cache interface {
Get(key string) ([]byte, error)
Set(key string, value []byte) error
}
// FileCache struct for file-based caching
type FileCache struct {
dir string
// granular locks
scopedLocks struct {
sync.Mutex
@@ -25,10 +29,12 @@ type FileCache struct {
}
}
func New(fs afero.Fs, root string) *FileCache {
return &FileCache{
fs: afero.NewBasePathFs(fs, root),
// NewFileCache creates a new FileCache
func NewFileCache(dir string) (*FileCache, error) {
if err := os.MkdirAll(dir, 0700); err != nil {
return nil, fmt.Errorf("can't make directory %s: %v", dir, err)
}
return &FileCache{dir: dir}, nil
}
func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
@@ -37,11 +43,11 @@ func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
defer mu.Unlock()
fileName := f.getFileName(key)
if err := f.fs.MkdirAll(filepath.Dir(fileName), 0700); err != nil { //nolint:gomnd
if err := os.MkdirAll(filepath.Dir(fileName), 0700); err != nil {
return err
}
if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil { //nolint:gomnd
if err := os.WriteFile(fileName, value, 0600); err != nil {
return err
}
@@ -68,15 +74,15 @@ func (f *FileCache) Delete(ctx context.Context, key string) error {
defer mu.Unlock()
fileName := f.getFileName(key)
if err := f.fs.Remove(fileName); err != nil && !errors.Is(err, os.ErrNotExist) {
if err := os.Remove(fileName); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
return nil
}
func (f *FileCache) open(key string) (afero.File, bool, error) {
func (f *FileCache) open(key string) (*os.File, bool, error) {
fileName := f.getFileName(key)
file, err := f.fs.Open(fileName)
file, err := os.Open(fileName)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, false, nil
@@ -106,5 +112,5 @@ func (f *FileCache) getFileName(key string) string {
hasher := sha1.New() //nolint:gosec
_, _ = hasher.Write([]byte(key))
hash := hex.EncodeToString(hasher.Sum(nil))
return fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash)
return filepath.Join(f.dir, fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash))
}
+23 -11
View File
@@ -2,10 +2,10 @@ package diskcache
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
)
@@ -15,35 +15,39 @@ func TestFileCache(t *testing.T) {
key = "key"
value = "some text"
newValue = "new text"
cacheRoot = "/cache"
cacheRoot = "cache"
cachedFilePath = "a/62/a62f2225bf70bfaccbc7f1ef2a397836717377de"
)
fs := afero.NewMemMapFs()
cache := New(fs, "/cache")
// Create temporary directory for the cache
cacheDir, err := os.MkdirTemp("", cacheRoot)
require.NoError(t, err)
defer os.RemoveAll(cacheDir) // Clean up
cache, err := NewFileCache(cacheDir)
require.NoError(t, err)
// store new key
err := cache.Store(ctx, key, []byte(value))
err = cache.Store(ctx, key, []byte(value))
require.NoError(t, err)
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, value)
checkValue(t, ctx, cache, filepath.Join(cacheDir, cachedFilePath), key, value)
// update existing key
err = cache.Store(ctx, key, []byte(newValue))
require.NoError(t, err)
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, newValue)
checkValue(t, ctx, cache, filepath.Join(cacheDir, cachedFilePath), key, newValue)
// delete key
err = cache.Delete(ctx, key)
require.NoError(t, err)
exists, err := afero.Exists(fs, filepath.Join(cacheRoot, cachedFilePath))
require.NoError(t, err)
exists := fileExists(filepath.Join(cacheDir, cachedFilePath))
require.False(t, exists)
}
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:golint
func checkValue(t *testing.T, ctx context.Context, cache *FileCache, fileFullPath string, key, wantValue string) {
t.Helper()
// check actual file content
b, err := afero.ReadFile(fs, fileFullPath)
b, err := os.ReadFile(fileFullPath)
require.NoError(t, err)
require.Equal(t, wantValue, string(b))
@@ -53,3 +57,11 @@ func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath str
require.True(t, ok)
require.Equal(t, wantValue, string(b))
}
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
+1 -1
View File
@@ -91,7 +91,7 @@ func ParseSearch(value string) *SearchOptions {
opts.LargerThan = updateSize(size)
}
if strings.HasPrefix(filter, "smallerThan=") {
opts.Conditions["larger"] = true
opts.Conditions["smaller"] = true
size := strings.TrimPrefix(filter, "smallerThan=")
opts.SmallerThan = updateSize(size)
}
+154
View File
@@ -0,0 +1,154 @@
package files
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
// Helper function to create error messages dynamically
func errorMsg(extension, expectedType string, expectedMatch bool) string {
matchStatus := "to match"
if !expectedMatch {
matchStatus = "to not match"
}
return fmt.Sprintf("Expected %s %s type '%s'", extension, matchStatus, expectedType)
}
func TestIsMatchingType(t *testing.T) {
// Test cases where IsMatchingType should return true
trueTestCases := []struct {
extension string
expectedType string
}{
{".pdf", "pdf"},
{".doc", "doc"},
{".docx", "doc"},
{".json", "text"},
{".sh", "text"},
{".zip", "archive"},
{".rar", "archive"},
}
for _, tc := range trueTestCases {
assert.True(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, true))
}
// Test cases where IsMatchingType should return false
falseTestCases := []struct {
extension string
expectedType string
}{
{".mp4", "doc"},
{".mp4", "text"},
{".mp4", "archive"},
}
for _, tc := range falseTestCases {
assert.False(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, false))
}
}
func TestUpdateSize(t *testing.T) {
// Helper function for size error messages
sizeErrorMsg := func(input string, expected, actual int) string {
return fmt.Sprintf("Expected size for input '%s' to be %d, got %d", input, expected, actual)
}
// Test cases for updateSize
testCases := []struct {
input string
expected int
}{
{"150", 150},
{"invalid", 100},
{"", 100},
}
for _, tc := range testCases {
actual := updateSize(tc.input)
assert.Equal(t, tc.expected, actual, sizeErrorMsg(tc.input, tc.expected, actual))
}
}
func TestIsDoc(t *testing.T) {
// Test cases where IsMatchingType should return true for document types
docTrueTestCases := []struct {
extension string
expectedType string
}{
{".doc", "doc"},
{".pdf", "doc"},
}
for _, tc := range docTrueTestCases {
assert.True(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, true))
}
// Test case where IsMatchingType should return false for document types
docFalseTestCases := []struct {
extension string
expectedType string
}{
{".mp4", "doc"},
}
for _, tc := range docFalseTestCases {
assert.False(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, false))
}
}
func TestIsText(t *testing.T) {
// Test cases where IsMatchingType should return true for text types
textTrueTestCases := []struct {
extension string
expectedType string
}{
{".json", "text"},
{".sh", "text"},
}
for _, tc := range textTrueTestCases {
assert.True(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, true))
}
// Test case where IsMatchingType should return false for text types
textFalseTestCases := []struct {
extension string
expectedType string
}{
{".mp4", "text"},
}
for _, tc := range textFalseTestCases {
assert.False(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, false))
}
}
func TestIsArchive(t *testing.T) {
// Test cases where IsMatchingType should return true for archive types
archiveTrueTestCases := []struct {
extension string
expectedType string
}{
{".zip", "archive"},
{".rar", "archive"},
}
for _, tc := range archiveTrueTestCases {
assert.True(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, true))
}
// Test case where IsMatchingType should return false for archive types
archiveFalseTestCases := []struct {
extension string
expectedType string
}{
{".mp4", "archive"},
}
for _, tc := range archiveFalseTestCases {
assert.False(t, IsMatchingType(tc.extension, tc.expectedType), errorMsg(tc.extension, tc.expectedType, false))
}
}
+260 -161
View File
@@ -1,59 +1,68 @@
package files
import (
"crypto/md5" //nolint:gosec
"crypto/sha1" //nolint:gosec
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"fmt"
"hash"
"io"
"mime"
"net/http"
"os"
filepath "path/filepath"
"path/filepath"
"strings"
"sync"
"time"
"unicode/utf8"
"github.com/spf13/afero"
"github.com/gtsteffaniak/filebrowser/errors"
"github.com/gtsteffaniak/filebrowser/rules"
"github.com/gtsteffaniak/filebrowser/users"
"github.com/gtsteffaniak/filebrowser/settings"
)
var (
bytesInMegabyte int64 = 1000000
pathMutexes = make(map[string]*sync.Mutex)
pathMutexesMu sync.Mutex // Mutex to protect the pathMutexes map
pathMutexes = make(map[string]*sync.Mutex)
pathMutexesMu sync.Mutex // Mutex to protect the pathMutexes map
)
type ReducedItem struct {
Name string `json:"name"`
Size int64 `json:"size"`
ModTime time.Time `json:"modified"`
IsDir bool `json:"isDir,omitempty"`
Type string `json:"type"`
}
// FileInfo describes a file.
// reduced item is non-recursive reduced "Items", used to pass flat items array
type FileInfo struct {
*Listing
Fs afero.Fs `json:"-"`
Path string `json:"path,omitempty"`
Name string `json:"name"`
Size int64 `json:"size"`
Extension string `json:"-"`
ModTime time.Time `json:"modified"`
CacheTime time.Time `json:"-"`
Mode os.FileMode `json:"-"`
IsDir bool `json:"isDir,omitempty"`
IsSymlink bool `json:"isSymlink,omitempty"`
Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"`
Token string `json:"token,omitempty"`
Items []*FileInfo `json:"-"`
ReducedItems []ReducedItem `json:"items,omitempty"`
Path string `json:"path,omitempty"`
Name string `json:"name"`
Size int64 `json:"size"`
Extension string `json:"-"`
ModTime time.Time `json:"modified"`
CacheTime time.Time `json:"-"`
Mode os.FileMode `json:"-"`
IsDir bool `json:"isDir,omitempty"`
IsSymlink bool `json:"isSymlink,omitempty"`
Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"`
Token string `json:"token,omitempty"`
NumDirs int `json:"numDirs"`
NumFiles int `json:"numFiles"`
}
// FileOptions are the options when getting a file info.
type FileOptions struct {
Fs afero.Fs
Path string
Path string // realpath
IsDir bool
Modify bool
Expand bool
ReadHeader bool
@@ -62,38 +71,43 @@ type FileOptions struct {
Content bool
}
// Sorting constants
const (
SortingByName = "name"
SortingBySize = "size"
SortingByModified = "modified"
)
// Listing is a collection of files.
type Listing struct {
Items []*FileInfo `json:"items"`
Path string `json:"path"`
NumDirs int `json:"numDirs"`
NumFiles int `json:"numFiles"`
Sorting users.Sorting `json:"sorting"`
}
// NewFileInfo creates a File object from a path and a given user. This File
// object will be automatically filled depending on if it is a directory
// or a file. If it's a video file, it will also detect any subtitles.
// Legacy file info method, only called on non-indexed directories.
// Once indexing completes for the first time, NewFileInfo is never called.
func NewFileInfo(opts FileOptions) (*FileInfo, error) {
index := GetIndex(rootPath)
if !opts.Checker.Check(opts.Path) {
return nil, os.ErrPermission
}
file, err := stat(opts.Path, opts) // Pass opts.Path here
file, err := stat(opts)
if err != nil {
return nil, err
}
if opts.Expand {
if file.IsDir {
if err := file.readListing(opts.Path, opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
if err = file.readListing(opts.Path, opts.Checker, opts.ReadHeader); err != nil {
return nil, err
}
cleanedItems := []ReducedItem{}
for _, item := range file.Items {
// This is particularly useful for root of index, while indexing hasn't finished.
// adds the directory sizes for directories that have been indexed already.
if item.IsDir {
adjustedPath := index.makeIndexPath(opts.Path+"/"+item.Name, true)
info, _ := index.GetMetadataInfo(adjustedPath)
item.Size = info.Size
}
cleanedItems = append(cleanedItems, ReducedItem{
Name: item.Name,
Size: item.Size,
IsDir: item.IsDir,
ModTime: item.ModTime,
Type: item.Type,
})
}
file.Items = nil
file.ReducedItems = cleanedItems
return file, nil
}
err = file.detectType(opts.Path, opts.Modify, opts.Content, true)
@@ -113,113 +127,82 @@ func FileInfoFaster(opts FileOptions) (*FileInfo, error) {
return nil, os.ErrPermission
}
index := GetIndex(rootPath)
trimmed := strings.TrimPrefix(opts.Path, "/")
if trimmed == "" {
trimmed = "/"
adjustedPath := index.makeIndexPath(opts.Path, opts.IsDir)
if opts.IsDir {
info, exists := index.GetMetadataInfo(adjustedPath)
if exists && !opts.Content {
// Let's not refresh if less than a second has passed
if time.Since(info.CacheTime) > time.Second {
go RefreshFileInfo(opts) //nolint:errcheck
}
// refresh cache after
return &info, nil
}
}
adjustedPath := makeIndexPath(trimmed, index.Root)
var info FileInfo
info, exists := index.GetMetadataInfo(adjustedPath)
if exists && !opts.Content {
// Check if the cache time is less than 1 second
if time.Since(info.CacheTime) > time.Second {
go RefreshFileInfo(opts)
}
// refresh cache after
return &info, nil
} else {
// don't bother caching content
if opts.Content {
file, err := NewFileInfo(opts)
return file, err
}
updated := RefreshFileInfo(opts)
if !updated {
file, err := NewFileInfo(opts)
return file, err
}
info, exists = index.GetMetadataInfo(adjustedPath)
if !exists || info.Name == "" {
return &FileInfo{}, errors.ErrEmptyKey
}
return &info, nil
// don't bother caching content
if opts.Content {
file, err := NewFileInfo(opts)
return file, err
}
err := RefreshFileInfo(opts)
if err != nil {
file, err := NewFileInfo(opts)
return file, err
}
info, exists := index.GetMetadataInfo(adjustedPath + "/" + filepath.Base(opts.Path))
if !exists || info.Name == "" {
return NewFileInfo(opts)
}
return &info, nil
}
func RefreshFileInfo(opts FileOptions) bool {
func RefreshFileInfo(opts FileOptions) error {
if !opts.Checker.Check(opts.Path) {
return false
return fmt.Errorf("permission denied: %s", opts.Path)
}
index := GetIndex(rootPath)
trimmed := strings.TrimPrefix(opts.Path, "/")
if trimmed == "" {
trimmed = "/"
}
adjustedPath := makeIndexPath(trimmed, index.Root)
file, err := stat(opts.Path, opts) // Pass opts.Path here
adjustedPath := index.makeIndexPath(opts.Path, opts.IsDir)
file, err := stat(opts)
if err != nil {
return false
return fmt.Errorf("File/folder does not exist to refresh data: %s", opts.Path)
}
_ = file.detectType(adjustedPath, true, opts.Content, opts.ReadHeader)
_ = file.detectType(opts.Path, true, opts.Content, opts.ReadHeader)
if file.IsDir {
err := file.readListing(opts.Path, opts.Checker, opts.ReadHeader)
if err != nil {
return false
return fmt.Errorf("Dir info could not be read: %s", opts.Path)
}
//_, exists := index.GetFileMetadata(adjustedPath)
return index.UpdateFileMetadata(adjustedPath, *file)
} else {
//_, exists := index.GetFileMetadata(adjustedPath)
return index.UpdateFileMetadata(adjustedPath, *file)
}
result := index.UpdateFileMetadata(adjustedPath, *file)
if !result {
return fmt.Errorf("File/folder does not exist in metadata: %s", adjustedPath)
}
return nil
}
func stat(path string, opts FileOptions) (*FileInfo, error) {
var file *FileInfo
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
info, _, err := lstaterFs.LstatIfPossible(path)
if err == nil {
file = &FileInfo{
Fs: opts.Fs,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
Token: opts.Token,
}
if info.IsDir() {
file.IsDir = true
}
if info.Mode()&os.ModeSymlink != 0 {
file.IsSymlink = true
}
}
func stat(opts FileOptions) (*FileInfo, error) {
info, err := os.Lstat(opts.Path)
if err != nil {
return nil, err
}
if file == nil || file.IsSymlink {
info, err := opts.Fs.Stat(opts.Path)
if err != nil {
return nil, err
}
if file != nil && file.IsSymlink {
file.Size = info.Size()
file.IsDir = info.IsDir()
return file, nil
}
file = &FileInfo{
Fs: opts.Fs,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
Token: opts.Token,
file := &FileInfo{
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
Token: opts.Token,
}
if info.IsDir() {
file.IsDir = true
}
if info.Mode()&os.ModeSymlink != 0 {
file.IsSymlink = true
targetInfo, err := os.Stat(opts.Path)
if err == nil {
file.Size = targetInfo.Size()
file.IsDir = targetInfo.IsDir()
}
}
@@ -237,7 +220,7 @@ func (i *FileInfo) Checksum(algo string) error {
i.Checksums = map[string]string{}
}
reader, err := i.Fs.Open(i.Path)
reader, err := os.Open(i.Path)
if err != nil {
return err
}
@@ -266,23 +249,120 @@ func (i *FileInfo) Checksum(algo string) error {
// RealPath gets the real path for the file, resolving symlinks if supported.
func (i *FileInfo) RealPath() string {
if realPathFs, ok := i.Fs.(interface {
RealPath(name string) (fPath string, err error)
}); ok {
realPath, err := realPathFs.RealPath(i.Path)
if err == nil {
return realPath
}
realPath, err := filepath.EvalSymlinks(i.Path)
if err == nil {
return realPath
}
return i.Path
}
func GetRealPath(relativePath ...string) (string, bool, error) {
combined := []string{settings.Config.Server.Root}
for _, path := range relativePath {
combined = append(combined, strings.TrimPrefix(path, settings.Config.Server.Root))
}
joinedPath := filepath.Join(combined...)
// Convert relative path to absolute path
absolutePath, err := filepath.Abs(joinedPath)
if err != nil {
return "", false, err
}
if !Exists(absolutePath) {
return absolutePath, false, nil // return without error
}
// Resolve symlinks and get the real path
return resolveSymlinks(absolutePath)
}
func DeleteFiles(absPath string, opts FileOptions) error {
err := os.RemoveAll(absPath)
if err != nil {
return err
}
opts.Path = filepath.Dir(absPath)
err = RefreshFileInfo(opts)
if err != nil {
return errors.ErrEmptyKey
}
return nil
}
func WriteDirectory(opts FileOptions) error {
// Ensure the parent directories exist
err := os.MkdirAll(opts.Path, 0775)
if err != nil {
return err
}
opts.Path = filepath.Dir(opts.Path)
err = RefreshFileInfo(opts)
if err != nil {
return errors.ErrEmptyKey
}
return nil
}
func WriteFile(opts FileOptions, in io.Reader) error {
dst := opts.Path
parentDir := filepath.Dir(dst)
// Split the directory from the destination path
dir := filepath.Dir(dst)
// Create the directory and all necessary parents
err := os.MkdirAll(dir, 0775)
if err != nil {
return err
}
return i.Path
// Open the file for writing (create if it doesn't exist, truncate if it does)
file, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
if err != nil {
return err
}
defer file.Close()
// Copy the contents from the reader to the file
_, err = io.Copy(file, in)
if err != nil {
return err
}
opts.Path = parentDir
err = RefreshFileInfo(opts)
if err != nil {
return errors.ErrEmptyKey
}
return nil
}
// resolveSymlinks resolves symlinks in the given path
func resolveSymlinks(path string) (string, bool, error) {
for {
// Get the file info
info, err := os.Lstat(path)
if err != nil {
return "", false, err
}
// Check if it's a symlink
if info.Mode()&os.ModeSymlink != 0 {
// Read the symlink target
target, err := os.Readlink(path)
if err != nil {
return "", false, err
}
// Resolve the target relative to the symlink's directory
path = filepath.Join(filepath.Dir(path), target)
} else {
// Not a symlink, so return the resolved path and check if it's a directory
return path, info.IsDir(), nil
}
}
}
// addContent reads and sets content based on the file type.
func (i *FileInfo) addContent(path string) error {
if !i.IsDir {
afs := &afero.Afero{Fs: i.Fs}
content, err := afs.ReadFile(path)
content, err := os.ReadFile(path)
if err != nil {
return err
}
@@ -301,6 +381,9 @@ func (i *FileInfo) addContent(path string) error {
// detectType detects the file type.
func (i *FileInfo) detectType(path string, modify, saveContent, readHeader bool) error {
if i.IsDir {
return nil
}
if IsNamedPipe(i.Mode) {
i.Type = "blob"
if saveContent {
@@ -345,7 +428,6 @@ func (i *FileInfo) detectType(path string, modify, saveContent, readHeader bool)
}
}
}
if i.Type == "" {
i.Type = "blob"
if saveContent {
@@ -358,15 +440,15 @@ func (i *FileInfo) detectType(path string, modify, saveContent, readHeader bool)
// readFirstBytes reads the first bytes of the file.
func (i *FileInfo) readFirstBytes() []byte {
reader, err := i.Fs.Open(i.Path)
file, err := os.Open(i.Path)
if err != nil {
i.Type = "blob"
return nil
}
defer reader.Close()
defer file.Close()
buffer := make([]byte, 512) //nolint:gomnd
n, err := reader.Read(buffer)
n, err := file.Read(buffer)
if err != nil && err != io.EOF {
i.Type = "blob"
return nil
@@ -387,7 +469,8 @@ func (i *FileInfo) detectSubtitles(parentDir string) {
// Directory must have been deleted, remove it from the index
return
}
// Read the directory contents
defer dir.Close() // Ensure directory handle is closed
files, err := dir.Readdir(-1)
if err != nil {
return
@@ -412,20 +495,24 @@ func (i *FileInfo) detectSubtitles(parentDir string) {
// readListing reads the contents of a directory and fills the listing.
func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bool) error {
afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path)
dir, err := os.Open(i.Path)
if err != nil {
return err
}
defer dir.Close()
files, err := dir.Readdir(-1)
if err != nil {
return err
}
listing := &Listing{
listing := &FileInfo{
Items: []*FileInfo{},
Path: i.Path,
NumDirs: 0,
NumFiles: 0,
}
for _, f := range dir {
for _, f := range files {
name := f.Name()
fPath := filepath.Join(i.Path, name)
@@ -436,7 +523,7 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
isSymlink, isInvalidLink := false, false
if IsSymlink(f.Mode()) {
isSymlink = true
info, err := i.Fs.Stat(fPath)
info, err := os.Stat(fPath)
if err == nil {
f = info
} else {
@@ -475,9 +562,10 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
listing.Items = append(listing.Items, file)
}
i.Listing = listing
i.Items = listing.Items
return nil
}
func IsNamedPipe(mode os.FileMode) bool {
return mode&os.ModeNamedPipe != 0
}
@@ -498,3 +586,14 @@ func getMutex(path string) *sync.Mutex {
return pathMutexes[path]
}
func Exists(path string) bool {
_, err := os.Stat(path)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return false
}
+76
View File
@@ -0,0 +1,76 @@
package files
import (
"os"
"path/filepath"
"strings"
"testing"
)
func Test_GetRealPath(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
return
}
trimPrefix := filepath.Dir(filepath.Dir(cwd)) + "/"
tests := []struct {
name string
paths []string
want struct {
path string
isDir bool
}
}{
{
name: "current directory",
paths: []string{
"./",
},
want: struct {
path string
isDir bool
}{
path: "backend/files",
isDir: true,
},
},
{
name: "current directory",
paths: []string{
"./file.go",
},
want: struct {
path string
isDir bool
}{
path: "backend/files/file.go",
isDir: false,
},
},
{
name: "other test case",
paths: []string{
"/mnt/doesnt/exist",
},
want: struct {
path string
isDir bool
}{
path: "/mnt/doesnt/exist",
isDir: false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
realPath, isDir, err := GetRealPath(tt.paths...)
adjustedRealPath := strings.TrimPrefix(realPath, trimPrefix)
if tt.want.path != adjustedRealPath || tt.want.isDir != isDir {
t.Errorf("expected %v:%v but got: %v:%v", tt.want.path, tt.want.isDir, adjustedRealPath, isDir)
}
if err != nil {
t.Error("got error", err)
}
})
}
}
+101 -70
View File
@@ -1,9 +1,9 @@
package files
import (
"bytes"
"log"
"os"
"path/filepath"
"strings"
"sync"
"time"
@@ -11,22 +11,12 @@ import (
"github.com/gtsteffaniak/filebrowser/settings"
)
type Directory struct {
Metadata map[string]FileInfo
Files string
}
type File struct {
Name string
IsDir bool
}
type Index struct {
Root string
Directories map[string]Directory
Directories map[string]FileInfo
NumDirs int
NumFiles int
inProgress bool
quickList []File
LastIndexed time.Time
mu sync.RWMutex
}
@@ -48,16 +38,12 @@ func indexingScheduler(intervalMinutes uint32) {
rootPath = settings.Config.Server.Root
}
si := GetIndex(rootPath)
log.Printf("Indexing Files...")
log.Printf("Configured to run every %v minutes", intervalMinutes)
log.Printf("Indexing from root: %s", si.Root)
for {
startTime := time.Now()
// Set the indexing flag to indicate that indexing is in progress
si.resetCount()
// Perform the indexing operation
err := si.indexFiles(si.Root)
si.quickList = []File{}
// Reset the indexing flag to indicate that indexing has finished
si.inProgress = false
// Update the LastIndexed time
@@ -79,89 +65,134 @@ func indexingScheduler(intervalMinutes uint32) {
// Define a function to recursively index files and directories
func (si *Index) indexFiles(path string) error {
// Check if the current directory has been modified since the last indexing
path = strings.TrimSuffix(path, "/")
adjustedPath := makeIndexPath(path, si.Root)
// Ensure path is cleaned and normalized
adjustedPath := si.makeIndexPath(path, true)
// Open the directory
dir, err := os.Open(path)
if err != nil {
// Directory must have been deleted, remove it from the index
// If the directory can't be opened (e.g., deleted), remove it from the index
si.RemoveDirectory(adjustedPath)
return err
}
defer dir.Close()
dirInfo, err := dir.Stat()
if err != nil {
dir.Close()
return err
}
// Compare the last modified time of the directory with the last indexed time
lastIndexed := si.LastIndexed
if dirInfo.ModTime().Before(lastIndexed) {
dir.Close()
// Check if the directory is already up-to-date
if dirInfo.ModTime().Before(si.LastIndexed) {
return nil
}
// Read the directory contents
// Read directory contents
files, err := dir.Readdir(-1)
if err != nil {
return err
}
dir.Close()
si.UpdateQuickList(files)
si.InsertFiles(path)
// done separately for memory efficiency on recursion
si.InsertDirs(path)
// Recursively process files and directories
fileInfos := []*FileInfo{}
var totalSize int64
var numDirs, numFiles int
for _, file := range files {
parentInfo := &FileInfo{
Name: file.Name(),
Size: file.Size(),
ModTime: file.ModTime(),
IsDir: file.IsDir(),
}
childInfo, err := si.InsertInfo(path, parentInfo)
if err != nil {
// Log error, but continue processing other files
continue
}
// Accumulate directory size and items
totalSize += childInfo.Size
if childInfo.IsDir {
numDirs++
} else {
numFiles++
}
_ = childInfo.detectType(path, true, false, false)
fileInfos = append(fileInfos, childInfo)
}
// Create FileInfo for the current directory
dirFileInfo := &FileInfo{
Items: fileInfos,
Name: filepath.Base(path),
Size: totalSize,
ModTime: dirInfo.ModTime(),
CacheTime: time.Now(),
IsDir: true,
NumDirs: numDirs,
NumFiles: numFiles,
}
// Add directory to index
si.mu.Lock()
si.Directories[adjustedPath] = *dirFileInfo
si.NumDirs += numDirs
si.NumFiles += numFiles
si.mu.Unlock()
return nil
}
func (si *Index) InsertFiles(path string) {
adjustedPath := makeIndexPath(path, si.Root)
subDirectory := Directory{}
buffer := bytes.Buffer{}
// InsertInfo function to handle adding a file or directory into the index
func (si *Index) InsertInfo(parentPath string, file *FileInfo) (*FileInfo, error) {
filePath := filepath.Join(parentPath, file.Name)
for _, f := range si.GetQuickList() {
if !f.IsDir {
buffer.WriteString(f.Name + ";")
si.UpdateCount("files")
// Check if it's a directory and recursively index it
if file.IsDir {
// Recursively index directory
err := si.indexFiles(filePath)
if err != nil {
return nil, err
}
// Return directory info from the index
adjustedPath := si.makeIndexPath(filePath, true)
si.mu.RLock()
dirInfo := si.Directories[adjustedPath]
si.mu.RUnlock()
return &dirInfo, nil
}
// Use GetMetadataInfo and SetFileMetadata for safer read and write operations
subDirectory.Files = buffer.String()
si.SetDirectoryInfo(adjustedPath, subDirectory)
// Create FileInfo for regular files
fileInfo := &FileInfo{
Path: filePath,
Name: file.Name,
Size: file.Size,
ModTime: file.ModTime,
IsDir: false,
}
return fileInfo, nil
}
func (si *Index) InsertDirs(path string) {
adjustedPath := makeIndexPath(path, si.Root)
for _, f := range si.GetQuickList() {
if f.IsDir {
if _, exists := si.Directories[adjustedPath]; exists {
si.UpdateCount("dirs")
// Add or update the directory in the map
if adjustedPath == "/" {
si.SetDirectoryInfo("/"+f.Name, Directory{})
} else {
si.SetDirectoryInfo(adjustedPath+"/"+f.Name, Directory{})
}
}
err := si.indexFiles(path + "/" + f.Name)
if err != nil {
if err.Error() == "invalid argument" {
log.Printf("Could not index \"%v\": %v \n", path, "Permission Denied")
} else {
log.Printf("Could not index \"%v\": %v \n", path, err)
}
}
}
}
}
func makeIndexPath(path string, root string) string {
if path == root {
func (si *Index) makeIndexPath(subPath string, isDir bool) string {
if si.Root == subPath {
return "/"
}
adjustedPath := strings.TrimPrefix(path, root+"/")
// clean path
subPath = strings.TrimSuffix(subPath, "/")
// remove index prefix
adjustedPath := strings.TrimPrefix(subPath, si.Root)
// remove trailing slash
adjustedPath = strings.TrimSuffix(adjustedPath, "/")
// add leading slash for root of index
if adjustedPath == "" {
adjustedPath = "/"
} else if !isDir {
adjustedPath = filepath.Dir(adjustedPath)
}
if !strings.HasPrefix(adjustedPath, "/") {
adjustedPath = "/" + adjustedPath
}
return adjustedPath
}
+17 -8
View File
@@ -2,6 +2,7 @@ package files
import (
"encoding/json"
"fmt"
"math/rand"
"reflect"
"testing"
@@ -23,18 +24,26 @@ func BenchmarkFillIndex(b *testing.B) {
func (si *Index) createMockData(numDirs, numFilesPerDir int) {
for i := 0; i < numDirs; i++ {
dirName := generateRandomPath(rand.Intn(3) + 1)
files := []File{}
// Append a new Directory to the slice
files := []*FileInfo{} // Slice of FileInfo
// Simulating files and directories with FileInfo
for j := 0; j < numFilesPerDir; j++ {
newFile := File{
Name: "file-" + getRandomTerm() + getRandomExtension(),
IsDir: false,
newFile := &FileInfo{
Name: "file-" + getRandomTerm() + getRandomExtension(),
IsDir: false,
Size: rand.Int63n(1000), // Random size
ModTime: time.Now().Add(-time.Duration(rand.Intn(100)) * time.Hour), // Random mod time
}
files = append(files, newFile)
}
si.UpdateQuickListForTests(files)
si.InsertFiles(dirName)
si.InsertDirs(dirName)
// Simulate inserting files into index
for _, file := range files {
_, err := si.InsertInfo(dirName, file)
if err != nil {
fmt.Println("Error inserting file:", err)
}
}
}
}
+89 -58
View File
@@ -2,7 +2,6 @@ package files
import (
"math/rand"
"os"
"path/filepath"
"sort"
"strings"
@@ -30,12 +29,17 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
continue
}
si.mu.Lock()
defer si.mu.Unlock()
for dirName, dir := range si.Directories {
isDir := true
files := strings.Split(dir.Files, ";")
files := []string{}
for _, item := range dir.Items {
if !item.IsDir {
files = append(files, item.Name)
}
}
value, found := sessionInProgress.Load(sourceSession)
if !found || value != runningHash {
si.mu.Unlock()
return []string{}, map[string]map[string]bool{}
}
if count > maxSearchResults {
@@ -46,7 +50,9 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
continue // path not matched
}
fileTypes := map[string]bool{}
matches, fileType := containsSearchTerm(dirName, searchTerm, *searchOptions, isDir, fileTypes)
si.mu.Unlock()
matches, fileType := si.containsSearchTerm(dirName, searchTerm, *searchOptions, isDir, fileTypes)
si.mu.Lock()
if matches {
fileListTypes[pathName] = fileType
matching = append(matching, pathName)
@@ -65,10 +71,11 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
if count > maxSearchResults {
break
}
fullName := pathName + file
fullName := strings.TrimLeft(pathName+file, "/")
fileTypes := map[string]bool{}
matches, fileType := containsSearchTerm(fullName, searchTerm, *searchOptions, isDir, fileTypes)
si.mu.Unlock()
matches, fileType := si.containsSearchTerm(fullName, searchTerm, *searchOptions, isDir, fileTypes)
si.mu.Lock()
if !matches {
continue
}
@@ -77,6 +84,7 @@ func (si *Index) Search(search string, scope string, sourceSession string) ([]st
count++
}
}
si.mu.Unlock()
}
// Sort the strings based on the number of elements after splitting by "/"
sort.Slice(matching, func(i, j int) bool {
@@ -102,65 +110,88 @@ func scopedPathNameFilter(pathName string, scope string, isDir bool) string {
return pathName
}
func containsSearchTerm(pathName string, searchTerm string, options SearchOptions, isDir bool, fileTypes map[string]bool) (bool, map[string]bool) {
func (si *Index) containsSearchTerm(pathName string, searchTerm string, options SearchOptions, isDir bool, fileTypes map[string]bool) (bool, map[string]bool) {
largerThan := int64(options.LargerThan) * 1024 * 1024
smallerThan := int64(options.SmallerThan) * 1024 * 1024
conditions := options.Conditions
path := getLastPathComponent(pathName)
// Convert to lowercase once
fileName := filepath.Base(pathName)
adjustedPath := si.makeIndexPath(pathName, isDir)
// Convert to lowercase if not exact match
if !conditions["exact"] {
path = strings.ToLower(path)
fileName = strings.ToLower(fileName)
searchTerm = strings.ToLower(searchTerm)
}
if strings.Contains(path, searchTerm) {
// Calculate fileSize only if needed
var fileSize int64
matchesAllConditions := true
extension := filepath.Ext(path)
for _, k := range AllFiletypeOptions {
if IsMatchingType(extension, k) {
fileTypes[k] = true
// Check if the file name contains the search term
if !strings.Contains(fileName, searchTerm) {
return false, map[string]bool{}
}
// Initialize file size and fileTypes map
var fileSize int64
extension := filepath.Ext(fileName)
// Collect file types
for _, k := range AllFiletypeOptions {
if IsMatchingType(extension, k) {
fileTypes[k] = true
}
}
fileTypes["dir"] = isDir
// Get file info if needed for size-related conditions
if largerThan > 0 || smallerThan > 0 {
fileInfo, exists := si.GetMetadataInfo(adjustedPath)
if !exists {
return false, fileTypes
} else if !isDir {
// Look for specific file in ReducedItems
for _, item := range fileInfo.ReducedItems {
lower := strings.ToLower(item.Name)
if strings.Contains(lower, searchTerm) {
if item.Size == 0 {
return false, fileTypes
}
fileSize = item.Size
break
}
}
} else {
fileSize = fileInfo.Size
}
if fileSize == 0 {
return false, fileTypes
}
}
// Evaluate all conditions
for t, v := range conditions {
if t == "exact" {
continue
}
switch t {
case "larger":
if largerThan > 0 {
if fileSize <= largerThan {
return false, fileTypes
}
}
case "smaller":
if smallerThan > 0 {
if fileSize >= smallerThan {
return false, fileTypes
}
}
default:
// Handle other file type conditions
notMatchType := v != fileTypes[t]
if notMatchType {
return false, fileTypes
}
}
fileTypes["dir"] = isDir
for t, v := range conditions {
if t == "exact" {
continue
}
var matchesCondition bool
switch t {
case "larger":
if fileSize == 0 {
fileSize = getFileSize(pathName)
}
matchesCondition = fileSize > int64(options.LargerThan)*bytesInMegabyte
case "smaller":
if fileSize == 0 {
fileSize = getFileSize(pathName)
}
matchesCondition = fileSize < int64(options.SmallerThan)*bytesInMegabyte
default:
matchesCondition = v == fileTypes[t]
}
if !matchesCondition {
matchesAllConditions = false
}
}
return matchesAllConditions, fileTypes
}
// Clear variables and return
return false, map[string]bool{}
}
func getFileSize(filepath string) int64 {
fileInfo, err := os.Stat(rootPath + "/" + filepath)
if err != nil {
return 0
}
return fileInfo.Size()
}
func getLastPathComponent(path string) string {
// Use filepath.Base to extract the last component of the path
return filepath.Base(path)
return true, fileTypes
}
func generateRandomHash(length int) string {
+96 -168
View File
@@ -11,7 +11,7 @@ func BenchmarkSearchAllIndexes(b *testing.B) {
InitializeIndex(5, false)
si := GetIndex(rootPath)
si.createMockData(50, 3) // 1000 dirs, 3 files per dir
si.createMockData(50, 3) // 50 dirs, 3 files per dir
// Generate 100 random search terms
searchTerms := generateRandomSearchTerms(100)
@@ -26,87 +26,90 @@ func BenchmarkSearchAllIndexes(b *testing.B) {
}
}
// loop over test files and compare output
func TestParseSearch(t *testing.T) {
value := ParseSearch("my test search")
want := &SearchOptions{
Conditions: map[string]bool{
"exact": false,
tests := []struct {
input string
want *SearchOptions
}{
{
input: "my test search",
want: &SearchOptions{
Conditions: map[string]bool{"exact": false},
Terms: []string{"my test search"},
},
},
Terms: []string{"my test search"},
}
if !reflect.DeepEqual(value, want) {
t.Fatalf("\n got: %+v\n want: %+v", value, want)
}
value = ParseSearch("case:exact my|test|search")
want = &SearchOptions{
Conditions: map[string]bool{
"exact": true,
{
input: "case:exact my|test|search",
want: &SearchOptions{
Conditions: map[string]bool{"exact": true},
Terms: []string{"my", "test", "search"},
},
},
Terms: []string{"my", "test", "search"},
}
if !reflect.DeepEqual(value, want) {
t.Fatalf("\n got: %+v\n want: %+v", value, want)
}
value = ParseSearch("type:largerThan=100 type:smallerThan=1000 test")
want = &SearchOptions{
Conditions: map[string]bool{
"exact": false,
"larger": true,
{
input: "type:largerThan=100 type:smallerThan=1000 test",
want: &SearchOptions{
Conditions: map[string]bool{"exact": false, "larger": true, "smaller": true},
Terms: []string{"test"},
LargerThan: 100,
SmallerThan: 1000,
},
},
Terms: []string{"test"},
LargerThan: 100,
SmallerThan: 1000,
}
if !reflect.DeepEqual(value, want) {
t.Fatalf("\n got: %+v\n want: %+v", value, want)
}
value = ParseSearch("type:audio thisfile")
want = &SearchOptions{
Conditions: map[string]bool{
"exact": false,
"audio": true,
{
input: "type:audio thisfile",
want: &SearchOptions{
Conditions: map[string]bool{"exact": false, "audio": true},
Terms: []string{"thisfile"},
},
},
Terms: []string{"thisfile"},
}
if !reflect.DeepEqual(value, want) {
t.Fatalf("\n got: %+v\n want: %+v", value, want)
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
value := ParseSearch(tt.input)
if !reflect.DeepEqual(value, tt.want) {
t.Fatalf("\n got: %+v\n want: %+v", value, tt.want)
}
})
}
}
func TestSearchWhileIndexing(t *testing.T) {
InitializeIndex(5, false)
si := GetIndex(rootPath)
// Generate 100 random search terms
// Generate 100 random search terms
searchTerms := generateRandomSearchTerms(10)
for i := 0; i < 5; i++ {
// Execute the SearchAllIndexes function
go si.createMockData(100, 100) // 1000 dirs, 3 files per dir
go si.createMockData(100, 100) // Creating mock data concurrently
for _, term := range searchTerms {
go si.Search(term, "/", "test")
go si.Search(term, "/", "test") // Search concurrently
}
}
}
func TestSearchIndexes(t *testing.T) {
index := Index{
Directories: map[string]Directory{
"test": {
Files: "audio1.wav;",
},
"test/path": {
Files: "file.txt;",
},
"new": {},
"new/test": {
Files: "audio.wav;video.mp4;video.MP4;",
},
"new/test/path": {
Files: "archive.zip;",
Directories: map[string]FileInfo{
"test": {Items: []*FileInfo{{Name: "audio1.wav"}}},
"test/path": {Items: []*FileInfo{{Name: "file.txt"}}},
"new/test": {Items: []*FileInfo{
{Name: "audio.wav"},
{Name: "video.mp4"},
{Name: "video.MP4"},
}},
"new/test/path": {Items: []*FileInfo{{Name: "archive.zip"}}},
"/firstDir": {Items: []*FileInfo{
{Name: "archive.zip", Size: 100},
{Name: "thisIsDir", IsDir: true, Size: 2 * 1024 * 1024},
}},
"/firstDir/thisIsDir": {
Items: []*FileInfo{
{Name: "hi.txt"},
},
Size: 2 * 1024 * 1024,
},
},
}
tests := []struct {
search string
scope string
@@ -118,7 +121,7 @@ func TestSearchIndexes(t *testing.T) {
scope: "/new/",
expectedResult: []string{"test/audio.wav"},
expectedTypes: map[string]map[string]bool{
"test/audio.wav": map[string]bool{"audio": true, "dir": false},
"test/audio.wav": {"audio": true, "dir": false},
},
},
{
@@ -126,16 +129,41 @@ func TestSearchIndexes(t *testing.T) {
scope: "/",
expectedResult: []string{"test/", "new/test/"},
expectedTypes: map[string]map[string]bool{
"test/": map[string]bool{"dir": true},
"new/test/": map[string]bool{"dir": true},
"test/": {"dir": true},
"new/test/": {"dir": true},
},
},
{
search: "archive",
scope: "/",
expectedResult: []string{"new/test/path/archive.zip"},
expectedResult: []string{"firstDir/archive.zip", "new/test/path/archive.zip"},
expectedTypes: map[string]map[string]bool{
"new/test/path/archive.zip": map[string]bool{"archive": true, "dir": false},
"new/test/path/archive.zip": {"archive": true, "dir": false},
"firstDir/archive.zip": {"archive": true, "dir": false},
},
},
{
search: "arch",
scope: "/firstDir",
expectedResult: []string{"archive.zip"},
expectedTypes: map[string]map[string]bool{
"archive.zip": {"archive": true, "dir": false},
},
},
{
search: "isdir",
scope: "/",
expectedResult: []string{"firstDir/thisIsDir/"},
expectedTypes: map[string]map[string]bool{
"firstDir/thisIsDir/": {"dir": true},
},
},
{
search: "dir type:largerThan=1",
scope: "/",
expectedResult: []string{"firstDir/thisIsDir/"},
expectedTypes: map[string]map[string]bool{
"firstDir/thisIsDir/": {"dir": true},
},
},
{
@@ -146,18 +174,17 @@ func TestSearchIndexes(t *testing.T) {
"new/test/video.MP4",
},
expectedTypes: map[string]map[string]bool{
"new/test/video.MP4": map[string]bool{"video": true, "dir": false},
"new/test/video.mp4": map[string]bool{"video": true, "dir": false},
"new/test/video.MP4": {"video": true, "dir": false},
"new/test/video.mp4": {"video": true, "dir": false},
},
},
}
for _, tt := range tests {
t.Run(tt.search, func(t *testing.T) {
actualResult, actualTypes := index.Search(tt.search, tt.scope, "")
assert.Equal(t, tt.expectedResult, actualResult)
if !reflect.DeepEqual(tt.expectedTypes, actualTypes) {
t.Fatalf("\n got: %+v\n want: %+v", actualTypes, tt.expectedTypes)
}
assert.Equal(t, tt.expectedTypes, actualTypes)
})
}
}
@@ -186,6 +213,7 @@ func Test_scopedPathNameFilter(t *testing.T) {
want: "", // Update this with the expected result
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := scopedPathNameFilter(tt.args.pathName, tt.args.scope, tt.args.isDir); got != tt.want {
@@ -194,103 +222,3 @@ func Test_scopedPathNameFilter(t *testing.T) {
})
}
}
func Test_isDoc(t *testing.T) {
type args struct {
extension string
}
tests := []struct {
name string
args args
want bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isDoc(tt.args.extension); got != tt.want {
t.Errorf("isDoc() = %v, want %v", got, tt.want)
}
})
}
}
func Test_getFileSize(t *testing.T) {
type args struct {
filepath string
}
tests := []struct {
name string
args args
want int64
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := getFileSize(tt.args.filepath); got != tt.want {
t.Errorf("getFileSize() = %v, want %v", got, tt.want)
}
})
}
}
func Test_isArchive(t *testing.T) {
type args struct {
extension string
}
tests := []struct {
name string
args args
want bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isArchive(tt.args.extension); got != tt.want {
t.Errorf("isArchive() = %v, want %v", got, tt.want)
}
})
}
}
func Test_getLastPathComponent(t *testing.T) {
type args struct {
path string
}
tests := []struct {
name string
args args
want string
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := getLastPathComponent(tt.args.path); got != tt.want {
t.Errorf("getLastPathComponent() = %v, want %v", got, tt.want)
}
})
}
}
func Test_generateRandomHash(t *testing.T) {
type args struct {
length int
}
tests := []struct {
name string
args args
want string
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := generateRandomHash(tt.args.length); got != tt.want {
t.Errorf("generateRandomHash() = %v, want %v", got, tt.want)
}
})
}
}
+25 -77
View File
@@ -1,57 +1,32 @@
package files
import (
"io/fs"
"log"
"time"
"github.com/gtsteffaniak/filebrowser/settings"
)
// GetFileMetadata retrieves the FileInfo from the specified directory in the index.
func (si *Index) GetFileMetadata(adjustedPath string) (FileInfo, bool) {
si.mu.RLock()
dir, exists := si.Directories[adjustedPath]
si.mu.RUnlock()
if exists {
// Initialize the Metadata map if it is nil
if dir.Metadata == nil {
dir.Metadata = make(map[string]FileInfo)
si.SetDirectoryInfo(adjustedPath, dir)
return FileInfo{}, false
} else {
return dir.Metadata[adjustedPath], true
}
}
return FileInfo{}, false
}
// UpdateFileMetadata updates the FileInfo for the specified directory in the index.
func (si *Index) UpdateFileMetadata(adjustedPath string, info FileInfo) bool {
si.mu.Lock()
defer si.mu.Unlock()
dir, exists := si.Directories[adjustedPath]
if !exists {
// Initialize the Metadata map if it is nil
if dir.Metadata == nil {
dir.Metadata = make(map[string]FileInfo)
}
si.Directories[adjustedPath] = dir
// Release the read lock before calling SetFileMetadata
si.Directories[adjustedPath] = FileInfo{}
}
return si.SetFileMetadata(adjustedPath, info)
return si.SetFileMetadata(adjustedPath, dir)
}
// SetFileMetadata sets the FileInfo for the specified directory in the index.
// internal use only
func (si *Index) SetFileMetadata(adjustedPath string, info FileInfo) bool {
_, exists := si.Directories[adjustedPath]
if !exists {
return false
}
info.CacheTime = time.Now()
si.Directories[adjustedPath].Metadata[adjustedPath] = info
si.Directories[adjustedPath] = info
return true
}
@@ -60,34 +35,40 @@ func (si *Index) GetMetadataInfo(adjustedPath string) (FileInfo, bool) {
si.mu.RLock()
dir, exists := si.Directories[adjustedPath]
si.mu.RUnlock()
if exists {
// Initialize the Metadata map if it is nil
if dir.Metadata == nil {
dir.Metadata = make(map[string]FileInfo)
si.SetDirectoryInfo(adjustedPath, dir)
}
info, metadataExists := dir.Metadata[adjustedPath]
return info, metadataExists
if !exists {
return dir, exists
}
return FileInfo{}, false
// remove recursive items, we only want this directories direct files
cleanedItems := []ReducedItem{}
for _, item := range dir.Items {
cleanedItems = append(cleanedItems, ReducedItem{
Name: item.Name,
Size: item.Size,
IsDir: item.IsDir,
ModTime: item.ModTime,
Type: item.Type,
})
}
dir.Items = nil
dir.ReducedItems = cleanedItems
realPath, _, _ := GetRealPath(adjustedPath)
dir.Path = realPath
return dir, exists
}
// SetDirectoryInfo sets the directory information in the index.
func (si *Index) SetDirectoryInfo(adjustedPath string, dir Directory) {
func (si *Index) SetDirectoryInfo(adjustedPath string, dir FileInfo) {
si.mu.Lock()
si.Directories[adjustedPath] = dir
si.mu.Unlock()
}
// SetDirectoryInfo sets the directory information in the index.
func (si *Index) GetDirectoryInfo(adjustedPath string) (Directory, bool) {
func (si *Index) GetDirectoryInfo(adjustedPath string) (FileInfo, bool) {
si.mu.RLock()
dir, exists := si.Directories[adjustedPath]
si.mu.RUnlock()
if exists {
return dir, true
}
return Directory{}, false
return dir, exists
}
func (si *Index) RemoveDirectory(path string) {
@@ -127,7 +108,7 @@ func GetIndex(root string) *Index {
}
newIndex := &Index{
Root: rootPath,
Directories: make(map[string]Directory), // Initialize the map
Directories: map[string]FileInfo{},
NumDirs: 0,
NumFiles: 0,
inProgress: false,
@@ -137,36 +118,3 @@ func GetIndex(root string) *Index {
indexesMutex.Unlock()
return newIndex
}
func (si *Index) UpdateQuickList(files []fs.FileInfo) {
si.mu.Lock()
defer si.mu.Unlock()
si.quickList = []File{}
for _, file := range files {
newFile := File{
Name: file.Name(),
IsDir: file.IsDir(),
}
si.quickList = append(si.quickList, newFile)
}
}
func (si *Index) UpdateQuickListForTests(files []File) {
si.mu.Lock()
defer si.mu.Unlock()
si.quickList = []File{}
for _, file := range files {
newFile := File{
Name: file.Name,
IsDir: file.IsDir,
}
si.quickList = append(si.quickList, newFile)
}
}
func (si *Index) GetQuickList() []File {
si.mu.Lock()
defer si.mu.Unlock()
newQuickList := si.quickList
return newQuickList
}
+262
View File
@@ -0,0 +1,262 @@
package files
import (
"testing"
"github.com/stretchr/testify/assert"
)
var testIndex Index
// Test for GetFileMetadata// Test for GetFileMetadata
func TestGetFileMetadataSize(t *testing.T) {
t.Parallel()
tests := []struct {
name string
adjustedPath string
expectedName string
expectedSize int64
}{
{
name: "testpath exists",
adjustedPath: "/testpath",
expectedName: "testfile.txt",
expectedSize: 100,
},
{
name: "testpath exists",
adjustedPath: "/testpath",
expectedName: "directory",
expectedSize: 100,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fileInfo, _ := testIndex.GetMetadataInfo(tt.adjustedPath)
// Iterate over fileInfo.Items to look for expectedName
for _, item := range fileInfo.ReducedItems {
// Assert the existence and the name
if item.Name == tt.expectedName {
assert.Equal(t, tt.expectedSize, item.Size)
break
}
}
})
}
}
// Test for GetFileMetadata// Test for GetFileMetadata
func TestGetFileMetadata(t *testing.T) {
t.Parallel()
tests := []struct {
name string
adjustedPath string
expectedName string
expectedExists bool
}{
{
name: "testpath exists",
adjustedPath: "/testpath",
expectedName: "testfile.txt",
expectedExists: true,
},
{
name: "testpath not exists",
adjustedPath: "/testpath",
expectedName: "nonexistent.txt",
expectedExists: false,
},
{
name: "File exists in /anotherpath",
adjustedPath: "/anotherpath",
expectedName: "afile.txt",
expectedExists: true,
},
{
name: "File does not exist in /anotherpath",
adjustedPath: "/anotherpath",
expectedName: "nonexistentfile.txt",
expectedExists: false,
},
{
name: "Directory does not exist",
adjustedPath: "/nonexistentpath",
expectedName: "",
expectedExists: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fileInfo, _ := testIndex.GetMetadataInfo(tt.adjustedPath)
found := false
// Iterate over fileInfo.Items to look for expectedName
for _, item := range fileInfo.ReducedItems {
// Assert the existence and the name
if item.Name == tt.expectedName {
found = true
break
}
}
assert.Equal(t, tt.expectedExists, found)
})
}
}
// Test for UpdateFileMetadata
func TestUpdateFileMetadata(t *testing.T) {
index := &Index{
Directories: map[string]FileInfo{
"/testpath": {
Path: "/testpath",
Name: "testpath",
IsDir: true,
ReducedItems: []ReducedItem{
{Name: "testfile.txt"},
{Name: "anotherfile.txt"},
},
},
},
}
info := FileInfo{Name: "testfile.txt"}
success := index.UpdateFileMetadata("/testpath", info)
if !success {
t.Fatalf("expected UpdateFileMetadata to succeed")
}
dir, exists := index.Directories["/testpath"]
if !exists || dir.ReducedItems[0].Name != "testfile.txt" {
t.Fatalf("expected testfile.txt to be updated in the directory metadata")
}
}
// Test for GetDirMetadata
func TestGetDirMetadata(t *testing.T) {
t.Parallel()
_, exists := testIndex.GetMetadataInfo("/testpath")
if !exists {
t.Fatalf("expected GetDirMetadata to return initialized metadata map")
}
_, exists = testIndex.GetMetadataInfo("/nonexistent")
if exists {
t.Fatalf("expected GetDirMetadata to return false for nonexistent directory")
}
}
// Test for SetDirectoryInfo
func TestSetDirectoryInfo(t *testing.T) {
index := &Index{
Directories: map[string]FileInfo{
"/testpath": {
Path: "/testpath",
Name: "testpath",
IsDir: true,
Items: []*FileInfo{
{Name: "testfile.txt"},
{Name: "anotherfile.txt"},
},
},
},
}
dir := FileInfo{
Path: "/newPath",
Name: "newPath",
IsDir: true,
Items: []*FileInfo{
{Name: "testfile.txt"},
},
}
index.SetDirectoryInfo("/newPath", dir)
storedDir, exists := index.Directories["/newPath"]
if !exists || storedDir.Items[0].Name != "testfile.txt" {
t.Fatalf("expected SetDirectoryInfo to store directory info correctly")
}
}
// Test for GetDirectoryInfo
func TestGetDirectoryInfo(t *testing.T) {
t.Parallel()
dir, exists := testIndex.GetDirectoryInfo("/testpath")
if !exists || dir.Items[0].Name != "testfile.txt" {
t.Fatalf("expected GetDirectoryInfo to return correct directory info")
}
_, exists = testIndex.GetDirectoryInfo("/nonexistent")
if exists {
t.Fatalf("expected GetDirectoryInfo to return false for nonexistent directory")
}
}
// Test for RemoveDirectory
func TestRemoveDirectory(t *testing.T) {
index := &Index{
Directories: map[string]FileInfo{
"/testpath": {},
},
}
index.RemoveDirectory("/testpath")
_, exists := index.Directories["/testpath"]
if exists {
t.Fatalf("expected directory to be removed")
}
}
// Test for UpdateCount
func TestUpdateCount(t *testing.T) {
index := &Index{}
index.UpdateCount("files")
if index.NumFiles != 1 {
t.Fatalf("expected NumFiles to be 1 after UpdateCount('files')")
}
if index.NumFiles != 1 {
t.Fatalf("expected NumFiles to be 1 after UpdateCount('files')")
}
index.UpdateCount("dirs")
if index.NumDirs != 1 {
t.Fatalf("expected NumDirs to be 1 after UpdateCount('dirs')")
}
index.UpdateCount("unknown")
// Just ensure it does not panic or update any counters
if index.NumFiles != 1 || index.NumDirs != 1 {
t.Fatalf("expected counts to remain unchanged for unknown type")
}
index.resetCount()
if index.NumFiles != 0 || index.NumDirs != 0 || !index.inProgress {
t.Fatalf("expected resetCount to reset counts and set inProgress to true")
}
}
func init() {
testIndex = Index{
Root: "/",
NumFiles: 10,
NumDirs: 5,
inProgress: false,
Directories: map[string]FileInfo{
"/testpath": {
Path: "/testpath",
Name: "testpath",
IsDir: true,
NumDirs: 1,
NumFiles: 2,
Items: []*FileInfo{
{Name: "testfile.txt", Size: 100},
{Name: "anotherfile.txt", Size: 100},
},
},
"/anotherpath": {
Path: "/anotherpath",
Name: "anotherpath",
IsDir: true,
NumDirs: 1,
NumFiles: 1,
Items: []*FileInfo{
{Name: "directory", IsDir: true, Size: 100},
{Name: "afile.txt", Size: 100},
},
},
},
}
}
+10 -10
View File
@@ -2,23 +2,23 @@ package fileutils
import (
"os"
"path"
"github.com/spf13/afero"
"path/filepath"
)
// Copy copies a file or folder from one place to another.
func Copy(fs afero.Fs, src, dst string) error {
if src = path.Clean("/" + src); src == "" {
func Copy(src, dst string) error {
src = filepath.Clean(src)
if src == "" {
return os.ErrNotExist
}
if dst = path.Clean("/" + dst); dst == "" {
dst = filepath.Clean(dst)
if dst == "" {
return os.ErrNotExist
}
if src == "/" || dst == "/" {
// Prohibit copying from or to the virtual root directory.
// Prohibit copying from or to the root directory.
return os.ErrInvalid
}
@@ -26,14 +26,14 @@ func Copy(fs afero.Fs, src, dst string) error {
return os.ErrInvalid
}
info, err := fs.Stat(src)
info, err := os.Stat(src)
if err != nil {
return err
}
if info.IsDir() {
return CopyDir(fs, src, dst)
return CopyDir(src, dst)
}
return CopyFile(fs, src, dst)
return CopyFile(src, dst)
}
+15 -10
View File
@@ -2,27 +2,32 @@ package fileutils
import (
"errors"
"github.com/spf13/afero"
"os"
"path/filepath"
)
// CopyDir copies a directory from source to dest and all
// of its sub-directories. It doesn't stop if it finds an error
// during the copy. Returns an error if any.
func CopyDir(fs afero.Fs, source, dest string) error {
func CopyDir(source, dest string) error {
// Get properties of source.
srcinfo, err := fs.Stat(source)
srcinfo, err := os.Stat(source)
if err != nil {
return err
}
// Create the destination directory.
err = fs.MkdirAll(dest, srcinfo.Mode())
err = os.MkdirAll(dest, srcinfo.Mode())
if err != nil {
return err
}
dir, _ := fs.Open(source)
dir, err := os.Open(source)
if err != nil {
return err
}
defer dir.Close()
obs, err := dir.Readdir(-1)
if err != nil {
return err
@@ -31,18 +36,18 @@ func CopyDir(fs afero.Fs, source, dest string) error {
var errs []error
for _, obj := range obs {
fsource := source + "/" + obj.Name()
fdest := dest + "/" + obj.Name()
fsource := filepath.Join(source, obj.Name())
fdest := filepath.Join(dest, obj.Name())
if obj.IsDir() {
// Create sub-directories, recursively.
err = CopyDir(fs, fsource, fdest)
err = CopyDir(fsource, fdest)
if err != nil {
errs = append(errs, err)
}
} else {
// Perform the file copy.
err = CopyFile(fs, fsource, fdest)
err = CopyFile(fsource, fdest)
if err != nil {
errs = append(errs, err)
}
+24 -38
View File
@@ -5,24 +5,22 @@ import (
"os"
"path"
"path/filepath"
"github.com/spf13/afero"
)
// MoveFile moves file from src to dst.
// By default the rename filesystem system call is used. If src and dst point to different volumes
// the file copy is used as a fallback
func MoveFile(fs afero.Fs, src, dst string) error {
if fs.Rename(src, dst) == nil {
// MoveFile moves a file from src to dst.
// By default, the rename system call is used. If src and dst point to different volumes,
// the file copy is used as a fallback.
func MoveFile(src, dst string) error {
if os.Rename(src, dst) == nil {
return nil
}
// fallback
err := CopyFile(fs, src, dst)
err := CopyFile(src, dst)
if err != nil {
_ = fs.Remove(dst)
_ = os.Remove(dst)
return err
}
if err := fs.Remove(src); err != nil {
if err := os.Remove(src); err != nil {
return err
}
return nil
@@ -30,23 +28,22 @@ func MoveFile(fs afero.Fs, src, dst string) error {
// CopyFile copies a file from source to dest and returns
// an error if any.
func CopyFile(fs afero.Fs, source, dest string) error {
func CopyFile(source, dest string) error {
// Open the source file.
src, err := fs.Open(source)
src, err := os.Open(source)
if err != nil {
return err
}
defer src.Close()
// Makes the directory needed to create the dst
// file.
err = fs.MkdirAll(filepath.Dir(dest), 0666) //nolint:gomnd
// Makes the directory needed to create the dst file.
err = os.MkdirAll(filepath.Dir(dest), 0775) //nolint:gomnd
if err != nil {
return err
}
// Create the destination file.
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) //nolint:gomnd
dst, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) //nolint:gomnd
if err != nil {
return err
}
@@ -58,12 +55,12 @@ func CopyFile(fs afero.Fs, source, dest string) error {
return err
}
// Copy the mode
info, err := fs.Stat(source)
// Copy the mode.
info, err := os.Stat(source)
if err != nil {
return err
}
err = fs.Chmod(dest, info.Mode())
err = os.Chmod(dest, info.Mode())
if err != nil {
return err
}
@@ -71,7 +68,7 @@ func CopyFile(fs afero.Fs, source, dest string) error {
return nil
}
// CommonPrefix returns common directory path of provided files
// CommonPrefix returns the common directory path of provided files.
func CommonPrefix(sep byte, paths ...string) string {
// Handle special cases.
switch len(paths) {
@@ -81,30 +78,19 @@ func CommonPrefix(sep byte, paths ...string) string {
return path.Clean(paths[0])
}
// Note, we treat string as []byte, not []rune as is often
// done in Go. (And sep as byte, not rune). This is because
// most/all supported OS' treat paths as string of non-zero
// bytes. A filename may be displayed as a sequence of Unicode
// runes (typically encoded as UTF-8) but paths are
// not required to be valid UTF-8 or in any normalized form
// (e.g. "é" (U+00C9) and "é" (U+0065,U+0301) are different
// file names.
// Treat string as []byte, not []rune as is often done in Go.
c := []byte(path.Clean(paths[0]))
// We add a trailing sep to handle the case where the
// common prefix directory is included in the path list
// (e.g. /home/user1, /home/user1/foo, /home/user1/bar).
// path.Clean will have cleaned off trailing / separators with
// the exception of the root directory, "/" (in which case we
// make it "//", but this will get fixed up to "/" bellow).
// Add a trailing sep to handle the case where the common prefix directory
// is included in the path list.
c = append(c, sep)
// Ignore the first path since it's already in c
// Ignore the first path since it's already in c.
for _, v := range paths[1:] {
// Clean up each path before testing it
// Clean up each path before testing it.
v = path.Clean(v) + string(sep)
// Find the first non-common byte and truncate c
// Find the first non-common byte and truncate c.
if len(v) < len(c) {
c = c[:len(v)]
}
@@ -116,7 +102,7 @@ func CommonPrefix(sep byte, paths ...string) string {
}
}
// Remove trailing non-separator characters and the final separator
// Remove trailing non-separator characters and the final separator.
for i := len(c) - 1; i >= 0; i-- {
if c[i] == sep {
c = c[:i]
+21 -21
View File
@@ -19,38 +19,38 @@ require (
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
golang.org/x/crypto v0.25.0
golang.org/x/image v0.18.0
golang.org/x/text v0.16.0
golang.org/x/crypto v0.26.0
golang.org/x/image v0.19.0
golang.org/x/text v0.17.0
)
require (
github.com/andybalholm/brotli v1.0.1 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
github.com/fatih/color v1.10.0 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
github.com/golang/snappy v0.0.2 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.11.4 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/nwaples/rardecode v1.1.0 // indirect
github.com/pierrec/lz4/v4 v4.1.2 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/ulikunitz/xz v0.5.9 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/bbolt v1.3.4 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
go.etcd.io/bbolt v1.3.11 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+48 -33
View File
@@ -2,8 +2,9 @@ github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@@ -31,17 +32,19 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003142440-7a1927d49d9d/go.mod h1:LV
github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU=
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw=
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
@@ -54,15 +57,17 @@ github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOW
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -73,11 +78,13 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -87,20 +94,23 @@ github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/marusama/semaphore/v2 v2.5.0 h1:o/1QJD9DBYOWRnDhPwDVAXQn6mQYD0gZaS1Tpx6DJGM=
github.com/marusama/semaphore/v2 v2.5.0/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM=
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
@@ -116,22 +126,24 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -139,31 +151,34 @@ golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk=
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
Binary file not shown.
+5 -9
View File
@@ -2,7 +2,6 @@ package http
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
@@ -127,11 +126,10 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
return http.StatusBadRequest, nil
}
user := &users.User{
Username: info.Username,
Password: info.Password,
}
settings.Config.UserDefaults.Apply(user)
user := users.ApplyDefaults(users.User{})
user.Username = info.Username
user.Password = info.Password
userHome, err := d.settings.MakeUserDir(user.Username, user.Scope, d.server.Root)
if err != nil {
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
@@ -139,8 +137,7 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
}
user.Scope = userHome
log.Printf("new user: %s, home dir: [%s].", user.Username, userHome)
settings.Config.UserDefaults.Apply(user)
err = d.store.Users.Save(user)
err = d.store.Users.Save(&user)
if err == errors.ErrExist {
return http.StatusConflict, err
} else if err != nil {
@@ -157,7 +154,6 @@ var renewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data
func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (int, error) {
duration, err := time.ParseDuration(settings.Config.Auth.TokenExpirationTime)
if err != nil {
fmt.Println("Error parsing duration:", err)
duration = time.Hour * 2
}
claims := &authToken{
+12 -3
View File
@@ -15,11 +15,20 @@ type modifyRequest struct {
Which []string `json:"which"` // Answer to: which fields?
}
var (
store *storage.Storage
server *settings.Server
fileCache FileCache
)
func SetupEnv(storage *storage.Storage, s *settings.Server, cache FileCache) {
store = storage
server = s
fileCache = cache
}
func NewHandler(
imgSvc ImgService,
fileCache FileCache,
store *storage.Storage,
server *settings.Server,
assetsFs fs.FS,
) (http.Handler, error) {
server.Clean()
+2 -2
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"os"
"github.com/gorilla/mux"
@@ -46,7 +47,6 @@ func previewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, re
}
file, err := files.FileInfoFaster(files.FileOptions{
Fs: d.user.Fs,
Path: "/" + vars["path"],
Modify: d.user.Perm.Modify,
Expand: true,
@@ -110,7 +110,7 @@ func handleImagePreview(
func createPreview(imgSvc ImgService, fileCache FileCache,
file *files.FileInfo, previewSize PreviewSize) ([]byte, error) {
fd, err := file.Fs.Open(file.Path)
fd, err := os.Open(file.Path)
if err != nil {
return nil, err
}
+19 -21
View File
@@ -2,13 +2,12 @@ package http
import (
"errors"
"fmt"
"net/http"
"net/url"
"path"
"path/filepath"
"strings"
"github.com/spf13/afero"
"golang.org/x/crypto/bcrypt"
"github.com/gtsteffaniak/filebrowser/files"
@@ -25,26 +24,20 @@ var withHashFile = func(fn handleFunc) handleFunc {
return errToStatus(err), err
}
if link.Hash != "" {
status, err := authenticateShareRequest(r, link)
if status != 0 || err != nil {
var status int
status, err = authenticateShareRequest(r, link)
if err != nil || status != 0 {
return status, err
}
}
d.user = &users.PublicUser
if path == "/" {
path = link.Path
} else if strings.HasPrefix("/"+path, link.Path) {
path = "/" + path
} else {
path = link.Path + "/" + path
realPath, isDir, err := files.GetRealPath(d.user.Scope, link.Path, path)
if err != nil {
return http.StatusNotFound, err
}
sharePath := settings.Config.Server.Root + path
lastComponent := filepath.Base(sharePath)
basePath := filepath.Dir(sharePath)
fsPath := afero.NewBasePathFs(afero.NewOsFs(), basePath)
file, err := files.FileInfoFaster(files.FileOptions{
Fs: fsPath,
Path: lastComponent,
Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify,
Expand: true,
ReadHeader: d.server.TypeDetectionByHeader,
@@ -63,14 +56,15 @@ func ifPathWithName(r *http.Request) (id, filePath string) {
pathElements := strings.Split(r.URL.Path, "/")
id = pathElements[0]
allButFirst := path.Join(pathElements[1:]...)
if len(pathElements) == 1 {
allButFirst = "/"
}
return id, allButFirst
}
var publicShareHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
file := d.raw.(*files.FileInfo)
file, ok := d.raw.(*files.FileInfo)
if !ok {
return http.StatusInternalServerError, fmt.Errorf("failed to assert type *files.FileInfo")
}
file.Path = strings.TrimPrefix(file.Path, settings.Config.Server.Root)
if file.IsDir {
return renderJSON(w, r, file)
@@ -86,7 +80,11 @@ var publicUserGetHandler = func(w http.ResponseWriter, r *http.Request, d *data)
}
var publicDlHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
file := d.raw.(*files.FileInfo)
file, ok := d.raw.(*files.FileInfo)
if !ok {
return http.StatusInternalServerError, fmt.Errorf("failed to assert type *files.FileInfo")
}
if !file.IsDir {
return rawFileHandler(w, r, file)
}
+8 -7
View File
@@ -8,10 +8,10 @@ import (
"testing"
"github.com/asdine/storm/v3"
"github.com/spf13/afero"
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/share"
"github.com/gtsteffaniak/filebrowser/storage"
"github.com/gtsteffaniak/filebrowser/storage/bolt"
"github.com/gtsteffaniak/filebrowser/users"
)
@@ -74,8 +74,13 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
t.Errorf("failed to close db: %v", err)
}
})
storage, err := bolt.NewStorage(db)
authStore, userStore, shareStore, settingsStore, err := bolt.NewStorage(db)
storage := &storage.Storage{
Auth: authStore,
Users: userStore,
Share: shareStore,
Settings: settingsStore,
}
if err != nil {
t.Fatalf("failed to get storage: %v", err)
}
@@ -92,7 +97,6 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
storage.Users = &customFSUser{
Store: storage.Users,
fs: &afero.MemMapFs{},
}
recorder := httptest.NewRecorder()
@@ -122,7 +126,6 @@ func newHTTPRequest(t *testing.T, requestModifiers ...func(*http.Request)) *http
type customFSUser struct {
users.Store
fs afero.Fs
}
func (cu *customFSUser) Get(baseScope string, id interface{}) (*users.User, error) {
@@ -130,7 +133,5 @@ func (cu *customFSUser) Get(baseScope string, id interface{}) (*users.User, erro
if err != nil {
return nil, err
}
user.Fs = cu.fs
return user, nil
}
+10 -6
View File
@@ -5,6 +5,7 @@ import (
"log"
"net/http"
"net/url"
"os"
gopath "path"
"path/filepath"
"strings"
@@ -80,10 +81,13 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
if !d.user.Perm.Download {
return http.StatusAccepted, nil
}
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil {
return http.StatusInternalServerError, err
}
file, err := files.FileInfoFaster(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: d.server.TypeDetectionByHeader,
@@ -108,7 +112,7 @@ func addFile(ar archiver.Writer, d *data, path, commonPath string) error {
if !d.Check(path) {
return nil
}
info, err := d.user.Fs.Stat(path)
info, err := os.Stat(path)
if err != nil {
return err
}
@@ -117,7 +121,7 @@ func addFile(ar archiver.Writer, d *data, path, commonPath string) error {
return nil
}
file, err := d.user.Fs.Open(path)
file, err := os.Open(path)
if err != nil {
return err
}
@@ -198,7 +202,7 @@ func rawDirHandler(w http.ResponseWriter, r *http.Request, d *data, file *files.
}
func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo) (int, error) {
fd, err := file.Fs.Open(file.Path)
fd, err := os.Open(file.Path)
if err != nil {
return http.StatusInternalServerError, err
}
+74 -133
View File
@@ -3,7 +3,6 @@ package http
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
@@ -12,7 +11,6 @@ import (
"strings"
"github.com/shirou/gopsutil/v3/disk"
"github.com/spf13/afero"
"github.com/gtsteffaniak/filebrowser/errors"
"github.com/gtsteffaniak/filebrowser/files"
@@ -20,9 +18,13 @@ import (
)
var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil {
return http.StatusNotFound, err
}
file, err := files.FileInfoFaster(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify,
Expand: true,
ReadHeader: d.server.TypeDetectionByHeader,
@@ -32,19 +34,16 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
if err != nil {
return errToStatus(err), err
}
if file.IsDir {
file.Listing.Sorting = d.user.Sorting
return renderJSON(w, r, file)
}
if checksum := r.URL.Query().Get("checksum"); checksum != "" {
err := file.Checksum(checksum)
if err == errors.ErrInvalidOption {
return http.StatusBadRequest, nil
} else if err != nil {
return http.StatusInternalServerError, err
if !file.IsDir {
if checksum := r.URL.Query().Get("checksum"); checksum != "" {
err := file.Checksum(checksum)
if err == errors.ErrInvalidOption {
return http.StatusBadRequest, nil
} else if err != nil {
return http.StatusInternalServerError, err
}
}
}
return renderJSON(w, r, file)
})
@@ -53,15 +52,19 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
if r.URL.Path == "/" || !d.user.Perm.Delete {
return http.StatusForbidden, nil
}
file, err := files.FileInfoFaster(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil {
return http.StatusNotFound, err
}
fileOpts := files.FileOptions{
Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
})
}
file, err := files.FileInfoFaster(fileOpts)
if err != nil {
return errToStatus(err), err
}
@@ -72,14 +75,10 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
return errToStatus(err), err
}
err = d.RunHook(func() error {
return d.user.Fs.RemoveAll(r.URL.Path)
}, "delete", r.URL.Path, "", d.user)
err = files.DeleteFiles(realPath, fileOpts)
if err != nil {
return errToStatus(err), err
}
return http.StatusOK, nil
})
}
@@ -89,21 +88,27 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
return http.StatusForbidden, nil
}
// Directories creation on POST.
if strings.HasSuffix(r.URL.Path, "/") {
err := d.user.Fs.MkdirAll(r.URL.Path, 0775) //nolint:gomnd
return errToStatus(err), err
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil {
return http.StatusNotFound, err
}
file, err := files.FileInfoFaster(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
fileOpts := files.FileOptions{
Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
})
}
// Directories creation on POST.
if strings.HasSuffix(r.URL.Path, "/") {
err = files.WriteDirectory(fileOpts) // Assign to the existing `err` variable
if err != nil {
return errToStatus(err), err
}
return http.StatusOK, nil
}
file, err := files.FileInfoFaster(fileOpts)
if err == nil {
if r.URL.Query().Get("override") != "true" {
return http.StatusConflict, nil
@@ -119,22 +124,7 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
return errToStatus(err), err
}
}
err = d.RunHook(func() error {
info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body)
if writeErr != nil {
return writeErr
}
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
w.Header().Set("ETag", etag)
return nil
}, "upload", r.URL.Path, "", d.user)
if err != nil {
_ = d.user.Fs.RemoveAll(r.URL.Path)
}
err = files.WriteFile(fileOpts, r.Body)
return errToStatus(err), err
})
}
@@ -149,27 +139,23 @@ var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
return http.StatusMethodNotAllowed, nil
}
exists, err := afero.Exists(d.user.Fs, r.URL.Path)
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil {
return http.StatusInternalServerError, err
return http.StatusNotFound, err
}
if !exists {
return http.StatusNotFound, nil
fileOpts := files.FileOptions{
Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
}
err = d.RunHook(func() error {
info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body)
if writeErr != nil {
return writeErr
}
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
w.Header().Set("ETag", etag)
return nil
}, "save", r.URL.Path, "", d.user)
err = files.WriteFile(fileOpts, r.Body)
return errToStatus(err), err
})
// TODO fix and verify this function still works in tests
func resourcePatchHandler(fileCache FileCache) handleFunc {
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
src := r.URL.Path
@@ -185,28 +171,20 @@ func resourcePatchHandler(fileCache FileCache) handleFunc {
if dst == "/" || src == "/" {
return http.StatusForbidden, nil
}
err = checkParent(src, dst)
if err != nil {
return http.StatusBadRequest, err
}
override := r.URL.Query().Get("override") == "true"
rename := r.URL.Query().Get("rename") == "true"
if !override && !rename {
if _, err = d.user.Fs.Stat(dst); err == nil {
if _, err = os.Stat(dst); err == nil {
return http.StatusConflict, nil
}
}
if rename {
dst = addVersionSuffix(dst, d.user.Fs)
dst = addVersionSuffix(dst)
}
// Permission for overwriting the file
if override && !d.user.Perm.Modify {
return http.StatusForbidden, nil
}
err = d.RunHook(func() error {
return patchAction(r.Context(), action, src, dst, d, fileCache)
}, action, src, dst, d.user)
@@ -215,69 +193,22 @@ func resourcePatchHandler(fileCache FileCache) handleFunc {
})
}
func checkParent(src, dst string) error {
rel, err := filepath.Rel(src, dst)
if err != nil {
return err
}
rel = filepath.ToSlash(rel)
if !strings.HasPrefix(rel, "../") && rel != ".." && rel != "." {
return errors.ErrSourceIsParent
}
return nil
}
func addVersionSuffix(source string, fs afero.Fs) string {
func addVersionSuffix(source string) string {
counter := 1
dir, name := path.Split(source)
ext := filepath.Ext(name)
base := strings.TrimSuffix(name, ext)
for {
if _, err := fs.Stat(source); err != nil {
if _, err := os.Stat(source); err != nil {
break
}
renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext)
source = path.Join(dir, renamed)
counter++
}
return source
}
func writeFile(fs afero.Fs, dst string, in io.Reader) (os.FileInfo, error) {
dir, _ := path.Split(dst)
err := fs.MkdirAll(dir, 0775) //nolint:gomnd
if err != nil {
return nil, err
}
file, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) //nolint:gomnd
if err != nil {
return nil, err
}
defer file.Close()
_, err = io.Copy(file, in)
if err != nil {
return nil, err
}
// Gets the info about the file.
info, err := file.Stat()
if err != nil {
return nil, err
}
//files.RefreshFileInfo(files.FileOptions{
// Fs: info,
//})
return info, nil
}
func delThumbs(ctx context.Context, fileCache FileCache, file *files.FileInfo) error {
for _, previewSizeName := range PreviewSizeNames() {
size, _ := ParsePreviewSize(previewSizeName)
@@ -297,17 +228,24 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
return errors.ErrPermissionDenied
}
return fileutils.Copy(d.user.Fs, src, dst)
return fileutils.Copy(src, dst)
case "rename":
if !d.user.Perm.Rename {
return errors.ErrPermissionDenied
}
src = path.Clean("/" + src)
dst = path.Clean("/" + dst)
realDest, _, err := files.GetRealPath(d.user.Scope, dst)
if err != nil {
return err
}
realSrc, isDir, err := files.GetRealPath(d.user.Scope, src)
if err != nil {
return err
}
file, err := files.FileInfoFaster(files.FileOptions{
Fs: d.user.Fs,
Path: src,
Path: realSrc,
IsDir: isDir,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: false,
@@ -323,7 +261,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
return err
}
return fileutils.MoveFile(d.user.Fs, src, dst)
return fileutils.MoveFile(realSrc, realDest)
default:
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
}
@@ -335,9 +273,13 @@ type DiskUsageResponse struct {
}
var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil {
return http.StatusNotFound, err
}
file, err := files.FileInfoFaster(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: false,
@@ -353,7 +295,6 @@ var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (
Used: 0,
})
}
usage, err := disk.UsageWithContext(r.Context(), fPath)
if err != nil {
return errToStatus(err), err
+4 -8
View File
@@ -15,20 +15,17 @@ type settingsData struct {
Defaults settings.UserDefaults `json:"defaults"`
Rules []rules.Rule `json:"rules"`
Frontend settings.Frontend `json:"frontend"`
Shell []string `json:"shell"`
Commands map[string][]string `json:"commands"`
}
var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
data := &settingsData{
Signup: settings.Config.Auth.Signup,
CreateUserDir: settings.Config.Server.CreateUserDir,
UserHomeBasePath: settings.Config.Server.UserHomeBasePath,
Signup: d.settings.Auth.Signup,
CreateUserDir: d.settings.Server.CreateUserDir,
UserHomeBasePath: d.settings.Server.UserHomeBasePath,
Defaults: d.settings.UserDefaults,
Rules: d.settings.Rules,
Frontend: d.settings.Frontend,
Shell: d.settings.Shell,
Commands: d.settings.Commands,
}
return renderJSON(w, r, data)
@@ -46,8 +43,7 @@ var settingsPutHandler = withAdmin(func(w http.ResponseWriter, r *http.Request,
d.settings.UserDefaults = req.Defaults
d.settings.Rules = req.Rules
d.settings.Frontend = req.Frontend
d.settings.Shell = req.Shell
d.settings.Commands = req.Commands
d.settings.Auth.Signup = req.Signup
err = d.store.Settings.Save(d.settings)
return errToStatus(err), err
})
+7 -1
View File
@@ -34,6 +34,7 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
"Color": d.settings.Frontend.Color,
"BaseURL": d.server.BaseURL,
"Version": version.Version,
"CommitSHA": version.CommitSHA,
"StaticURL": path.Join(d.server.BaseURL, "/static"),
"Signup": settings.Config.Auth.Signup,
"NoAuth": d.settings.Auth.Method == "noauth",
@@ -64,7 +65,12 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
if err != nil {
return http.StatusInternalServerError, err
}
auther := raw.(*auth.JSONAuth)
auther, ok := raw.(*auth.JSONAuth)
if !ok {
return http.StatusInternalServerError, fmt.Errorf("failed to assert type *auth.JSONAuth")
}
if auther.ReCaptcha != nil {
data["ReCaptcha"] = auther.ReCaptcha.Key != "" && auther.ReCaptcha.Secret != ""
data["ReCaptchaHost"] = auther.ReCaptcha.Host
+10 -11
View File
@@ -2,7 +2,6 @@ package http
import (
"encoding/json"
"log"
"net/http"
"reflect"
"sort"
@@ -13,6 +12,8 @@ import (
"golang.org/x/text/language"
"github.com/gtsteffaniak/filebrowser/errors"
"github.com/gtsteffaniak/filebrowser/files"
"github.com/gtsteffaniak/filebrowser/storage"
"github.com/gtsteffaniak/filebrowser/users"
)
@@ -129,15 +130,7 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
return http.StatusBadRequest, errors.ErrEmptyPassword
}
userHome, err := d.settings.MakeUserDir(req.Data.Username, req.Data.Scope, d.server.Root)
if err != nil {
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
return http.StatusInternalServerError, err
}
req.Data.Scope = userHome
log.Printf("user: %s, home dir: [%s].", req.Data.Username, userHome)
err = d.store.Users.Save(req.Data)
err = storage.CreateUser(*req.Data, req.Data.Perm.Admin)
if err != nil {
return http.StatusInternalServerError, err
}
@@ -155,6 +148,10 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
if req.Data.ID != d.raw.(uint) {
return http.StatusBadRequest, nil
}
_, _, err = files.GetRealPath(d.server.Root, req.Data.Scope)
if err != nil {
return http.StatusBadRequest, nil
}
if len(req.Which) == 0 || req.Which[0] == "all" {
req.Which = []string{}
@@ -165,7 +162,9 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if field.Name != "Password" && field.Name != "Fs" {
if field.Name == "Password" && req.Data.Password != "" {
req.Which = append(req.Which, field.Name)
} else if field.Name != "Password" && field.Name != "Fs" {
req.Which = append(req.Which, field.Name)
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ checkExit() {
if command -v go &> /dev/null
then
printf "\n == Running tests == \n"
go test -race -v ./...
go test -race -parallel -v ./...
checkExit
else
echo "ERROR: unable to perform tests"
+3 -2
View File
@@ -7,6 +7,7 @@ import (
"os/exec"
"strings"
"github.com/gtsteffaniak/filebrowser/files"
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/users"
)
@@ -19,8 +20,8 @@ type Runner struct {
// RunHook runs the hooks for the before and after event.
func (r *Runner) RunHook(fn func() error, evt, path, dst string, user *users.User) error {
path = user.FullPath(path)
dst = user.FullPath(dst)
path, _, _ = files.GetRealPath(user.Scope, path)
dst, _, _ = files.GetRealPath(user.Scope, dst)
if r.Enabled {
if val, ok := r.Commands["before_"+evt]; ok {
+16 -24
View File
@@ -3,10 +3,9 @@ package settings
import (
"log"
"os"
"strings"
"path/filepath"
"github.com/goccy/go-yaml"
"github.com/gtsteffaniak/filebrowser/users"
)
var Config Settings
@@ -19,22 +18,30 @@ func Initialize(configFile string) {
log.Fatalf("Error unmarshaling YAML data: %v", err)
}
Config.UserDefaults.Perm = Config.UserDefaults.Permissions
Config.Server.Root = strings.TrimSuffix(Config.Server.Root, "/")
// Convert relative path to absolute path
realRoot, err := filepath.Abs(Config.Server.Root)
if err != nil {
log.Fatalf("Error getting root path: %v", err)
}
_, err = os.Stat(realRoot)
if err != nil {
log.Fatalf("ERROR: Configured Root Path does not exist! %v", err)
}
Config.Server.Root = realRoot
}
func loadConfigFile(configFile string) []byte {
// Open and read the YAML file
yamlFile, err := os.Open(configFile)
if err != nil {
log.Printf("ERROR: opening config file\n %v\n WARNING: Using default config only\n If this was a mistake, please make sure the file exists and is accessible by the filebrowser binary.\n\n", err)
Config = setDefaults()
return []byte{}
log.Println(err)
os.Exit(1)
}
defer yamlFile.Close()
stat, err := yamlFile.Stat()
if err != nil {
log.Fatalf("Error getting file information: %s", err.Error())
log.Fatalf("error getting file information: %s", err.Error())
}
yamlData := make([]byte, stat.Size())
@@ -77,8 +84,9 @@ func setDefaults() Settings {
HideDotfiles: true,
DarkMode: false,
DisableSettings: false,
ViewMode: "normal",
Locale: "en",
Permissions: users.Permissions{
Permissions: Permissions{
Create: false,
Rename: false,
Modify: false,
@@ -90,19 +98,3 @@ func setDefaults() Settings {
},
}
}
// Apply applies the default options to a user.
func (d *UserDefaults) Apply(u *users.User) {
u.StickySidebar = d.StickySidebar
u.DisableSettings = d.DisableSettings
u.DarkMode = d.DarkMode
u.Scope = d.Scope
u.Locale = d.Locale
u.ViewMode = d.ViewMode
u.SingleClick = d.SingleClick
u.Perm = d.Perm
u.Sorting = d.Sorting
u.Commands = d.Commands
u.HideDotfiles = d.HideDotfiles
u.DateFormat = d.DateFormat
}
+4 -6
View File
@@ -6,16 +6,14 @@ import (
"log"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/spf13/afero"
)
var (
invalidFilenameChars = regexp.MustCompile(`[^0-9A-Za-z@_\-.]`)
dashes = regexp.MustCompile(`[\-]+`)
dashes = regexp.MustCompile(`[\-]+`)
)
// MakeUserDir makes the user directory according to settings.
@@ -32,8 +30,8 @@ func (s *Settings) MakeUserDir(username, userScope, serverRoot string) (string,
userScope = path.Join("/", userScope)
fs := afero.NewBasePathFs(afero.NewOsFs(), serverRoot)
if err := fs.MkdirAll(userScope, os.ModePerm); err != nil {
fullPath := filepath.Join(serverRoot, userScope)
if err := os.MkdirAll(fullPath, os.ModePerm); err != nil {
return "", fmt.Errorf("failed to create user home dir: [%s]: %w", userScope, err)
}
return userScope, nil
+12
View File
@@ -39,3 +39,15 @@ func GenerateKey() ([]byte, error) {
func GetSettingsConfig(nameType string, Value string) string {
return nameType + Value
}
func AdminPerms() Permissions {
return Permissions{
Create: true,
Rename: true,
Modify: true,
Delete: true,
Share: true,
Download: true,
Admin: true,
}
}
-4
View File
@@ -65,10 +65,6 @@ func (s *Storage) Save(set *Settings) error {
set.Rules = []rules.Rule{}
}
if set.Shell == nil {
set.Shell = []string{}
}
if set.Commands == nil {
set.Commands = map[string][]string{}
}
+17 -6
View File
@@ -2,7 +2,6 @@ package settings
import (
"github.com/gtsteffaniak/filebrowser/rules"
"github.com/gtsteffaniak/filebrowser/users"
)
type Settings struct {
@@ -75,15 +74,27 @@ type UserDefaults struct {
Scope string `json:"scope"`
Locale string `json:"locale"`
ViewMode string `json:"viewMode"`
GallerySize int `json:"gallerySize"`
SingleClick bool `json:"singleClick"`
Rules []rules.Rule `json:"rules"`
Sorting struct {
By string `json:"by"`
Asc bool `json:"asc"`
} `json:"sorting"`
Perm users.Permissions `json:"perm"`
Permissions users.Permissions `json:"permissions"`
Commands []string `json:"commands,omitempty"`
HideDotfiles bool `json:"hideDotfiles"`
DateFormat bool `json:"dateFormat"`
Perm Permissions `json:"perm"`
Permissions Permissions `json:"permissions"`
Commands []string `json:"commands,omitempty"`
HideDotfiles bool `json:"hideDotfiles"`
DateFormat bool `json:"dateFormat"`
}
type Permissions struct {
Admin bool `json:"admin"`
Execute bool `json:"execute"`
Create bool `json:"create"`
Rename bool `json:"rename"`
Modify bool `json:"modify"`
Delete bool `json:"delete"`
Share bool `json:"share"`
Download bool `json:"download"`
}
+1 -1
View File
@@ -28,5 +28,5 @@ func (s authBackend) Get(t string) (auth.Auther, error) {
}
func (s authBackend) Save(a auth.Auther) error {
return save(s.db, "auther", a)
return Save(s.db, "auther", a)
}
+2 -14
View File
@@ -6,26 +6,14 @@ import (
"github.com/gtsteffaniak/filebrowser/auth"
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/share"
"github.com/gtsteffaniak/filebrowser/storage"
"github.com/gtsteffaniak/filebrowser/users"
)
// NewStorage creates a storage.Storage based on Bolt DB.
func NewStorage(db *storm.DB) (*storage.Storage, error) {
func NewStorage(db *storm.DB) (*auth.Storage, *users.Storage, *share.Storage, *settings.Storage, error) {
userStore := users.NewStorage(usersBackend{db: db})
shareStore := share.NewStorage(shareBackend{db: db})
settingsStore := settings.NewStorage(settingsBackend{db: db})
authStore := auth.NewStorage(authBackend{db: db}, userStore)
err := save(db, "version", 2) //nolint:gomnd
if err != nil {
return nil, err
}
return &storage.Storage{
Auth: authStore,
Users: userStore,
Share: shareStore,
Settings: settingsStore,
}, nil
return authStore, userStore, shareStore, settingsStore, nil
}
+2 -2
View File
@@ -15,7 +15,7 @@ func (s settingsBackend) Get() (*settings.Settings, error) {
}
func (s settingsBackend) Save(set *settings.Settings) error {
return save(s.db, "settings", set)
return Save(s.db, "settings", set)
}
func (s settingsBackend) GetServer() (*settings.Server, error) {
@@ -27,5 +27,5 @@ func (s settingsBackend) GetServer() (*settings.Server, error) {
}
func (s settingsBackend) SaveServer(server *settings.Server) error {
return save(s.db, "server", server)
return Save(s.db, "server", server)
}
+1 -1
View File
@@ -15,6 +15,6 @@ func get(db *storm.DB, name string, to interface{}) error {
return err
}
func save(db *storm.DB, name string, from interface{}) error {
func Save(db *storm.DB, name string, from interface{}) error {
return db.Set("config", name, from)
}
+119
View File
@@ -1,10 +1,20 @@
package storage
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/asdine/storm/v3"
"github.com/gtsteffaniak/filebrowser/auth"
"github.com/gtsteffaniak/filebrowser/errors"
"github.com/gtsteffaniak/filebrowser/files"
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/share"
"github.com/gtsteffaniak/filebrowser/storage/bolt"
"github.com/gtsteffaniak/filebrowser/users"
"github.com/gtsteffaniak/filebrowser/utils"
)
// Storage is a storage powered by a Backend which makes the necessary
@@ -15,3 +25,112 @@ type Storage struct {
Auth *auth.Storage
Settings *settings.Storage
}
var store *Storage
func InitializeDb(path string) (*Storage, bool, error) {
exists, err := dbExists(path)
if err != nil {
panic(err)
}
db, err := storm.Open(path)
utils.CheckErr(fmt.Sprintf("storm.Open path %v", path), err)
authStore, userStore, shareStore, settingsStore, err := bolt.NewStorage(db)
if err != nil {
return nil, exists, err
}
err = bolt.Save(db, "version", 2) //nolint:gomnd
if err != nil {
return nil, exists, err
}
store = &Storage{
Auth: authStore,
Users: userStore,
Share: shareStore,
Settings: settingsStore,
}
if !exists {
quickSetup(store)
}
return store, exists, err
}
func dbExists(path string) (bool, error) {
stat, err := os.Stat(path)
if err == nil {
return stat.Size() != 0, nil
}
if os.IsNotExist(err) {
d := filepath.Dir(path)
_, err = os.Stat(d)
if os.IsNotExist(err) {
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet,gomnd
return false, err
}
return false, nil
}
}
return false, err
}
func quickSetup(store *Storage) {
settings.Config.Auth.Key = utils.GenerateKey()
if settings.Config.Auth.Method == "noauth" {
err := store.Auth.Save(&auth.NoAuth{})
utils.CheckErr("store.Auth.Save", err)
} else {
settings.Config.Auth.Method = "password"
err := store.Auth.Save(&auth.JSONAuth{})
utils.CheckErr("store.Auth.Save", err)
}
err := store.Settings.Save(&settings.Config)
utils.CheckErr("store.Settings.Save", err)
err = store.Settings.SaveServer(&settings.Config.Server)
utils.CheckErr("store.Settings.SaveServer", err)
user := users.ApplyDefaults(users.User{})
user.Username = settings.Config.Auth.AdminUsername
user.Password = settings.Config.Auth.AdminPassword
user.Perm.Admin = true
user.Scope = "./"
user.DarkMode = true
user.ViewMode = "normal"
user.LockPassword = false
user.Perm = settings.AdminPerms()
err = store.Users.Save(&user)
utils.CheckErr("store.Users.Save", err)
}
// create new user
func CreateUser(userInfo users.User, asAdmin bool) error {
// must have username or password to create
if userInfo.Username == "" || userInfo.Password == "" {
return errors.ErrInvalidRequestParams
}
newUser := users.ApplyDefaults(userInfo)
if asAdmin {
newUser.Perm = settings.AdminPerms()
}
// create new home directory
userHome, err := settings.Config.MakeUserDir(newUser.Username, newUser.Scope, settings.Config.Server.Root)
if err != nil {
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
return err
}
newUser.Scope = userHome
log.Printf("user: %s, home dir: [%s].", newUser.Username, userHome)
_, _, err = files.GetRealPath(settings.Config.Server.Root, newUser.Scope)
if err != nil {
log.Println("user path is not valid", newUser.Scope)
return nil
}
err = store.Users.Save(&newUser)
if err != nil {
return err
}
return nil
}
+1 -21
View File
@@ -1,7 +1,6 @@
package users
import (
"log"
"sync"
"time"
@@ -54,9 +53,6 @@ func (s *Storage) Get(baseScope string, id interface{}) (user *User, err error)
if err != nil {
return
}
if err := user.Clean(baseScope); err != nil {
return nil, err
}
return user, err
}
@@ -66,23 +62,12 @@ func (s *Storage) Gets(baseScope string) ([]*User, error) {
if err != nil {
return nil, err
}
for _, user := range users {
if err := user.Clean(baseScope); err != nil { //nolint:govet
return nil, err
}
}
return users, err
}
// Update updates a user in the database.
func (s *Storage) Update(user *User, fields ...string) error {
err := user.Clean("")
if err != nil {
return err
}
err = s.back.Update(user, fields...)
err := s.back.Update(user, fields...)
if err != nil {
return err
}
@@ -137,11 +122,6 @@ func (s *Storage) DeleteRule(userID string, ruleID string) error {
// Save saves the user in a storage.
func (s *Storage) Save(user *User) error {
log.Println("Saving new user:", user.Username)
if err := user.Clean(""); err != nil {
return err
}
return s.back.Save(user)
}
+37 -54
View File
@@ -1,25 +1,12 @@
package users
import (
"path/filepath"
"regexp"
"github.com/spf13/afero"
"github.com/gtsteffaniak/filebrowser/rules"
"github.com/gtsteffaniak/filebrowser/settings"
)
type Permissions struct {
Admin bool `json:"admin"`
Execute bool `json:"execute"`
Create bool `json:"create"`
Rename bool `json:"rename"`
Modify bool `json:"modify"`
Delete bool `json:"delete"`
Share bool `json:"share"`
Download bool `json:"download"`
}
// SortingSettings represents the sorting settings.
type Sorting struct {
By string `json:"by"`
@@ -28,24 +15,24 @@ type Sorting struct {
// User describes a user.
type User struct {
StickySidebar bool `json:"stickySidebar"`
DarkMode bool `json:"darkMode"`
DisableSettings bool `json:"disableSettings"`
ID uint `storm:"id,increment" json:"id"`
Username string `storm:"unique" json:"username"`
Password string `json:"password"`
Scope string `json:"scope"`
Locale string `json:"locale"`
LockPassword bool `json:"lockPassword"`
ViewMode string `json:"viewMode"`
SingleClick bool `json:"singleClick"`
Perm Permissions `json:"perm"`
Commands []string `json:"commands"`
Sorting Sorting `json:"sorting"`
Fs afero.Fs `json:"-" yaml:"-"`
Rules []rules.Rule `json:"rules"`
HideDotfiles bool `json:"hideDotfiles"`
DateFormat bool `json:"dateFormat"`
StickySidebar bool `json:"stickySidebar"`
DarkMode bool `json:"darkMode"`
DisableSettings bool `json:"disableSettings"`
ID uint `storm:"id,increment" json:"id"`
Username string `storm:"unique" json:"username"`
Password string `json:"password"`
Scope string `json:"scope"`
Locale string `json:"locale"`
LockPassword bool `json:"lockPassword"`
ViewMode string `json:"viewMode"`
SingleClick bool `json:"singleClick"`
Perm settings.Permissions `json:"perm"`
Commands []string `json:"commands"`
Sorting Sorting `json:"sorting"`
Rules []rules.Rule `json:"rules"`
HideDotfiles bool `json:"hideDotfiles"`
DateFormat bool `json:"dateFormat"`
GallerySize int `json:"gallerySize"`
}
var PublicUser = User{
@@ -54,8 +41,7 @@ var PublicUser = User{
Scope: "./",
ViewMode: "normal",
LockPassword: true,
Fs: afero.NewMemMapFs(),
Perm: Permissions{
Perm: settings.Permissions{
Create: false,
Rename: false,
Modify: false,
@@ -71,26 +57,6 @@ func (u *User) GetRules() []rules.Rule {
return u.Rules
}
// Clean cleans up a user and verifies if all its fields
// are alright to be saved.
//
//nolint:gocyclo
func (u *User) Clean(baseScope string) error {
if u.Fs == nil {
scope := u.Scope
scope = filepath.Join(baseScope, filepath.Join("/", scope)) //nolint:gocritic
u.Fs = afero.NewBasePathFs(afero.NewOsFs(), scope)
}
return nil
}
// FullPath gets the full path for a user's relative path.
func (u *User) FullPath(path string) string {
return afero.FullBaseFsPath(u.Fs.(*afero.BasePathFs), path)
}
// CanExecute checks if an user can execute a specific command.
func (u *User) CanExecute(command string) bool {
if !u.Perm.Execute {
@@ -105,3 +71,20 @@ func (u *User) CanExecute(command string) bool {
return false
}
// Apply applies the default options to a user.
func ApplyDefaults(u User) User {
u.StickySidebar = settings.Config.UserDefaults.StickySidebar
u.DisableSettings = settings.Config.UserDefaults.DisableSettings
u.DarkMode = settings.Config.UserDefaults.DarkMode
u.Scope = settings.Config.UserDefaults.Scope
u.Locale = settings.Config.UserDefaults.Locale
u.ViewMode = settings.Config.UserDefaults.ViewMode
u.SingleClick = settings.Config.UserDefaults.SingleClick
u.Perm = settings.Config.UserDefaults.Perm
u.Sorting = settings.Config.UserDefaults.Sorting
u.Commands = settings.Config.UserDefaults.Commands
u.HideDotfiles = settings.Config.UserDefaults.HideDotfiles
u.DateFormat = settings.Config.UserDefaults.DateFormat
return u
}
+19
View File
@@ -0,0 +1,19 @@
package utils
import (
"log"
"github.com/gtsteffaniak/filebrowser/settings"
)
func CheckErr(source string, err error) {
if err != nil {
log.Fatalf("%s: %v", source, err)
}
}
func GenerateKey() []byte {
k, err := settings.GenerateKey()
CheckErr("generateKey", err)
return k
}
+3 -4
View File
@@ -1,8 +1,7 @@
package version
var (
// Version is the current File Browser version.
Version = "(0.2.7)"
// CommitSHA is the commmit sha.
CommitSHA = "(unknown)"
// Dynamically updated during build via build args
Version = "untracked"
CommitSHA = "untracked"
)
@@ -35,9 +35,7 @@ auth:
tokenExpirationTime: 2h
header: ""
method: json
command: ""
signup: false
shell: ""
frontend:
name: ""
disableExternal: false
@@ -63,7 +61,6 @@ userDefaults:
delete: true
share: true
download: true
commands: []
hideDotfiles: false
dateFormat: false
```
@@ -165,10 +162,6 @@ userDefaults:
- `oath` - oath authentication
- `noauth` - no authentication/login required.
- `command`: Deprecated: This is the authentication command.
- `shell`: This is the shell configuration.
- `Signup`: This boolean value indicates whether user signup is enabled on the login page. NOTE: Be mindful of `userDefaults` settings if enabled. Default: `false`
- `AdminUsername`: This is the username of the admin user. Default: `admin`
@@ -227,7 +220,6 @@ userDefaults:
- `download`: This boolean value determines whether download permissions are granted.
- `commands`: Deprecated: This is a list of commands.
- `hideDotfiles`: This boolean value determines whether dotfiles are hidden. (`true` or `false`)
+2
View File
@@ -0,0 +1,2 @@
# Contributing Guide
+2
View File
@@ -0,0 +1,2 @@
# Getting Started using FileBrowser Quantum
+22
View File
@@ -0,0 +1,22 @@
# Migration help
It is possible to use the same database as used by filebrowser/filebrowser,
but you will need to follow the following process:
1. Create a configuration file as mentioned above.
2. Copy your database file from the original filebrowser to the path of
the new one.
3. Update the configuration file to use the database (under server in
filebrowser.yml)
4. If you are using docker, update the docker-compose file or docker run
command to use the config file as described in the install section
above.
5. If you are not using docker, just make sure you run filebrowser -c
filebrowser.yml and have a valid filebrowser config.
Note: share links will not work and will need to be re-created after migration.
The filebrowser Quantum application should run with the same user and rules that
you have from the original. But keep in mind the differences that may not work
the same way, but all user configuration should be available.
+24
View File
@@ -0,0 +1,24 @@
# Planned Roadmap
upcoming 0.2.x releases:
- Replace http routes for gorilla/mux with stdlib
- Theme configuration from settings
- File syncronization improvements
- more filetype previews
next major 0.3.0 release :
- multiple sources https://github.com/filebrowser/filebrowser/issues/2514
- introduce jobs as replacement to runners.
- Add Job status to the sidebar
- index status.
- Job status from users
- upload status
Unplanned Future releases:
- Add tools to sidebar
- duplicate file detector.
- bulk rename https://github.com/filebrowser/filebrowser/issues/2473
- metrics tracker - user access, file access, download count, last login, etc
- support minio, s3, and backblaze sources https://github.com/filebrowser/filebrowser/issues/2544
+2 -1
View File
@@ -16,7 +16,7 @@
"lint": "npm run typecheck && eslint src/",
"lint:fix": "eslint --fix src/",
"format": "prettier --write .",
"test": "playwright test"
"test": "npx playwright test"
},
"dependencies": {
"ace-builds": "^1.24.2",
@@ -32,6 +32,7 @@
"vue-router": "^4.3.0"
},
"devDependencies": {
"@playwright/test": "^1.42.1",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-typescript": "^13.0.0",
+43
View File
@@ -0,0 +1,43 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "line",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
/* Set default locale to English (US) */
locale: "en-US",
},
/* Configure projects for major browsers */
projects: [
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
],
});
+3 -3
View File
@@ -9,7 +9,7 @@
<script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit" data-vite-ignore></script>
[{[ end ]}]
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]</title>
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]FileBrowser Quantum[{[ end ]}]</title>
<link rel="icon" type="image/png" sizes="256x256" href="[{[ .StaticURL ]}]/img/icons/favicon-256x256.png">
@@ -33,8 +33,8 @@
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
var dynamicManifest = {
"name": window.FileBrowser.Name || 'File Browser',
"short_name": window.FileBrowser.Name || 'File Browser',
"name": window.FileBrowser.Name || 'FileBrowser Quantum',
"short_name": window.FileBrowser.Name || 'FileBrowser',
"icons": [
{
"src": fullStaticURL + "/img/icons/android-chrome-256x256.png",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "File Browser",
"short_name": "File Browser",
"name": "FileBrowser",
"short_name": "FileBrowser",
"icons": [
{
"src": "./img/icons/android-chrome-192x192.png",
+5 -4
View File
@@ -3,17 +3,18 @@
</template>
<script>
import { onMounted } from 'vue';
import { onMounted } from "vue";
import { mutations } from "@/store"; // Import your store's mutations
mutations.setLoading("main-app", true);
export default {
name: "app",
computed: {},
setup() {
onMounted(() => {
mutations.setLoading(false); // Call your mutation or method to set loading to false
console.log("static vars", window.FileBrowser);
mutations.setLoading("main-app", false);
// Query the loading element and remove it from the DOM
const loadingDiv = document.getElementById('loading');
const loadingDiv = document.getElementById("loading");
if (loadingDiv) {
loadingDiv.remove();
}
+158 -102
View File
@@ -1,122 +1,151 @@
import { createURL, fetchURL, removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import { state } from "@/store";
import { notify } from "@/notify";
export async function fetch(url,content=false) {
url = removePrefix(url);
// Notify if errors occur
export async function fetch(url, content = false) {
try {
url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}?content=${content}`, {});
const res = await fetchURL(`/api/resources${url}?content=${content}`, {});
const data = await res.json();
data.url = `/files${url}`;
let data = await res.json();
data.url = `/files${url}`;
if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => {
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => {
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) {
item.url += "/";
}
if (item.isDir) {
item.url += "/";
}
return item;
});
}
return item;
});
return data;
} catch (err) {
notify.showError(err.message || "Error fetching data");
throw err;
}
return data;
}
async function resourceAction(url, method, content) {
url = removePrefix(url);
try {
url = removePrefix(url);
let opts = { method };
let opts = { method };
if (content) {
opts.body = content;
if (content) {
opts.body = content;
}
const res = await fetchURL(`/api/resources${url}`, opts);
return res;
} catch (err) {
notify.showError(err.message || "Error performing resource action");
throw err;
}
const res = await fetchURL(`/api/resources${url}`, opts);
return res;
}
export async function remove(url) {
return resourceAction(url, "DELETE");
try {
return await resourceAction(url, "DELETE");
} catch (err) {
notify.showError(err.message || "Error deleting resource");
throw err;
}
}
export async function put(url, content = "") {
return resourceAction(url, "PUT", content);
try {
return await resourceAction(url, "PUT", content);
} catch (err) {
notify.showError(err.message || "Error putting resource");
throw err;
}
}
export function download(format, ...files) {
let url = `${baseURL}/api/raw`;
try {
let url = `${baseURL}/api/raw`;
if (files.length === 1) {
url += removePrefix(files[0]) + "?";
} else {
let arg = "";
if (files.length === 1) {
url += removePrefix(files[0]) + "?";
} else {
let arg = "";
for (let file of files) {
arg += removePrefix(file) + ",";
for (let file of files) {
arg += removePrefix(file) + ",";
}
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
}
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
}
if (format) {
url += `algo=${format}&`;
}
if (format) {
url += `algo=${format}&`;
}
if (state.jwt) {
url += `auth=${state.jwt}&`;
}
if (state.jwt) {
url += `auth=${state.jwt}&`;
window.open(url);
} catch (err) {
notify.showError(err.message || "Error downloading files");
}
window.open(url);
}
export async function post(url, content = "", overwrite = false, onupload) {
url = removePrefix(url);
try {
url = removePrefix(url);
let bufferContent;
if (
content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)
) {
bufferContent = await new Response(content).arrayBuffer();
}
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest();
request.open(
"POST",
`${baseURL}/api/resources${url}?override=${overwrite}`,
true
);
request.setRequestHeader("X-Auth", state.jwt);
if (typeof onupload === "function") {
request.upload.onprogress = onupload;
let bufferContent;
if (
content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)
) {
bufferContent = await new Response(content).arrayBuffer();
}
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText);
} else if (request.status === 409) {
reject(request.status);
} else {
reject(request.responseText);
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest();
request.open(
"POST",
`${baseURL}/api/resources${url}?override=${overwrite}`,
true
);
request.setRequestHeader("X-Auth", state.jwt);
if (typeof onupload === "function") {
request.upload.onprogress = onupload;
}
};
request.onerror = () => {
reject(new Error("001 Connection aborted"));
};
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText);
} else if (request.status === 409) {
reject(request.status);
} else {
reject(request.responseText);
}
};
request.send(bufferContent || content);
});
request.onerror = () => {
reject(new Error("001 Connection aborted"));
};
request.send(bufferContent || content);
});
} catch (err) {
notify.showError(err.message || "Error posting resource");
throw err;
}
}
function moveCopy(items, copy = false, overwrite = false, rename = false) {
@@ -131,7 +160,10 @@ function moveCopy(items, copy = false, overwrite = false, rename = false) {
promises.push(resourceAction(url, "PATCH"));
}
return Promise.all(promises);
return Promise.all(promises).catch((err) => {
notify.showError(err.message || "Error moving/copying resources");
throw err;
});
}
export function move(items, overwrite = false, rename = false) {
@@ -143,44 +175,68 @@ export function copy(items, overwrite = false, rename = false) {
}
export async function checksum(url, algo) {
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
return (await data.json()).checksums[algo];
try {
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
return (await data.json()).checksums[algo];
} catch (err) {
notify.showError(err.message || "Error fetching checksum");
throw err;
}
}
export function getDownloadURL(file, inline) {
const params = {
...(inline && { inline: "true" }),
};
try {
const params = {
...(inline && { inline: "true" }),
};
return createURL("api/raw" + file.path, params);
return createURL("api/raw" + file.path, params);
} catch (err) {
notify.showError(err.message || "Error getting download URL");
throw err;
}
}
export function getPreviewURL(file, size) {
const params = {
inline: "true",
key: Date.parse(file.modified),
};
try {
const params = {
inline: "true",
key: Date.parse(file.modified),
};
return createURL("api/preview/" + size + file.path, params);
return createURL("api/preview/" + size + file.path, params);
} catch (err) {
notify.showError(err.message || "Error getting preview URL");
throw err;
}
}
export function getSubtitlesURL(file) {
const params = {
inline: "true",
};
try {
const params = {
inline: "true",
};
const subtitles = [];
for (const sub of file.subtitles) {
subtitles.push(createURL("api/raw" + sub, params));
const subtitles = [];
for (const sub of file.subtitles) {
subtitles.push(createURL("api/raw" + sub, params));
}
return subtitles;
} catch (err) {
notify.showError(err.message || "Error fetching subtitles URL");
throw err;
}
return subtitles;
}
export async function usage(url) {
url = removePrefix(url);
try {
url = removePrefix(url);
const res = await fetchURL(`/api/usage${url}`, {});
return await res.json();
const res = await fetchURL(`/api/usage${url}`, {});
return await res.json();
} catch (err) {
notify.showError(err.message || "Error fetching usage data");
throw err;
}
}
+2 -1
View File
@@ -84,5 +84,6 @@ export function getDownloadURL(share, inline = false) {
if (share.path == undefined) {
share.path = ""
}
return createURL("api/public/dl/" + share.hash + "/"+share.path, params, false);
const path = share.path.replace("/share/"+share.hash +"/","")
return createURL("api/public/dl/" + share.hash + "/"+path, params, false);
}
+19 -18
View File
@@ -1,27 +1,28 @@
import { fetchURL, removePrefix } from "./utils";
import url from "../utils/url";
import { notify } from "@/notify"; // Import notify for error handling
export default async function search(base, query) {
base = removePrefix(base);
query = encodeURIComponent(query);
try {
base = removePrefix(base);
query = encodeURIComponent(query);
if (!base.endsWith("/")) {
base += "/";
}
let res = await fetchURL(`/api/search${base}?query=${query}`, {});
let data = await res.json();
data = data.map((item) => {
item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) {
item.url += "/";
if (!base.endsWith("/")) {
base += "/";
}
return item;
});
const res = await fetchURL(`/api/search${base}?query=${query}`, {});
return data;
let data = await res.json();
data = data.map((item) => {
item.url = `/files${base}` + url.encodePath(item.path);
return item;
});
return data;
} catch (err) {
notify.showError(err.message || "Error occurred during search");
throw err;
}
}
+57 -28
View File
@@ -1,47 +1,76 @@
import { fetchURL, fetchJSON } from "@/api/utils";
import { notify } from "@/notify"; // Import notify for error handling
export async function getAllUsers() {
return await fetchJSON(`/api/users`, {});
try {
return await fetchJSON(`/api/users`, {});
} catch (err) {
notify.showError(err.message || "Failed to fetch users");
throw err; // Re-throw to handle further if needed
}
}
export async function get(id) {
return fetchJSON(`/api/users/${id}`, {});
try {
return await fetchJSON(`/api/users/${id}`, {});
} catch (err) {
notify.showError(err.message || `Failed to fetch user with ID: ${id}`);
throw err;
}
}
export async function create(user) {
const res = await fetchURL(`/api/users`, {
method: "POST",
body: JSON.stringify({
what: "user",
which: [],
data: user,
}),
});
try {
const res = await fetchURL(`/api/users`, {
method: "POST",
body: JSON.stringify({
what: "user",
which: [],
data: user,
}),
});
if (res.status === 201) {
return res.headers.get("Location");
if (res.status === 201) {
return res.headers.get("Location");
} else {
throw new Error("Failed to create user");
}
} catch (err) {
notify.showError(err.message || "Error creating user");
throw err;
}
}
export async function update(user, which = ["all"]) {
if (which[0] != "password") {
user.password = "";
try {
// List of keys to exclude from the "which" array
const excludeKeys = ["id", "name"];
// Filter out the keys from "which"
which = which.filter(item => !excludeKeys.includes(item));
if (user.username === "publicUser") {
return;
}
await fetchURL(`/api/users/${user.id}`, {
method: "PUT",
body: JSON.stringify({
what: "user",
which: which,
data: user,
}),
});
} catch (err) {
notify.showError(err.message || `Failed to update user with ID: ${user.id}`);
throw err;
}
if (user.username == "publicUser") {
return
}
await fetchURL(`/api/users/${user.id}`, {
method: "PUT",
body: JSON.stringify({
what: "user",
which: which,
data: user,
}),
});
}
export async function remove(id) {
await fetchURL(`/api/users/${id}`, {
method: "DELETE",
});
try {
await fetchURL(`/api/users/${id}`, {
method: "DELETE",
});
} catch (err) {
notify.showError(err.message || `Failed to delete user with ID: ${id}`);
throw err;
}
}
+2 -2
View File
@@ -2,7 +2,7 @@ import { state } from "@/store";
import { renew, logout } from "@/utils/auth";
import { baseURL } from "@/utils/constants";
import { encodePath } from "@/utils/url";
import { showError } from "@/notify";
import { notify } from "@/notify";
export async function fetchURL(url, opts, auth = true) {
opts = opts || {};
@@ -51,7 +51,7 @@ export async function fetchJSON(url, opts) {
if (res.status === 200) {
return res.json();
} else {
showError("unable to fetch : " + url + "status" + res.status);
notify.showError("unable to fetch : " + url + "status" + res.status);
throw new Error(res.status);
}
}
File renamed without changes.
+30 -10
View File
@@ -14,20 +14,42 @@
<component :is="element" :to="link.url">{{ link.name }}</component>
</span>
<action style="display: contents" v-if="showShare" icon="share" show="share" />
<div v-if="isCardView">
Size:
<input
v-model="gallerySize"
type="range"
id="gallery-size"
name="gallery-size"
:value="gallerySize"
min="0"
max="10"
@input="updateGallerySize"
@change="commitGallerySize"
/>
</div>
</div>
</template>
<script>
import { state, mutations } from "@/store"; // Import mutations as well
import Action from "@/components/header/Action.vue";
import { state, mutations, getters } from "@/store";
import Action from "@/components/Action.vue";
export default {
name: "breadcrumbs",
components: {
Action,
},
data() {
return {
gallerySize: state.user.gallerySize,
};
},
props: ["base", "noLink"],
computed: {
isCardView() {
return getters.isCardView();
},
items() {
const relativePath = state.route.path.replace(this.base, "");
let parts = relativePath.split("/");
@@ -74,17 +96,15 @@ export default {
return "router-link";
},
showShare() {
// Ensure user properties are accessed safely
if (state.route.path.startsWith("/share")) {
return false;
}
return state.user?.perm && state.user?.perm.share; // Access from state directly
return state.user?.perm && state.user?.perm.share;
},
},
methods: {
// Example of a method using mutations
updateUserPermissions(newPerms) {
mutations.updateUser({ perm: newPerms })
updateGallerySize(event) {
this.gallerySize = parseInt(event.target.value, 10);
},
commitGallerySize() {
mutations.setGallerySize(this.gallerySize);
},
},
};
+230
View File
@@ -0,0 +1,230 @@
<template>
<div
id="context-menu"
ref="contextMenu"
v-show="showContext"
:style="{
top: `${top}px`,
left: `${left}px`,
}"
class="button"
:class="{ 'dark-mode': isDarkMode, mobile: isMobile }"
>
<div v-if="selectedCount > 0" class="button selected-count-header">
<span>{{ selectedCount }} selected</span>
</div>
<action
v-if="!headerButtons.select"
icon="create_new_folder"
:label="$t('sidebar.newFolder')"
@action="showHover('newDir')"
/>
<action
v-if="!headerButtons.select"
icon="note_add"
:label="$t('sidebar.newFile')"
@action="showHover('newFile')"
/>
<action
v-if="!headerButtons.select"
icon="file_upload"
:label="$t('buttons.upload')"
@action="uploadFunc"
/>
<action
v-if="headerButtons.select"
icon="info"
:label="$t('buttons.info')"
show="info"
/>
<action
v-if="!isMultiple"
icon="check_circle"
:label="$t('buttons.selectMultiple')"
@action="toggleMultipleSelection"
/>
<action
v-if="headerButtons.download"
icon="file_download"
:label="$t('buttons.download')"
@action="startDownload"
:counter="selectedCount"
/>
<action
v-if="headerButtons.share"
icon="share"
:label="$t('buttons.share')"
show="share"
/>
<action
v-if="headerButtons.rename"
icon="mode_edit"
:label="$t('buttons.rename')"
show="rename"
/>
<action
v-if="headerButtons.copy"
icon="content_copy"
:label="$t('buttons.copyFile')"
show="copy"
/>
<action
v-if="headerButtons.move"
icon="forward"
:label="$t('buttons.moveFile')"
show="move"
/>
<action
v-if="headerButtons.delete"
icon="delete"
:label="$t('buttons.delete')"
show="delete"
/>
</div>
</template>
<script>
import downloadFiles from "@/utils/download";
import { state, getters, mutations } from "@/store"; // Import your custom store
import Action from "@/components/Action.vue";
export default {
name: "ContextMenu",
components: {
Action,
},
data() {
return {
posX: 0,
posY: 0,
};
},
computed: {
isMultiple() {
return state.multiple;
},
user() {
return state.user;
},
isMobile() {
return getters.isMobile();
},
showContext() {
if (getters.currentPromptName() == "ContextMenu" && state.prompts != []) {
this.setPositions();
return true;
}
return false;
},
top() {
// Ensure the context menu stays within the viewport
return Math.min(
this.posY,
window.innerHeight - (this.$refs.contextMenu?.clientHeight ?? 0)
);
},
left() {
return Math.min(
this.posX,
window.innerWidth - (this.$refs.contextMenu?.clientWidth ?? 0)
);
},
isDarkMode() {
return getters.isDarkMode();
},
headerButtons() {
return {
select: state.selected.length > 0,
upload: state.user.perm?.create && state.selected.length > 0,
download: state.user.perm.download && state.selected.length > 0,
delete: state.selected.length > 0 && state.user.perm.delete,
rename: state.selected.length === 1 && state.user.perm.rename,
share: state.selected.length === 1 && state.user.perm.share,
move: state.selected.length > 0 && state.user.perm.rename,
copy: state.selected.length > 0 && state.user.perm?.create,
};
},
selectedCount() {
return getters.selectedCount();
},
},
methods: {
uploadFunc() {
mutations.showHover("upload");
},
showHover(value) {
return mutations.showHover(value);
},
setPositions() {
const contextProps = getters.currentPrompt().props;
this.posX = contextProps.posX;
this.posY = contextProps.posY;
},
toggleMultipleSelection() {
mutations.setMultiple(!state.multiple);
mutations.closeHovers();
},
startDownload() {
downloadFiles();
},
},
};
</script>
<style scoped>
#context-menu {
position: absolute;
z-index: 1000;
background-color: white;
max-width: 20em;
min-width: 15em;
min-height: 4em;
height: auto;
display: flex;
flex-direction: column;
justify-content: center;
}
#context-menu.mobile {
top: 50% !important;
left: 50% !important;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.selected-count-header {
border-radius: 0.5em;
cursor: unset;
margin-bottom: 0.5em;
}
#context-menu .action {
width: auto;
display: flex;
align-items: center;
}
#context-menu > span {
display: inline-block;
margin-left: 1em;
color: #6f6f6f;
margin-right: auto;
}
#context-menu .action span {
display: none;
}
/* File selection */
#context-menu.dark-mode {
background: var(--surfaceSecondary) !important;
}
#context-menu.dark-mode span {
color: var(--textPrimary) !important;
}
</style>
-168
View File
@@ -1,168 +0,0 @@
<template>
<div v-if="selectedCount > 0" id="file-selection" :class="{ 'dark-mode': isDarkMode }">
<span>{{ selectedCount }} selected</span>
<div>
<action
v-if="headerButtons.select"
icon="info"
:label="$t('buttons.info')"
show="info"
/>
<action
v-if="headerButtons.select"
icon="check_circle"
:label="$t('buttons.selectMultiple')"
@action="toggleMultipleSelection"
/>
<action
v-if="headerButtons.download"
icon="file_download"
:label="$t('buttons.download')"
@action="download"
:counter="selectedCount"
/>
<action
v-if="headerButtons.share"
icon="share"
:label="$t('buttons.share')"
show="share"
/>
<action
v-if="headerButtons.rename"
icon="mode_edit"
:label="$t('buttons.rename')"
show="rename"
/>
<action
v-if="headerButtons.copy"
icon="content_copy"
:label="$t('buttons.copyFile')"
show="copy"
/>
<action
v-if="headerButtons.move"
icon="forward"
:label="$t('buttons.moveFile')"
show="move"
/>
<action
v-if="headerButtons.delete"
icon="delete"
:label="$t('buttons.delete')"
show="delete"
/>
</div>
</div>
</template>
<script>
import { state, getters, mutations } from "@/store"; // Import your custom store
import { files as api } from "@/api";
import Action from "@/components/header/Action.vue";
export default {
name: "fileSelection",
components: {
Action,
},
computed: {
isDarkMode() {
return getters.isDarkMode();
},
headerButtons() {
return {
select: state.selected.length > 0,
upload: state.user.perm?.create && state.selected.length > 0,
download: state.user.perm.download && state.selected.length > 0,
delete: state.selected.length > 0 && state.user.perm.delete,
rename: state.selected.length === 1 && state.user.perm.rename,
share: state.selected.length === 1 && state.user.perm.share,
move: state.selected.length > 0 && state.user.perm.rename,
copy: state.selected.length > 0 && state.user.perm?.create,
};
},
selectedCount() {
return getters.selectedCount();
},
},
methods: {
toggleMultipleSelection() {
mutations.setMultiple(!state.multiple);
mutations.closeHovers();
},
download() {
if (getters.isSingleFileSelected()) {
api.download(null, getters.selectedDownloadUrl());
return;
}
mutations.showHover({
name: "download",
confirm: (format) => {
mutations.closeHovers();
let files = [];
if (state.selected.length > 0) {
for (let i of state.selected) {
files.push(state.req.items[i].url);
}
} else {
files.push(state.route.path);
}
try {
api.download(format, ...files);
showSuccess("download started");
} catch (e) {
showError("error downloading", e);
}
},
});
},
},
};
</script>
<style>
@media (min-width: 800px) {
#file-selection {
bottom: 4em;
}
}
#file-selection .action {
border-radius: 50%;
width: auto;
}
#file-selection > span {
display: inline-block;
margin-left: 1em;
color: #6f6f6f;
margin-right: auto;
}
#file-selection .action span {
display: none;
}
/* File Selection */
#file-selection {
box-shadow: rgba(0, 0, 0, 0.3) 0px 2em 50px 10px;
position: fixed;
bottom: 4em;
left: 50%;
transform: translateX(-50%);
align-items: center;
background: #fff;
max-width: 30em;
z-index: 3;
border-radius: 1em;
display: flex;
width: 90%;
}
/* File selection */
#file-selection.dark-mode {
background: var(--surfaceSecondary) !important;
}
#file-selection.dark-mode span {
color: var(--textPrimary) !important;
}
</style>
+11
View File
@@ -219,3 +219,14 @@ export default {
},
};
</script>
<style>
.vue-simple-progress {
margin: 0.5em;
}
.vue-simple-progress,
.vue-simple-progress-bar {
border-radius: 0.5em;
}
</style>
+24 -56
View File
@@ -1,7 +1,7 @@
<template>
<div id="search" @click="open" :class="{ active, ongoing, 'dark-mode': isDarkMode }">
<div id="search" :class="{ active, ongoing, 'dark-mode': isDarkMode }">
<!-- Search input section -->
<div id="input">
<div id="input" @click="open">
<!-- Close button visible when search is active -->
<button
v-if="active"
@@ -16,6 +16,7 @@
<i v-else class="material-icons">search</i>
<!-- Input field for search -->
<input
id="main-input"
class="main-input"
type="text"
@keyup.exact="keyup"
@@ -34,13 +35,8 @@
<div class="button" style="width: 100%">Search Context: {{ getContext }}</div>
<!-- List of search results -->
<ul v-show="results.length > 0">
<li
v-for="(s, k) in results"
:key="k"
@click.stop.prevent="navigateTo(s.url)"
style="cursor: pointer"
>
<router-link to="#" event="">
<li v-for="(s, k) in results" :key="k" style="cursor: pointer">
<router-link :to="s.url">
<i v-if="s.dir" class="material-icons folder-icons"> folder </i>
<i v-else-if="s.audio" class="material-icons audio-icons"> volume_up </i>
<i v-else-if="s.image" class="material-icons image-icons"> photo </i>
@@ -170,20 +166,11 @@
<b>Multiple Search terms:</b> Additional terms separated by <code>|</code>,
for example <code>"test|not"</code> searches for both terms independently.
</p>
<p>
<b>File size:</b> Searching files by size may have significantly longer search
times.
</p>
</div>
<!-- List of search results -->
<ul v-show="results.length > 0">
<li
v-for="(s, k) in results"
:key="k"
@click.stop.prevent="navigateTo(s.url)"
style="cursor: pointer"
>
<router-link to="#" event="">
<li v-for="(s, k) in results" :key="k" style="cursor: pointer">
<router-link :to="s.url">
<i v-if="s.dir" class="material-icons folder-icons"> folder </i>
<i v-else-if="s.audio" class="material-icons audio-icons"> volume_up </i>
<i v-else-if="s.image" class="material-icons image-icons"> photo </i>
@@ -204,7 +191,6 @@
import ButtonGroup from "./ButtonGroup.vue";
import { search } from "@/api";
import { getters, mutations, state } from "@/store";
import { showError } from "@/notify";
var boxes = {
folder: { label: "folders", icon: "folder" },
@@ -258,34 +244,20 @@ export default {
this.submit();
},
active(active) {
// this is hear to allow for animation
const resultList = document.getElementById("result-list");
if (!active) {
resultList.classList.remove("active");
this.value = "";
event.stopPropagation();
mutations.closeHovers();
return;
}
setTimeout(() => {
resultList.classList.add("active");
document.getElementById("main-input").focus();
}, 100);
},
currentPrompt(val, old) {
this.active = val?.prompt === "search";
if (old?.prompt === "search" && !this.active) {
if (this.reload) {
this.setReload(true);
}
document.body.style.overflow = "auto";
this.ongoing = false;
this.results = [];
this.value = "";
this.active = false;
this.$refs.input.blur();
} else if (this.active) {
this.reload = false;
this.$refs.input.focus();
document.body.style.overflow = "hidden";
}
},
value() {
if (this.results.length) {
this.ongoing = false;
@@ -303,9 +275,6 @@ export default {
active() {
return getters.currentPromptName() == "search";
},
showOverlay() {
return getters.currentPrompt() !== null && getters.currentPromptName() !== "more";
},
isDarkMode() {
return getters.isDarkMode();
},
@@ -338,15 +307,13 @@ export default {
path = path.slice(1);
path = "./" + path.substring(path.indexOf("/") + 1);
path = path.replace(/\/+$/, "") + "/";
if (path == "./files/") {
path = "./";
}
return path;
},
},
methods: {
async navigateTo(url) {
mutations.closeHovers();
await this.$nextTick();
setTimeout(() => this.$router.push(url), 10);
},
basePath(str, isDir) {
let parts = str.replace(/(\/$|^\/)/, "").split("/");
if (parts.length <= 1) {
@@ -360,6 +327,7 @@ export default {
if (isDir) {
parts = "/" + parts; // fix weird rtl thing
}
return parts;
},
baseName(str) {
@@ -367,7 +335,9 @@ export default {
return parts.pop();
},
open() {
mutations.showHover("search");
if (!this.active) {
mutations.showHover("search");
}
},
close(event) {
this.value = "";
@@ -420,19 +390,17 @@ export default {
return;
}
let searchTypesFull = this.searchTypes;
if (this.largerThan != "") {
if (this.largerThan != "" && !this.isTypeSelectDisabled) {
searchTypesFull = searchTypesFull + "type:largerThan=" + this.largerThan + " ";
}
if (this.smallerThan != "") {
if (this.smallerThan != "" && !this.isTypeSelectDisabled) {
searchTypesFull = searchTypesFull + "type:smallerThan=" + this.smallerThan + " ";
}
let path = state.route.path;
this.ongoing = true;
try {
this.results = await search(path, searchTypesFull + this.value);
} catch (error) {
showError(error);
}
this.results = await search(path, searchTypesFull + this.value);
this.ongoing = false;
if (this.results.length == 0) {
this.noneMessage = "No results found in indexed search.";
-461
View File
@@ -1,461 +0,0 @@
<template>
<nav
id="sidebar"
:class="{ active: active, 'dark-mode': isDarkMode, sticky: user?.stickySidebar }"
>
<div class="card">
<div class="card-wrapper">
<button
v-if="user.username"
@click="navigateTo('/settings/profile')"
class="action"
>
<i class="material-icons">person</i>
<span>{{ user.username }}</span>
</button>
</div>
</div>
<div class="card">
<div class="card-wrapper" @mouseleave="resetHoverTextToDefault">
<span>{{ hoverText }}</span>
<div class="quick-toggles">
<div
:class="{ active: user?.singleClick }"
@click="toggleClick"
@mouseover="updateHoverText('Toggle single click')"
>
<i class="material-icons">ads_click</i>
</div>
<div
:class="{ active: user?.darkMode }"
@click="toggleDarkMode"
@mouseover="updateHoverText('Toggle dark mode')"
>
<i class="material-icons">dark_mode</i>
</div>
<div
:class="{ active: user?.stickySidebar }"
@click="toggleSticky"
@mouseover="updateHoverText('Toggle sticky sidebar')"
v-if="!isMobile"
>
<i class="material-icons">push_pin</i>
</div>
</div>
</div>
</div>
<!-- Section for logged-in users -->
<div v-if="isLoggedIn" class="sidebar-scroll-list">
<!-- Buttons visible if user has create permission -->
<div v-if="user.perm?.create">
<!-- New Folder button -->
<button
@click="showHover('newDir')"
class="action"
:aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')"
>
<i class="material-icons">create_new_folder</i>
<span>{{ $t("sidebar.newFolder") }}</span>
</button>
<!-- New File button -->
<button
@click="showHover('newFile')"
class="action"
:aria-label="$t('sidebar.newFile')"
:title="$t('sidebar.newFile')"
>
<i class="material-icons">note_add</i>
<span>{{ $t("sidebar.newFile") }}</span>
</button>
<!-- Upload button -->
<button id="upload-button" @click="uploadFunc" class="action">
<i class="material-icons">file_upload</i>
<span>Upload file</span>
</button>
</div>
<!-- Settings and Logout buttons -->
<div>
<!-- Settings button -->
<button
class="action"
@click="navigateTo('/settings/global')"
:aria-label="$t('sidebar.settings')"
:title="$t('sidebar.settings')"
>
<i class="material-icons">settings_applications</i>
<span>{{ $t("sidebar.settings") }}</span>
</button>
<!-- Logout button -->
<button
v-if="canLogout"
@click="logout"
class="action"
id="logout"
:aria-label="$t('sidebar.logout')"
:title="$t('sidebar.logout')"
>
<i class="material-icons">exit_to_app</i>
<span>{{ $t("sidebar.logout") }}</span>
</button>
</div>
<div v-if="isLoggedIn" class="sources card">
<span>Sources</span>
<div class="inner-card">
<!-- My Files button -->
<button
class="action"
@click="navigateTo('/files/')"
:aria-label="$t('sidebar.myFiles')"
:title="$t('sidebar.myFiles')"
>
<i class="material-icons">folder</i>
<span>{{ $t("sidebar.myFiles") }}</span>
<div class="usage-info">
<progress-bar :val="usage.usedPercentage" size="medium"></progress-bar>
<span style="text-align: center">{{ usage.usedPercentage }}%</span>
<span>{{ usage.used }} of {{ usage.total }} used</span>
</div>
</button>
</div>
</div>
</div>
<!-- Section for non-logged-in users -->
<div v-else class="sidebar-scroll-list">
<!-- Login button -->
<router-link
class="action"
to="/login"
:aria-label="$t('sidebar.login')"
:title="$t('sidebar.login')"
>
<i class="material-icons">exit_to_app</i>
<span>{{ $t("sidebar.login") }}</span>
</router-link>
<!-- Signup button, if signup is enabled -->
<router-link
v-if="signup"
class="action"
to="/login"
:aria-label="$t('sidebar.signup')"
:title="$t('sidebar.signup')"
>
<i class="material-icons">person_add</i>
<span>{{ $t("sidebar.signup") }}</span>
</router-link>
</div>
<div class="buffer"></div>
<div class="credits">
<span>
<a
rel="noopener noreferrer"
target="_blank"
href="https://github.com/gtsteffaniak/filebrowser"
>
File Browser
</a>
</span>
<span>{{ version }}</span>
<span>
<a @click="help">{{ $t("sidebar.help") }}</a>
</span>
</div>
</nav>
</template>
<script>
import * as auth from "@/utils/auth";
import {
version,
signup,
disableExternal,
disableUsedPercentage,
noAuth,
loginPage,
} from "@/utils/constants";
import { files, users } from "@/api";
import ProgressBar from "@/components/ProgressBar.vue";
import { getHumanReadableFilesize } from "@/utils/filesizes";
import { state, getters, mutations } from "@/store"; // Import your custom store
import { showError } from "@/notify";
export default {
name: "sidebar",
components: {
ProgressBar,
},
data() {
return {
hoverText: "Quick Toggles", // Initially empty
};
},
mounted() {
if (getters.isLoggedIn()) {
this.updateUsage();
}
},
computed: {
isMobile() {
return getters.isMobile();
},
isFiles() {
return getters.isFiles();
},
user() {
if (!getters.isLoggedIn()) {
return {};
}
return state.user;
},
isDarkMode() {
return getters.isDarkMode();
},
isLoggedIn() {
return getters.isLoggedIn();
},
currentPrompt() {
return getters.currentPrompt();
},
active() {
return getters.isSidebarVisible();
},
signup: () => signup,
version: () => version,
disableExternal: () => disableExternal,
disableUsedPercentage: () => disableUsedPercentage,
canLogout: () => !noAuth && loginPage,
usage: () => state.usage,
route: () => state.route,
},
watch: {
route() {
if (!getters.isLoggedIn()) {
return;
}
if (!state.user.stickySidebar) {
mutations.closeSidebar();
}
},
},
methods: {
updateHoverText(text) {
this.hoverText = text;
},
resetHoverTextToDefault() {
this.hoverText = "Quick Toggles"; // Reset to default hover text
},
toggleClick() {
mutations.updateUser({ singleClick: !state.user.singleClick });
},
toggleDarkMode() {
mutations.toggleDarkMode();
},
toggleSticky() {
let newSettings = state.user;
newSettings.stickySidebar = !state.user.stickySidebar;
users.update(newSettings, ["stickySidebar"]);
},
async updateUsage() {
if (!getters.isLoggedIn()) {
return;
}
let path = getters.getRoutePath();
let usageStats = { used: "0 B", total: "0 B", usedPercentage: 0 };
if (this.disableUsedPercentage) {
return usageStats;
}
try {
let usage = await files.usage(path);
usageStats = {
used: getHumanReadableFilesize(usage.used / 1024),
total: getHumanReadableFilesize(usage.total / 1024),
usedPercentage: Math.round((usage.used / usage.total) * 100),
};
} catch (error) {
showError("Error fetching usage", error);
}
mutations.setUsage(usageStats);
},
showHover(value) {
return mutations.showHover(value);
},
navigateTo(path) {
this.$router.push({ path: path }, () => {});
mutations.closeHovers();
},
// Show the help overlay
help() {
mutations.showHover("help");
},
uploadFunc() {
mutations.showHover("upload");
},
// Logout the user
logout: auth.logout,
},
};
</script>
<style>
.sidebar-scroll-list {
overflow: scroll;
margin-bottom: 0px !important;
}
#sidebar {
top: 0;
display: flex;
flex-direction: column;
padding: 1em;
padding-top: 5em;
width: 20em;
position: fixed;
z-index: 4;
left: -20em;
height: 100%;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
transition: 0.5s ease;
background-color: #ededed;
}
#sidebar.sticky {
z-index: 3;
}
@supports (backdrop-filter: none) {
nav {
background-color: transparent;
backdrop-filter: blur(16px) invert(0.1);
}
}
.usage-info {
padding: 0.5em;
}
body.rtl nav {
left: unset;
right: -17em;
}
#sidebar.active {
left: 0;
}
#sidebar.rtl nav.active {
left: unset;
right: 0;
}
#sidebar > div {
border-top: 1px solid rgba(0, 0, 0, 0.05);
margin-bottom: 0.5em;
}
#sidebar .card {
overflow: unset !important;
}
#sidebar .action {
width: 100%;
display: block;
white-space: nowrap;
height: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
body.rtl .action {
direction: rtl;
text-align: right;
}
#sidebar .action > * {
vertical-align: middle;
}
/* * * * * * * * * * * * * * * *
* FOOTER *
* * * * * * * * * * * * * * * */
.credits {
font-size: 1em;
color: var(--textSecondary);
padding-left: 1em;
}
.credits > span {
display: block;
margin-top: 0.5em;
margin-left: 0;
}
.credits a,
.credits a:hover {
color: inherit;
cursor: pointer;
}
.buffer {
flex-grow: 1;
}
.quick-toggles {
display: flex;
justify-content: space-evenly;
width: 100%;
margin-top: 0.5em !important;
}
.quick-toggles button {
border-radius: 10em;
cursor: pointer;
flex: none;
}
.card-wrapper {
display: flex !important;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 1em !important;
min-height: 4em;
box-shadow: 0 2px 2px #00000024, 0 1px 5px #0000001f, 0 3px 1px -2px #0003;
/* overflow: auto; */
border-radius: 1em;
height: 100%;
}
.sources {
padding: 1em;
margin-top: 0.5em !important;
}
.inner-card {
border-radius: 0.5em;
padding: 0px !important;
}
.quick-toggles div {
border-radius: 10em;
background-color: var(--surfaceSecondary);
}
.quick-toggles div i {
font-size: 2em;
padding: 0.25em;
border-radius: 10em;
cursor: pointer;
}
button.action {
border-radius: 0.5em;
}
.quick-toggles .active {
background-color: var(--blue) !important;
border-radius: 10em;
}
</style>
+34 -73
View File
@@ -10,20 +10,23 @@
@mouseup="mouseUp"
@wheel="wheelMove"
>
<div v-if="!isLoaded">Loading image...</div>
<img
v-if="!isTiff"
v-if="!isTiff && isLoaded"
:src="src"
class="image-ex-img image-ex-img-center"
class="image-ex-img"
ref="imgex"
@load="onLoad"
/>
<canvas v-else ref="imgex" class="image-ex-img"></canvas>
<canvas v-else-if="isLoaded" ref="imgex" class="image-ex-img"></canvas>
</div>
</template>
<script>
import { state, mutations } from "@/store";
import throttle from "@/utils/throttle";
import { showError } from "@/notify";
import { notify } from "@/notify";
export default {
props: {
src: String,
@@ -82,8 +85,17 @@ export default {
window.removeEventListener("resize", this.onResize);
document.removeEventListener("mouseup", this.onMouseUp);
},
computed: {
isLoaded() {
return !("preview-img" in state.loading);
},
},
watch: {
src: function () {
if (this.src == undefined || this.$refs.imgex == undefined) {
mutations.setLoading("preview-img", false);
return;
}
this.isTiff = this.checkIfTiff(this.src);
if (this.isTiff) {
this.decodeTiff(this.src);
@@ -94,6 +106,8 @@ export default {
this.scale = 1;
this.setZoom();
this.setCenter();
mutations.setLoading("preview-img", false);
this.showSpinner = false;
},
},
methods: {
@@ -117,40 +131,9 @@ export default {
imgex.onload = () => URL.revokeObjectURL(imgex.src); // Clean up URL object after loading
}
} catch (error) {
showError("Error decoding TIFF");
console.error("Error decoding TIFF:", error);
notify.showError("Error decoding TIFF");
}
},
onLoad() {
let img = this.$refs.imgex;
this.imageLoaded = true;
if (img === undefined) {
return;
}
img.classList.remove("image-ex-img-center");
this.setCenter();
img.classList.add("image-ex-img-ready");
document.addEventListener("mouseup", this.onMouseUp);
let realSize = img.naturalWidth;
let displaySize = img.offsetWidth;
// Image is in portrait orientation
if (img.naturalHeight > img.naturalWidth) {
realSize = img.naturalHeight;
displaySize = img.offsetHeight;
}
// Scale needed to display the image on full size
const fullScale = realSize / displaySize;
// Full size plus additional zoom
this.maxScale = fullScale + 4;
},
onMouseUp() {
this.inDrag = false;
},
@@ -258,32 +241,19 @@ export default {
}
},
doMove(x, y) {
let style = this.$refs.imgex.style;
let posX = this.pxStringToNumber(style.left) + x;
let posY = this.pxStringToNumber(style.top) + y;
style.left = posX + "px";
style.top = posY + "px";
this.position.relative.x = Math.abs(this.position.center.x - posX);
this.position.relative.y = Math.abs(this.position.center.y - posY);
if (posX < this.position.center.x) {
this.position.relative.x = this.position.relative.x * -1;
}
if (posY < this.position.center.y) {
this.position.relative.y = this.position.relative.y * -1;
}
this.position.relative.x += x;
this.position.relative.y += y;
// Update the transform with separate translate and scale values
this.$refs.imgex.style.transform = `translate(${this.position.relative.x}px, ${this.position.relative.y}px) scale(${this.scale})`;
},
wheelMove(event) {
this.scale += -Math.sign(event.deltaY) * this.zoomStep;
this.setZoom();
},
setZoom() {
this.scale = this.scale < this.minScale ? this.minScale : this.scale;
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale;
this.$refs.imgex.style.transform = `scale(${this.scale})`;
this.scale = Math.max(this.minScale, Math.min(this.maxScale, this.scale));
// Update the transform with both translate and scale values
this.$refs.imgex.style.transform = `translate(${this.position.relative.x}px, ${this.position.relative.y}px) scale(${this.scale})`;
},
pxStringToNumber(style) {
return +style.replace("px", "");
@@ -294,26 +264,17 @@ export default {
<style>
.image-ex-container {
margin: auto;
overflow: hidden;
position: relative;
max-width: 100%; /* Image container max width */
max-height: 100%; /* Image container max height */
overflow: hidden; /* Hide overflow if image exceeds container */
position: relative; /* Required for absolute positioning of child */
display: flex;
justify-content: center;
}
.image-ex-img {
max-width: 100%; /* Image max width */
max-height: 100%; /* Image max height */
position: absolute;
}
.image-ex-img-center {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
position: absolute;
transition: none;
}
.image-ex-img-ready {
left: 0;
top: 0;
transition: transform 0.1s ease;
}
</style>
+28 -41
View File
@@ -1,47 +1,45 @@
<template>
<div
:class="{ activebutton: this.isMaximized && this.isSelected }"
class="item"
<component
:is="quickNav ? 'a' : 'div'"
:href="quickNav ? url : undefined"
:class="{
item: true,
activebutton: isMaximized && isSelected,
}"
role="button"
tabindex="0"
:draggable="isDraggable"
@dragstart="dragStart"
@dragover="dragOver"
@drop="drop"
@click="itemClick"
:data-dir="isDir"
:data-type="type"
:aria-label="name"
:aria-selected="isSelected"
@click="quickNav ? toggleClick() : itemClick($event)"
>
<div
@click="toggleClick"
:class="{ activetitle: this.isMaximized && this.isSelected }"
>
<div @click="toggleClick" :class="{ activetitle: isMaximized && isSelected }">
<img
v-if="readOnly === undefined && type === 'image' && isThumbsEnabled && isInView"
v-lazy="thumbnailUrl"
:class="{ activeimg: this.isMaximized && this.isSelected }"
:class="{ activeimg: isMaximized && isSelected }"
ref="thumbnail"
/>
<i
:class="{ iconActive: this.isMaximized && this.isSelected }"
:class="{ iconActive: isMaximized && isSelected }"
v-else
class="material-icons"
></i>
</div>
<div :class="{ activecontent: this.isMaximized && this.isSelected }">
<div class="text" :class="{ activecontent: isMaximized && isSelected }">
<p class="name">{{ name }}</p>
<p v-if="isDir" class="size" data-order="-1">&mdash;</p>
<p v-else class="size" :data-order="humanSize()">{{ humanSize() }}</p>
<p class="size" :data-order="humanSize()">{{ humanSize() }}</p>
<p class="modified">
<time :datetime="modified">{{ humanTime() }}</time>
</p>
</div>
</div>
</component>
</template>
<style>
@@ -75,7 +73,7 @@ import { state, getters, mutations } from "@/store"; // Import your custom store
export default {
name: "item",
data: function () {
data() {
return {
isThumbnailInView: false,
isMaximized: false,
@@ -94,33 +92,21 @@ export default {
"path",
],
computed: {
quickNav() {
return state.user.singleClick && !state.multiple;
},
user() {
return state.user;
},
selected() {
return state.selected;
},
req() {
return state.req;
},
jwt() {
return state.jwt;
},
selectedCount() {
return getters.selectedCount();
},
isClicked() {
if (state.user.singleClick || !this.allowedView) {
return false;
}
return !this.isMaximized;
},
allowedView() {
return state.user.viewMode != "gallery" && state.user.viewMode != "normal";
},
singleClick() {
return this.readOnly == undefined && state.user.singleClick;
},
isSelected() {
return this.selected.indexOf(this.index) !== -1;
},
@@ -183,18 +169,18 @@ export default {
toggleClick() {
this.isMaximized = this.isClicked;
},
humanSize: function () {
humanSize() {
return this.type == "invalid_link"
? "invalid link"
: getHumanReadableFilesize(this.size);
},
humanTime: function () {
humanTime() {
if (this.readOnly == undefined && state.user.dateFormat) {
return fromNow(this.modified, state.user.locale).format("L LT");
}
return fromNow(this.modified, state.user.locale);
},
dragStart: function () {
dragStart() {
if (getters.selectedCount() === 0) {
mutations.addSelected(this.index);
return;
@@ -205,7 +191,7 @@ export default {
mutations.addSelected(this.index);
}
},
dragOver: function (event) {
dragOver(event) {
if (!this.canDrop) return;
event.preventDefault();
@@ -219,7 +205,7 @@ export default {
el.style.opacity = 1;
},
drop: async function (event) {
async drop(event) {
if (!this.canDrop) return;
event.preventDefault();
@@ -278,16 +264,17 @@ export default {
action(overwrite, rename);
},
itemClick: function (event) {
itemClick(event) {
console.log("should say something");
if (this.singleClick && !state.multiple) this.open();
else this.click(event);
},
click: function (event) {
click(event) {
if (!this.singleClick && getters.selectedCount() !== 0) event.preventDefault();
setTimeout(() => {
this.touches = 0;
}, 300);
}, 500);
this.touches++;
if (this.touches > 1) {
@@ -323,7 +310,7 @@ export default {
mutations.resetSelected();
mutations.addSelected(this.index);
},
open: function () {
open() {
this.$router.push({ path: this.url });
},
},
+2 -2
View File
@@ -52,7 +52,7 @@ import FileList from "./FileList.vue";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
import { showError } from "@/notify";
import { notify } from "@/notify";
export default {
name: "copy",
@@ -102,7 +102,7 @@ export default {
})
.catch((e) => {
buttons.done("copy");
showError(e);
notify.showError(e);
});
};
+4 -2
View File
@@ -33,7 +33,7 @@
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
import { state, getters, mutations } from "@/store";
import { showError } from "@/notify";
import { notify } from "@/notify";
export default {
name: "delete",
@@ -59,6 +59,7 @@ export default {
if (!this.isListing) {
await api.remove(state.route.path);
buttons.success("delete");
showSuccess("Deleted item successfully");
this.currentPrompt?.confirm();
this.closeHovers();
@@ -78,10 +79,11 @@ export default {
await Promise.all(promises);
buttons.success("delete");
showSuccess("Deleted item successfully");
mutations.setReload(true); // Handle reload as needed
} catch (e) {
buttons.done("delete");
showError(e);
notify.showError(e);
if (this.isListing) mutations.setReload(true); // Handle reload as needed
}
},
@@ -21,7 +21,7 @@
</template>
<script>
import { users as api } from "@/api";
import { showSuccess,showError } from "@/notify";
import { notify } from "@/notify";
import buttons from "@/utils/buttons";
import { state, mutations, getters } from "@/store";
@@ -40,12 +40,12 @@ export default {
event.preventDefault();
try {
await api.remove(this.user.id);
this.$router.push({ path: "/settings/users" });
showSuccess(this.$t("settings.userDeleted"));
this.$router.push({ path: "/settings",hash:"#users-main" });
notify.showSuccess(this.$t("settings.userDeleted"));
} catch (e) {
e.message === "403"
? showError(this.$t("errors.forbidden"), false)
: showError(e);
? notify.showError(this.$t("errors.forbidden"), false)
: notify.showError(e);
}
},
closeHovers() {
@@ -80,7 +80,7 @@ export default {
mutations.setReload(true); // Handle reload as needed
} catch (e) {
buttons.done("delete");
showError(e);
notify.showError(e);
if (this.isListing) mutations.setReload(true); // Handle reload as needed
}
},
Loaded 100 of 154 files, more files were not shown because too many files have changed in this diff. Show more