mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-08 10:25:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0270711a22 | ||
|
|
d2a3e50d37 | ||
|
|
266a76459d | ||
|
|
a79d44582f |
No files matched your search
+16
-16
@@ -7,22 +7,22 @@ on:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
jobs:
|
||||
test_frontend:
|
||||
name: Push release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.playwright
|
||||
push: false
|
||||
#test_frontend:
|
||||
# name: 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
|
||||
|
||||
@@ -10,7 +10,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
push_release_to_registry:
|
||||
name: Push dev release
|
||||
name: Push release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -35,16 +35,16 @@ jobs:
|
||||
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
|
||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
build-args: |
|
||||
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
||||
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.CLEANED_TAG }}
|
||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
+42
-1
@@ -2,6 +2,47 @@
|
||||
|
||||
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.3.4
|
||||
|
||||
**Bugfixes**:
|
||||
- Safari right-click actions.
|
||||
- Some small image viewer behavior
|
||||
- Progressive webapp "install to homescreen" fix.
|
||||
|
||||
## v0.3.3
|
||||
|
||||
**New Features**
|
||||
- Navigating remembers your previous scroll position when opening items and then navigating backwards.
|
||||
- New Icons with larger selection of file types
|
||||
- file "type" is shown on item info page.
|
||||
- added optional non-root "filebrowser" user for docker image. See https://github.com/gtsteffaniak/filebrowser/issues/251
|
||||
- File preview supports more file types:
|
||||
- images: jpg, bmp, gif, tiff, png, svg, heic, webp
|
||||
|
||||
**Notes**:
|
||||
- The file "type" is now either "directory" or a specific mimetype such as "text/xml".
|
||||
- update safari styling
|
||||
|
||||
**Bugfixes**:
|
||||
- Delete/move file/folders sometimes wouldn't work.
|
||||
- Possible fix for context menu not showing issue. See https://github.com/gtsteffaniak/filebrowser/issues/251
|
||||
- Fixed drag/drop not refreshing immediately to reflect changes.
|
||||
|
||||
## v0.3.2
|
||||
|
||||
**New Features**
|
||||
- Mobile search has the same features as desktop.
|
||||
|
||||
**Notes**:
|
||||
- Added compression. Helpful for browsing folders with a large number of items. Considering https://github.com/gtsteffaniak/filebrowser/issues/201 resolved, although future pagination support will still come.
|
||||
- Compressed download options limited to `.zip` and `.tar.gz`
|
||||
- right-click context menu stays in view.
|
||||
|
||||
**Bugfixes**:
|
||||
- search result links when non-default baseUrl configured
|
||||
- frontend sort bug squashed https://github.com/gtsteffaniak/filebrowser/issues/230
|
||||
- bug which caused "noauth" method not to work after v0.3.0 routes update
|
||||
|
||||
## v0.3.1
|
||||
|
||||
**New Features**
|
||||
@@ -171,7 +212,7 @@ This change focuses on minimizing and simplifying build process.
|
||||
- The shell feature has been deprecated.
|
||||
- Custom commands can be executed within the Docker container if needed.
|
||||
- The JSON config file is no longer used.
|
||||
- All configurations are now performed via the advanced `filebrowser.yaml`.
|
||||
- All configurations are now performed via the advanced `config.yaml`.
|
||||
- The only allowed flag is specifying the config file.
|
||||
- Removed old code for migrating database versions.
|
||||
- Eliminated all unused `cmd` code.
|
||||
|
||||
+6
-2
@@ -20,8 +20,12 @@ RUN npm run build-docker
|
||||
FROM alpine:latest
|
||||
ENV FILEBROWSER_NO_EMBEDED="true"
|
||||
RUN apk --no-cache add ca-certificates mailcap
|
||||
COPY --from=base /app/filebrowser* ./
|
||||
WORKDIR /home/filebrowser
|
||||
RUN adduser -D -s /bin/true -u 1000 filebrowser
|
||||
USER filebrowser
|
||||
COPY --from=base --chown=filebrowser:1000 /app/filebrowser* ./
|
||||
COPY --from=nbuild --chown=filebrowser:1000 /app/dist/ ./http/dist/
|
||||
USER root
|
||||
# exposing default port for auto discovery.
|
||||
EXPOSE 80
|
||||
COPY --from=nbuild /app/dist/ ./http/dist/
|
||||
ENTRYPOINT [ "./filebrowser" ]
|
||||
@@ -8,7 +8,7 @@ WORKDIR /app
|
||||
COPY ./frontend/package.json ./
|
||||
RUN npm i --maxsockets 1
|
||||
RUN npx playwright install --with-deps firefox
|
||||
COPY [ "backend/filebrowser.yaml", "./" ]
|
||||
COPY [ "backend/config.yaml", "./" ]
|
||||
COPY ./frontend/ ./frontend
|
||||
WORKDIR /app/frontend
|
||||
RUN npm run build-docker
|
||||
|
||||
@@ -9,42 +9,40 @@
|
||||
<img width="800" src="https://github.com/user-attachments/assets/e4a47229-66f8-4838-9575-dd2413596688" title="Main Screenshot">
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
> Starting with `v0.3.0` API routes have been slightly altered for friendly usage outside of the UI. The resources api returns items in separate `files` and `folder` objects now.
|
||||
> [!Note]
|
||||
> Starting with v0.3.3, configuration file mapping is different to support non-root user. Now, the default config file name is `config.yaml` and in docker the path is `/home/filebrowser/config.yaml` and `/home/filebrowser/<database_file>`. Please read the usage below to properly update your config to point the new config location. (open an issue for any help needed)
|
||||
|
||||
> [!WARNING]
|
||||
> If on windows, please use docker. The windows binary is unstable and may not work.
|
||||
|
||||
> [!WARNING]
|
||||
> There is no stable version yet. Always check release notes for bugfixes on functionality that may have been changed. If you notice any unexpected behavior -- please open an issue to have it fixed soon.
|
||||
> - There is no stable version yet. Always check release notes for bug fixes on functionality that may have been changed. If you notice any unexpected behavior -- please open an issue to have it fixed soon.
|
||||
|
||||
FileBrowser Quantum is a fork of the file browser opensource project with the following changes:
|
||||
|
||||
1. [x] Indexes files efficiently. See [indexing readme](./docs/indexing.md)
|
||||
- Real-time search results as you type
|
||||
1. [x] Indexes files efficiently. (See [indexing readme](./docs/indexing.md) for more info.)
|
||||
- Real-time search results as you type!
|
||||
- Search supports file/folder sizes and many file type filters.
|
||||
- Enhanced interactive results that shows file/folder sizes.
|
||||
1. [x] Revamped and simplified GUI navbar and sidebar menu.
|
||||
- Enhanced interactive results that show file/folder sizes.
|
||||
2. [x] Revamped and simplified GUI navbar and sidebar menu.
|
||||
- Additional compact view mode as well as refreshed view mode
|
||||
styles.
|
||||
- Many graphical and user experience improvements.
|
||||
- right-click context menu
|
||||
1. [x] Revamped and simplified configuration via `filebrowser.yml` config file.
|
||||
1. [x] Better listing browsing
|
||||
3. [x] Revamped and simplified configuration via `config.yaml` config file.
|
||||
4. [x] Better listing browsing
|
||||
- Switching view modes is instant
|
||||
- Folder sizes are shown as well
|
||||
- Changing Sort order is instant
|
||||
- The entire directory is loaded in 1/3 the time
|
||||
1. Developer API support
|
||||
- Can create long-live API Tokens.
|
||||
5. [x] Developer API support
|
||||
- Ability to create long-live API Tokens.
|
||||
- Helpful Swagger page available at `/swagger` endpoint.
|
||||
|
||||
Notable features that this fork *does not* have (removed):
|
||||
|
||||
- jobs/runners are not supported yet (planned).
|
||||
- shell commands are completely removed and will not be returning.
|
||||
- themes and branding are not fully supported yet (planned).
|
||||
- shell commands are completely removed and will not be returned.
|
||||
- Themes and branding are not fully supported yet (planned).
|
||||
- see feature matrix below for more.
|
||||
- pagination for directory items, so large directories with more than 100,000 items may be slow to load or not load at all.
|
||||
|
||||
## About
|
||||
|
||||
@@ -68,10 +66,11 @@ focus of this fork is on a few key principles:
|
||||
- Minimize external dependencies and standard library usage.
|
||||
- Of course -- adding much-needed features.
|
||||
|
||||
For more questions, see the [Q&A Readme](./docs/questions.md)
|
||||
|
||||
## Look
|
||||
|
||||
One way you can observe the improved user experience is how I changed
|
||||
the UI. The Navbar is simplified to a three-component system :
|
||||
The UI has a simple three-component navigation system :
|
||||
|
||||
1. (Left) The slide-out action panel button
|
||||
2. (Middle) The powerful search bar.
|
||||
@@ -95,7 +94,13 @@ Using docker:
|
||||
1. docker run (no persistent db):
|
||||
|
||||
```
|
||||
docker run -it -v /path/to/folder:/srv -p 80:80 gtstef/filebrowser
|
||||
docker run -it -v /path/to/folder:/srv -v $(pwd)/config.yaml:/home/filebrowser/config.yaml -p 80:80 gtstef/filebrowser
|
||||
```
|
||||
|
||||
or optionally, as non-root filebrowser user:
|
||||
|
||||
```
|
||||
docker run -u filebrowser -it -v $(pwd)/config.yaml:/home/filebrowser/config.yaml -v /path/to/folder:/srv -p 80:80 gtstef/filebrowser
|
||||
```
|
||||
|
||||
1. docker compose:
|
||||
@@ -107,11 +112,14 @@ services:
|
||||
filebrowser:
|
||||
volumes:
|
||||
- '/path/to/folder:/srv' # required (for now not configurable)
|
||||
- './database:/database' # optional if you want db to persist - configure a path under "database" dir in config file.
|
||||
- './filebrowser.yaml:/filebrowser.yaml' # required
|
||||
# optional if you want db to persist - configure a path under "database" dir in config file.
|
||||
- './database:/home/filebrowser/database'
|
||||
- './config.yaml:/home/filebrowser/config.yaml'
|
||||
ports:
|
||||
- '80:80'
|
||||
image: gtstef/filebrowser
|
||||
# optionally run as non-root filebrowser user
|
||||
#user: filebrowser
|
||||
restart: always
|
||||
```
|
||||
|
||||
@@ -122,8 +130,9 @@ services:
|
||||
filebrowser:
|
||||
volumes:
|
||||
- 'storage:/srv' # required (for now not configurable)
|
||||
- './database:/database' # optional if you want db to persist - configure a path under "database" dir in config file.
|
||||
- './filebrowser.yaml:/filebrowser.yaml' # required
|
||||
# optional if you want db to persist - configure a path under "database" dir in config file.
|
||||
- './database:/home/filebrowser/database'
|
||||
- './config.yaml:/home/filebrowser/config.yaml'
|
||||
ports:
|
||||
- '80:80'
|
||||
image: gtstef/filebrowser
|
||||
@@ -140,25 +149,26 @@ volumes:
|
||||
Not using docker (not recommended), download your binary from releases and run with your custom config file:
|
||||
|
||||
```
|
||||
./filebrowser -c <filebrowser.yml or other /path/to/config.yaml>
|
||||
./filebrowser -c <config.yaml or other /path/to/config.yaml>
|
||||
```
|
||||
|
||||
## Command Line Usage
|
||||
|
||||
There are very few commands available. There are 3 actions done via command line:
|
||||
There are very few commands available. There are 3 actions done via the command line:
|
||||
|
||||
1. Running the program, as shown on the install step. The only argument used is the config file, if you choose to override default "filebrowser.yaml"
|
||||
1. Running the program, as shown in the install step. The only argument used is the config file if you choose to override the default "config.yaml"
|
||||
2. Checking the version info via `./filebrowser version`
|
||||
3. Updating the DB, which currently only supports adding users via `./filebrowser set -u username,password [-a] [-s "example/scope"]`
|
||||
|
||||
## API Usage
|
||||
|
||||
FileBrowser Quantum comes with a swagger page that can be accessed from the API section of settings or by going to `/swagger` to see the full list:
|
||||
API tokens can be created to perform actions, access file information, and update user settings just like what can be done from the UI. You can create API tokens from the settings page via "API Management" section. This section will only show up if the user has "API" permissions, which can be granted by editing the user in user management.
|
||||
|
||||
Regardless of whether a user has API permissions, anyone can visit the swagger page which is found at `/swagger`. This swagger page uses a short-live token (2-hour exp) that the UI uses, but allows for quick access to all the API's and their described usage and requirements:
|
||||
|
||||

|
||||
|
||||
You use the token as a bearer token. For example in postman:
|
||||
|
||||
When using the API outside of swagger, you will need to set the API token as a bearer token authentication type. This means the authorization header will look like `Authorization: Bearer <token>`. For example in Postman:
|
||||
|
||||
Successful Request:
|
||||
|
||||
@@ -172,8 +182,8 @@ Failed Request
|
||||
## Configuration
|
||||
|
||||
All configuration is now done via a single configuration file:
|
||||
`filebrowser.yaml`, here is an example of minimal [configuration
|
||||
file](./backend/filebrowser.yaml).
|
||||
`config.yaml`, here is an example of minimal [configuration
|
||||
file](./backend/config.yaml).
|
||||
|
||||
View the [Configuration Help Page](./docs/configuration.md) for available
|
||||
configuration options and other help.
|
||||
@@ -181,7 +191,6 @@ configuration options and other help.
|
||||
|
||||
## Migration from the original filebrowser
|
||||
|
||||
If you currently use the original filebrowser but want to try using this.
|
||||
I would recommend that you start fresh without reusing the database. However,
|
||||
If you want to migrate your existing database to FileBrowser Quantum,
|
||||
visit the [migration
|
||||
@@ -202,12 +211,12 @@ 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 |
|
||||
Docker image size | 31 MB | 31 MB | 240 MB (main image) | 250 MB | ❌ | > 2 GB |
|
||||
Min. Memory Requirements | 128 MB | 128 MB | 128 MB (main image) | 128 MB | ❌ | 4 GB |
|
||||
has standalone binary | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
price | free | free | free | free tier | free tier | $99+ |
|
||||
rich media preview | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
upload files from the web? | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
upload files from the web? | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
Advanced Search? | ✅ | ❌ | ❌ | configurable | ✅ | ✅ |
|
||||
Indexed Search? | ✅ | ❌ | ❌ | configurable | ✅ | ✅ |
|
||||
Content-aware search? | ❌ | ❌ | ❌ | configurable | ✅ | ✅ |
|
||||
|
||||
@@ -24,15 +24,15 @@ builds:
|
||||
- upx {{ .Path }} # Compress the binary with UPX
|
||||
|
||||
# Build configuration for windows without arm
|
||||
- id: windows
|
||||
ldflags: *ldflags
|
||||
main: main.go
|
||||
binary: filebrowser
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
# - id: windows
|
||||
# ldflags: *ldflags
|
||||
# main: main.go
|
||||
# binary: filebrowser
|
||||
# goos:
|
||||
# - windows
|
||||
# goarch:
|
||||
# - amd64
|
||||
# - arm64
|
||||
|
||||
archives:
|
||||
- name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
|
||||
|
||||
@@ -3,7 +3,7 @@ package auth
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
// Auther is the authentication interface.
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
type hookCred struct {
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
type jsonCred struct {
|
||||
|
||||
@@ -3,8 +3,8 @@ package auth
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
// MethodNoAuth is used to identify no auth.
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
// MethodProxyAuth is used to identify no auth.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
// StorageBackend is a storage backend for auth storage.
|
||||
|
||||
+11
-11
@@ -7,17 +7,17 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"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/swagger/docs"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/diskcache"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
fbhttp "github.com/gtsteffaniak/filebrowser/backend/http"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/img"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/swagger/docs"
|
||||
"github.com/swaggo/swag"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/version"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/version"
|
||||
)
|
||||
|
||||
func getStore(config string) (*storage.Storage, bool) {
|
||||
@@ -47,7 +47,7 @@ func StartFilebrowser() {
|
||||
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.StringVar(&configPath, "c", "config.yaml", "Path to the config file, default: config.yaml")
|
||||
flag.BoolVar(&help, "h", false, "Get help about commands")
|
||||
|
||||
// Parse global flags (before subcommands)
|
||||
@@ -67,7 +67,7 @@ func StartFilebrowser() {
|
||||
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.")
|
||||
setCmd.StringVar(&dbConfig, "c", "config.yaml", "Path to the config file, default: config.yaml")
|
||||
|
||||
// Parse subcommand flags only if a subcommand is specified
|
||||
if len(os.Args) > 1 {
|
||||
|
||||
@@ -5,10 +5,10 @@ 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"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -5,10 +5,10 @@ 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"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
var usersCmd = &cobra.Command{
|
||||
|
||||
@@ -3,9 +3,9 @@ package cmd
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ package cmd
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -3,8 +3,8 @@ package cmd
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ package cmd
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func mustGetString(flags *pflag.FlagSet, flag string) string {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
server:
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
root: "./srv"
|
||||
root: "/srv"
|
||||
auth:
|
||||
method: password
|
||||
signup: false
|
||||
Binary file not shown.
@@ -24,11 +24,24 @@ var documentTypes = []string{
|
||||
".pdf", // Portable Document Format
|
||||
".odt", // OpenDocument Text
|
||||
".rtf", // Rich Text Format
|
||||
".conf",
|
||||
".bash_history",
|
||||
".gitignore",
|
||||
".htpasswd",
|
||||
".profile",
|
||||
".dockerignore",
|
||||
".editorconfig",
|
||||
|
||||
// Presentation Formats
|
||||
".ppt", ".pptx", // Microsoft PowerPoint
|
||||
".odp", // OpenDocument Presentation
|
||||
|
||||
// google docs
|
||||
".gdoc",
|
||||
|
||||
// google sheet
|
||||
".gsheet",
|
||||
|
||||
// Spreadsheet Formats
|
||||
".xls", ".xlsx", // Microsoft Excel
|
||||
".ods", // OpenDocument Spreadsheet
|
||||
@@ -102,6 +115,16 @@ type SearchOptions struct {
|
||||
Terms []string
|
||||
}
|
||||
|
||||
func extendedMimeTypeCheck(extension string) string {
|
||||
if isDoc(extension) {
|
||||
return "application/document"
|
||||
}
|
||||
if isText(extension) {
|
||||
return "text/plain"
|
||||
}
|
||||
return "blob"
|
||||
}
|
||||
|
||||
func ParseSearch(value string) SearchOptions {
|
||||
opts := SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
@@ -199,8 +222,6 @@ func IsMatchingType(extension string, matchType string) bool {
|
||||
switch matchType {
|
||||
case "doc":
|
||||
return isDoc(extension)
|
||||
case "pdf":
|
||||
return extension == ".pdf"
|
||||
case "text":
|
||||
return isText(extension)
|
||||
case "archive":
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestIsMatchingType(t *testing.T) {
|
||||
extension string
|
||||
expectedType string
|
||||
}{
|
||||
{".pdf", "pdf"},
|
||||
{".pdf", "doc"},
|
||||
{".doc", "doc"},
|
||||
{".docx", "doc"},
|
||||
{".json", "text"},
|
||||
|
||||
+25
-68
@@ -11,6 +11,7 @@ import (
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -20,11 +21,11 @@ import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/fileutils"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/fileutils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -201,7 +202,8 @@ func DeleteFiles(absPath string, opts FileOptions) error {
|
||||
return err
|
||||
}
|
||||
index := GetIndex(rootPath)
|
||||
err = index.RefreshFileInfo(opts)
|
||||
refreshConfig := FileOptions{Path: filepath.Dir(opts.Path), IsDir: true}
|
||||
err = index.RefreshFileInfo(refreshConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -341,74 +343,29 @@ func getContent(path string) (string, error) {
|
||||
return stringContent, nil
|
||||
}
|
||||
|
||||
// detectType detects the file type.
|
||||
func (i *ItemInfo) detectType(path string, modify, saveContent, readHeader bool) error {
|
||||
// DetectType detects the MIME type of a file and updates the ItemInfo struct.
|
||||
func (i *ItemInfo) DetectType(path string, saveContent bool) {
|
||||
name := i.Name
|
||||
var contentErr error
|
||||
|
||||
ext := filepath.Ext(name)
|
||||
var buffer []byte
|
||||
if readHeader {
|
||||
buffer = i.readFirstBytes(path)
|
||||
mimetype := mime.TypeByExtension(ext)
|
||||
if mimetype == "" {
|
||||
http.DetectContentType(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
for _, fileType := range AllFiletypeOptions {
|
||||
if IsMatchingType(ext, fileType) {
|
||||
i.Type = fileType
|
||||
}
|
||||
switch i.Type {
|
||||
case "text":
|
||||
if !modify {
|
||||
i.Type = "textImmutable"
|
||||
}
|
||||
if saveContent {
|
||||
return contentErr
|
||||
}
|
||||
case "video":
|
||||
// TODO add back somewhere else, not during metadata fetch
|
||||
//parentDir := strings.TrimRight(path, name)
|
||||
//i.detectSubtitles(parentDir)
|
||||
case "doc":
|
||||
if ext == ".pdf" {
|
||||
i.Type = "pdf"
|
||||
return nil
|
||||
}
|
||||
if saveContent {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
// Attempt MIME detection by file extension
|
||||
i.Type = strings.Split(mime.TypeByExtension(ext), ";")[0]
|
||||
if i.Type == "" {
|
||||
i.Type = "blob"
|
||||
if saveContent {
|
||||
return contentErr
|
||||
i.Type = extendedMimeTypeCheck(ext)
|
||||
}
|
||||
if i.Type == "blob" {
|
||||
realpath, _, _ := GetRealPath(path)
|
||||
// Read only the first 512 bytes for efficient MIME detection
|
||||
file, err := os.Open(realpath)
|
||||
if err != nil {
|
||||
|
||||
} else {
|
||||
defer file.Close()
|
||||
buffer := make([]byte, 512)
|
||||
n, _ := file.Read(buffer) // Ignore errors from Read
|
||||
i.Type = strings.Split(http.DetectContentType(buffer[:n]), ";")[0]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readFirstBytes reads the first bytes of the file.
|
||||
func (i *ItemInfo) readFirstBytes(path string) []byte {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
i.Type = "blob"
|
||||
return nil
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
buffer := make([]byte, 512) //nolint:gomnd
|
||||
n, err := file.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
i.Type = "blob"
|
||||
return nil
|
||||
}
|
||||
|
||||
return buffer[:n]
|
||||
}
|
||||
|
||||
// TODO add subtitles back
|
||||
|
||||
@@ -3,6 +3,7 @@ package files
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -71,3 +72,98 @@ func Test_GetRealPath(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortItems(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input FileInfo
|
||||
expected FileInfo
|
||||
}{
|
||||
{
|
||||
name: "Numeric and Lexicographical Sorting",
|
||||
input: FileInfo{
|
||||
Folders: []ItemInfo{
|
||||
{Name: "10"},
|
||||
{Name: "2"},
|
||||
{Name: "apple"},
|
||||
{Name: "Banana"},
|
||||
},
|
||||
Files: []ItemInfo{
|
||||
{Name: "File2"},
|
||||
{Name: "File10"},
|
||||
{Name: "File1"},
|
||||
{Name: "banana"},
|
||||
},
|
||||
},
|
||||
expected: FileInfo{
|
||||
Folders: []ItemInfo{
|
||||
{Name: "2"},
|
||||
{Name: "10"},
|
||||
{Name: "apple"},
|
||||
{Name: "Banana"},
|
||||
},
|
||||
Files: []ItemInfo{
|
||||
{Name: "banana"},
|
||||
{Name: "File1"},
|
||||
{Name: "File10"},
|
||||
{Name: "File2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Only Lexicographical Sorting",
|
||||
input: FileInfo{
|
||||
Folders: []ItemInfo{
|
||||
{Name: "dog"},
|
||||
{Name: "Cat"},
|
||||
{Name: "apple"},
|
||||
},
|
||||
Files: []ItemInfo{
|
||||
{Name: "Zebra"},
|
||||
{Name: "apple"},
|
||||
{Name: "cat"},
|
||||
},
|
||||
},
|
||||
expected: FileInfo{
|
||||
Folders: []ItemInfo{
|
||||
{Name: "apple"},
|
||||
{Name: "Cat"},
|
||||
{Name: "dog"},
|
||||
},
|
||||
Files: []ItemInfo{
|
||||
{Name: "apple"},
|
||||
{Name: "cat"},
|
||||
{Name: "Zebra"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
test.input.SortItems()
|
||||
|
||||
getNames := func(items []ItemInfo) []string {
|
||||
names := []string{}
|
||||
for _, folder := range items {
|
||||
names = append(names, folder.Name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
actualFolderNames := getNames(test.input.Folders)
|
||||
expectedFolderNames := getNames(test.expected.Folders)
|
||||
|
||||
if !reflect.DeepEqual(actualFolderNames, expectedFolderNames) {
|
||||
t.Errorf("Folders not sorted correctly.\nGot: %v\nExpected: %v", actualFolderNames, expectedFolderNames)
|
||||
}
|
||||
|
||||
actualFileNames := getNames(test.input.Files)
|
||||
expectedFileNames := getNames(test.expected.Files)
|
||||
|
||||
if !reflect.DeepEqual(actualFileNames, expectedFileNames) {
|
||||
t.Errorf("Files not sorted correctly.\nGot: %v\nExpected: %v", actualFileNames, expectedFileNames)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
type Index struct {
|
||||
@@ -132,7 +132,7 @@ func (si *Index) indexDirectory(adjustedPath string, quick, recursive bool) erro
|
||||
dirInfos = append(dirInfos, *itemInfo)
|
||||
si.NumDirs++
|
||||
} else {
|
||||
_ = itemInfo.detectType(combinedPath+file.Name(), true, false, false)
|
||||
itemInfo.DetectType(combinedPath+file.Name(), false)
|
||||
itemInfo.Size = file.Size()
|
||||
fileInfos = append(fileInfos, *itemInfo)
|
||||
totalSize += itemInfo.Size
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
// schedule in minutes
|
||||
@@ -97,15 +97,12 @@ func (si *Index) RunIndexing(origin string, quick bool) {
|
||||
if si.indexingTime < 3 || si.NumDirs < 10000 {
|
||||
si.assessment = "simple"
|
||||
si.SmartModifier = 4 * time.Minute
|
||||
log.Println("Index is small and efficient, adjusting scan interval accordingly.")
|
||||
} else if si.indexingTime > 120 || si.NumDirs > 500000 {
|
||||
si.assessment = "complex"
|
||||
modifier := si.indexingTime / 10 // seconds
|
||||
si.SmartModifier = time.Duration(modifier) * time.Minute
|
||||
log.Println("Index is large and complex, adjusting scan interval accordingly.")
|
||||
} else {
|
||||
si.assessment = "normal"
|
||||
log.Println("Index is normal, quick scan set to every 5 minutes.")
|
||||
}
|
||||
log.Printf("Index assessment : complexity=%v directories=%v files=%v \n", si.assessment, si.NumDirs, si.NumFiles)
|
||||
if si.NumDirs != prevNumDirs || si.NumFiles != prevNumFiles {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
func BenchmarkFillIndex(b *testing.B) {
|
||||
@@ -44,45 +44,6 @@ func (si *Index) createMockData(numDirs, numFilesPerDir int) {
|
||||
}
|
||||
}
|
||||
|
||||
func generateRandomPath(levels int) string {
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
dirName := "srv"
|
||||
for i := 0; i < levels; i++ {
|
||||
dirName += "/" + getRandomTerm()
|
||||
}
|
||||
return dirName
|
||||
}
|
||||
|
||||
func getRandomTerm() string {
|
||||
wordbank := []string{
|
||||
"hi", "test", "other", "name",
|
||||
"cool", "things", "more", "items",
|
||||
}
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
index := rand.Intn(len(wordbank))
|
||||
return wordbank[index]
|
||||
}
|
||||
|
||||
func getRandomExtension() string {
|
||||
wordbank := []string{
|
||||
".txt", ".mp3", ".mov", ".doc",
|
||||
".mp4", ".bak", ".zip", ".jpg",
|
||||
}
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
index := rand.Intn(len(wordbank))
|
||||
return wordbank[index]
|
||||
}
|
||||
|
||||
func generateRandomSearchTerms(numTerms int) []string {
|
||||
// Generate random search terms
|
||||
searchTerms := make([]string, numTerms)
|
||||
for i := 0; i < numTerms; i++ {
|
||||
searchTerms[i] = getRandomTerm()
|
||||
}
|
||||
return searchTerms
|
||||
}
|
||||
|
||||
// JSONBytesEqual compares the JSON in two byte slices.
|
||||
func JSONBytesEqual(a, b []byte) (bool, error) {
|
||||
var j, j2 interface{}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
func CreateMockData(numDirs, numFilesPerDir int) FileInfo {
|
||||
dir := FileInfo{}
|
||||
dir.Path = "/here/is/your/mock/dir"
|
||||
for i := 0; i < numDirs; i++ {
|
||||
newFile := ItemInfo{
|
||||
Name: "file-" + getRandomTerm() + getRandomExtension(),
|
||||
Size: rand.Int63n(1000), // Random size
|
||||
ModTime: time.Now().Add(-time.Duration(rand.Intn(100)) * time.Hour), // Random mod time
|
||||
Type: "blob",
|
||||
}
|
||||
dir.Folders = append(dir.Folders, newFile)
|
||||
}
|
||||
// Simulating files and directories with FileInfo
|
||||
for j := 0; j < numFilesPerDir; j++ {
|
||||
newFile := ItemInfo{
|
||||
Name: "file-" + getRandomTerm() + getRandomExtension(),
|
||||
Size: rand.Int63n(1000), // Random size
|
||||
ModTime: time.Now().Add(-time.Duration(rand.Intn(100)) * time.Hour), // Random mod time
|
||||
Type: "blob",
|
||||
}
|
||||
dir.Files = append(dir.Files, newFile)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func generateRandomPath(levels int) string {
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
dirName := "srv"
|
||||
for i := 0; i < levels; i++ {
|
||||
dirName += "/" + getRandomTerm()
|
||||
}
|
||||
return dirName
|
||||
}
|
||||
|
||||
func getRandomTerm() string {
|
||||
wordbank := []string{
|
||||
"hi", "test", "other", "name",
|
||||
"cool", "things", "more", "items",
|
||||
}
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
index := rand.Intn(len(wordbank))
|
||||
return wordbank[index]
|
||||
}
|
||||
|
||||
func getRandomExtension() string {
|
||||
wordbank := []string{
|
||||
".txt", ".mp3", ".mov", ".doc",
|
||||
".mp4", ".bak", ".zip", ".jpg",
|
||||
}
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
index := rand.Intn(len(wordbank))
|
||||
return wordbank[index]
|
||||
}
|
||||
|
||||
func generateRandomSearchTerms(numTerms int) []string {
|
||||
// Generate random search terms
|
||||
searchTerms := make([]string, numTerms)
|
||||
for i := 0; i < numTerms; i++ {
|
||||
searchTerms[i] = getRandomTerm()
|
||||
}
|
||||
return searchTerms
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -14,19 +14,19 @@ var (
|
||||
maxSearchResults = 100
|
||||
)
|
||||
|
||||
type searchResult struct {
|
||||
type SearchResult struct {
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
func (si *Index) Search(search string, scope string, sourceSession string) []searchResult {
|
||||
func (si *Index) Search(search string, scope string, sourceSession string) []SearchResult {
|
||||
// Remove slashes
|
||||
scope = si.makeIndexPath(scope)
|
||||
runningHash := utils.GenerateRandomHash(4)
|
||||
sessionInProgress.Store(sourceSession, runningHash) // Store the value in the sync.Map
|
||||
searchOptions := ParseSearch(search)
|
||||
results := make(map[string]searchResult, 0)
|
||||
results := make(map[string]SearchResult, 0)
|
||||
count := 0
|
||||
var directories []string
|
||||
cachedDirs, ok := utils.SearchResultsCache.Get(si.Root + scope).([]string)
|
||||
@@ -62,7 +62,7 @@ func (si *Index) Search(search string, scope string, sourceSession string) []sea
|
||||
}
|
||||
matches := reducedDir.containsSearchTerm(searchTerm, searchOptions)
|
||||
if matches {
|
||||
results[scopedPath] = searchResult{Path: scopedPath, Type: "directory", Size: dir.Size}
|
||||
results[scopedPath] = SearchResult{Path: scopedPath, Type: "directory", Size: dir.Size}
|
||||
count++
|
||||
}
|
||||
// search files first
|
||||
@@ -75,14 +75,14 @@ func (si *Index) Search(search string, scope string, sourceSession string) []sea
|
||||
value, found := sessionInProgress.Load(sourceSession)
|
||||
if !found || value != runningHash {
|
||||
si.mu.Unlock()
|
||||
return []searchResult{}
|
||||
return []SearchResult{}
|
||||
}
|
||||
if count > maxSearchResults {
|
||||
break
|
||||
}
|
||||
matches := item.containsSearchTerm(searchTerm, searchOptions)
|
||||
if matches {
|
||||
results[scopedPath] = searchResult{Path: scopedPath, Type: item.Type, Size: item.Size}
|
||||
results[scopedPath] = SearchResult{Path: scopedPath, Type: item.Type, Size: item.Size}
|
||||
count++
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func (si *Index) Search(search string, scope string, sourceSession string) []sea
|
||||
}
|
||||
|
||||
// Sort keys based on the number of elements in the path after splitting by "/"
|
||||
sortedKeys := make([]searchResult, 0, len(results))
|
||||
sortedKeys := make([]SearchResult, 0, len(results))
|
||||
for _, v := range results {
|
||||
sortedKeys = append(sortedKeys, v)
|
||||
}
|
||||
|
||||
@@ -119,12 +119,12 @@ func TestSearchIndexes(t *testing.T) {
|
||||
tests := []struct {
|
||||
search string
|
||||
scope string
|
||||
expectedResult []searchResult
|
||||
expectedResult []SearchResult
|
||||
}{
|
||||
{
|
||||
search: "audio",
|
||||
scope: "/new/",
|
||||
expectedResult: []searchResult{
|
||||
expectedResult: []SearchResult{
|
||||
{
|
||||
Path: "test/audio.wav",
|
||||
Type: "audio",
|
||||
@@ -135,7 +135,7 @@ func TestSearchIndexes(t *testing.T) {
|
||||
{
|
||||
search: "test",
|
||||
scope: "/",
|
||||
expectedResult: []searchResult{
|
||||
expectedResult: []SearchResult{
|
||||
{
|
||||
Path: "test/",
|
||||
Type: "directory",
|
||||
@@ -151,7 +151,7 @@ func TestSearchIndexes(t *testing.T) {
|
||||
{
|
||||
search: "archive",
|
||||
scope: "/",
|
||||
expectedResult: []searchResult{
|
||||
expectedResult: []SearchResult{
|
||||
{
|
||||
Path: "firstDir/archive.zip",
|
||||
Type: "archive",
|
||||
@@ -167,7 +167,7 @@ func TestSearchIndexes(t *testing.T) {
|
||||
{
|
||||
search: "arch",
|
||||
scope: "/firstDir",
|
||||
expectedResult: []searchResult{
|
||||
expectedResult: []SearchResult{
|
||||
{
|
||||
Path: "archive.zip",
|
||||
Type: "archive",
|
||||
@@ -178,7 +178,7 @@ func TestSearchIndexes(t *testing.T) {
|
||||
{
|
||||
search: "isdir",
|
||||
scope: "/",
|
||||
expectedResult: []searchResult{
|
||||
expectedResult: []SearchResult{
|
||||
{
|
||||
Path: "firstDir/thisIsDir/",
|
||||
Type: "directory",
|
||||
@@ -189,7 +189,7 @@ func TestSearchIndexes(t *testing.T) {
|
||||
{
|
||||
search: "IsDir type:largerThan=1",
|
||||
scope: "/",
|
||||
expectedResult: []searchResult{
|
||||
expectedResult: []SearchResult{
|
||||
{
|
||||
Path: "firstDir/thisIsDir/",
|
||||
Type: "directory",
|
||||
@@ -200,7 +200,7 @@ func TestSearchIndexes(t *testing.T) {
|
||||
{
|
||||
search: "video",
|
||||
scope: "/",
|
||||
expectedResult: []searchResult{
|
||||
expectedResult: []SearchResult{
|
||||
{
|
||||
Path: "new/test/video.MP4",
|
||||
Type: "video",
|
||||
|
||||
@@ -3,7 +3,7 @@ package files
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
// UpdateFileMetadata updates the FileInfo for the specified directory in the index.
|
||||
|
||||
+9
-18
@@ -1,4 +1,4 @@
|
||||
module github.com/gtsteffaniak/filebrowser
|
||||
module github.com/gtsteffaniak/filebrowser/backend
|
||||
|
||||
go 1.22.5
|
||||
|
||||
@@ -7,11 +7,10 @@ require (
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dsoprea/go-exif/v3 v3.0.1
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568
|
||||
github.com/goccy/go-yaml v1.14.3
|
||||
github.com/gabriel-vasile/mimetype v1.4.7
|
||||
github.com/goccy/go-yaml v1.15.7
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/marusama/semaphore/v2 v2.5.0
|
||||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
github.com/spf13/afero v1.11.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
@@ -19,16 +18,14 @@ require (
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/swaggo/http-swagger v1.3.4
|
||||
github.com/swaggo/swag v1.16.4
|
||||
golang.org/x/crypto v0.29.0
|
||||
golang.org/x/image v0.22.0
|
||||
golang.org/x/text v0.20.0
|
||||
golang.org/x/crypto v0.30.0
|
||||
golang.org/x/image v0.23.0
|
||||
golang.org/x/text v0.21.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // 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/go-errors/errors v1.5.1 // indirect
|
||||
@@ -41,21 +38,15 @@ require (
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // 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-20240221224432-82ca36839d55 // indirect
|
||||
github.com/swaggo/files v1.0.1 // 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.11 // indirect
|
||||
golang.org/x/net v0.31.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/tools v0.27.0 // indirect
|
||||
golang.org/x/net v0.32.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/tools v0.28.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
+18
-51
@@ -4,9 +4,6 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
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/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
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=
|
||||
@@ -14,9 +11,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
|
||||
github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=
|
||||
github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
|
||||
@@ -36,6 +30,8 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
@@ -53,8 +49,8 @@ github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9Z
|
||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/goccy/go-yaml v1.14.3 h1:8tVD+aqqPLWisSEhM+6wWoiURWXCx6BwaTKS6ZeITgM=
|
||||
github.com/goccy/go-yaml v1.14.3/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.15.7 h1:L7XuKpd/A66X4w/dlk08lVfiIADdy79a1AzRoIefC98=
|
||||
github.com/goccy/go-yaml v1.15.7/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
@@ -67,10 +63,8 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
||||
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/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=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
@@ -79,14 +73,6 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -96,16 +82,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/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/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/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
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-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
@@ -130,16 +106,8 @@ github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64
|
||||
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
|
||||
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
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/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
@@ -148,11 +116,11 @@ go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
|
||||
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
|
||||
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g=
|
||||
golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4=
|
||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
@@ -167,12 +135,12 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
|
||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -187,8 +155,8 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -197,15 +165,14 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
|
||||
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
|
||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
func createApiKeyHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
|
||||
@@ -16,11 +16,11 @@ import (
|
||||
"github.com/golang-jwt/jwt/v4/request"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/share"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
+49
-15
@@ -1,17 +1,20 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"github.com/gtsteffaniak/filebrowser/runner"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/runner"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
type requestContext struct {
|
||||
@@ -91,7 +94,15 @@ func withAdminHelper(fn handleFunc) handleFunc {
|
||||
// Middleware to retrieve and authenticate user
|
||||
func withUserHelper(fn handleFunc) handleFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
|
||||
|
||||
if settings.Config.Auth.Method == "noauth" {
|
||||
var err error
|
||||
// Retrieve the user from the store and store it in the context
|
||||
data.user, err = store.Users.Get(config.Server.Root, "admin")
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
return fn(w, r, data)
|
||||
}
|
||||
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
||||
return config.Auth.Key, nil
|
||||
}
|
||||
@@ -221,6 +232,7 @@ type ResponseWriterWrapper struct {
|
||||
http.ResponseWriter
|
||||
StatusCode int
|
||||
wroteHeader bool
|
||||
PayloadSize int
|
||||
}
|
||||
|
||||
// WriteHeader captures the status code and ensures it's only written once
|
||||
@@ -243,36 +255,37 @@ func (w *ResponseWriterWrapper) Write(b []byte) (int, error) {
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
// LoggingMiddleware logs each request and its status code
|
||||
// LoggingMiddleware logs each request and its status code.
|
||||
func LoggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
start := time.Now()
|
||||
|
||||
// Wrap the ResponseWriter to capture the status code
|
||||
// Wrap the ResponseWriter to capture the status code.
|
||||
wrappedWriter := &ResponseWriterWrapper{ResponseWriter: w, StatusCode: http.StatusOK}
|
||||
|
||||
// Call the next handler
|
||||
// Call the next handler.
|
||||
next.ServeHTTP(wrappedWriter, r)
|
||||
|
||||
// Determine the color based on the status code
|
||||
// Determine the color based on the status code.
|
||||
color := "\033[32m" // Default green color
|
||||
if wrappedWriter.StatusCode >= 300 && wrappedWriter.StatusCode < 500 {
|
||||
color = "\033[33m" // Yellow for client errors (4xx)
|
||||
} else if wrappedWriter.StatusCode >= 500 {
|
||||
color = "\033[31m" // Red for server errors (5xx)
|
||||
}
|
||||
// Capture the full URL path including the query parameters
|
||||
|
||||
// Capture the full URL path including the query parameters.
|
||||
fullURL := r.URL.Path
|
||||
if r.URL.RawQuery != "" {
|
||||
fullURL += "?" + r.URL.RawQuery
|
||||
}
|
||||
|
||||
// Log the request and its status code
|
||||
// Log the request, status code, and response size.
|
||||
log.Printf("%s%-7s | %3d | %-15s | %-12s | \"%s\"%s",
|
||||
color,
|
||||
r.Method,
|
||||
wrappedWriter.StatusCode, // Now capturing the correct status
|
||||
wrappedWriter.StatusCode, // Captured status code
|
||||
r.RemoteAddr,
|
||||
time.Since(start).String(),
|
||||
fullURL,
|
||||
@@ -281,15 +294,36 @@ func LoggingMiddleware(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func renderJSON(w http.ResponseWriter, _ *http.Request, data interface{}) (int, error) {
|
||||
func renderJSON(w http.ResponseWriter, r *http.Request, data interface{}) (int, error) {
|
||||
marsh, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
if _, err := w.Write(marsh); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
// Calculate size in KB
|
||||
payloadSizeKB := len(marsh) / 1024
|
||||
// Check if the client accepts gzip encoding and hasn't explicitly disabled it
|
||||
if acceptsGzip(r) && payloadSizeKB > 10 {
|
||||
// Enable gzip compression
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
gz := gzip.NewWriter(w)
|
||||
defer gz.Close()
|
||||
|
||||
if _, err := gz.Write(marsh); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
} else {
|
||||
// Normal response without compression
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
if _, err := w.Write(marsh); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func acceptsGzip(r *http.Request) bool {
|
||||
ae := r.Header.Get("Accept-Encoding")
|
||||
return ae != "" && strings.Contains(ae, "gzip")
|
||||
}
|
||||
@@ -8,15 +8,15 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/gtsteffaniak/filebrowser/diskcache"
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"github.com/gtsteffaniak/filebrowser/img"
|
||||
"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"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/diskcache"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/img"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage/bolt"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func setupTestEnv(t *testing.T) {
|
||||
|
||||
+20
-4
@@ -8,9 +8,10 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"github.com/gtsteffaniak/filebrowser/img"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/img"
|
||||
)
|
||||
|
||||
type ImgService interface {
|
||||
@@ -63,8 +64,8 @@ func previewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
|
||||
if fileInfo.Type == "directory" {
|
||||
return http.StatusBadRequest, fmt.Errorf("can't create preview for directory")
|
||||
}
|
||||
setContentDisposition(w, r, fileInfo)
|
||||
if fileInfo.Type != "image" {
|
||||
setContentDisposition(w, r, fileInfo.Name)
|
||||
if !strings.HasPrefix(fileInfo.Type, "image") {
|
||||
return http.StatusNotImplemented, fmt.Errorf("can't create preview for %s type", fileInfo.Type)
|
||||
}
|
||||
|
||||
@@ -150,3 +151,18 @@ func createPreview(imgSvc ImgService, fileCache FileCache, file *files.FileInfo,
|
||||
func previewCacheKey(f *files.FileInfo, previewSize string) string {
|
||||
return fmt.Sprintf("%x%x%x", f.RealPath(), f.ModTime.Unix(), previewSize)
|
||||
}
|
||||
|
||||
func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo) (int, error) {
|
||||
realPath, _, _ := files.GetRealPath(file.Path)
|
||||
fd, err := os.Open(realPath)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
setContentDisposition(w, r, file.Name)
|
||||
|
||||
w.Header().Set("Cache-Control", "private")
|
||||
http.ServeContent(w, r, file.Name, file.ModTime, fd)
|
||||
return 0, nil
|
||||
}
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
|
||||
_ "github.com/gtsteffaniak/filebrowser/swagger/docs"
|
||||
_ "github.com/gtsteffaniak/filebrowser/backend/swagger/docs"
|
||||
)
|
||||
|
||||
func publicShareHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
@@ -42,7 +42,7 @@ func publicDlHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
}
|
||||
|
||||
if file.Type == "directory" {
|
||||
return rawDirHandler(w, r, d, file.FileInfo)
|
||||
return rawFilesHandler(w, r, d, []string{file.Path})
|
||||
}
|
||||
|
||||
return rawFileHandler(w, r, file.FileInfo)
|
||||
|
||||
+158
-158
@@ -1,79 +1,28 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
gopath "path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"github.com/gtsteffaniak/filebrowser/fileutils"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
)
|
||||
|
||||
func slashClean(name string) string {
|
||||
if name == "" || name[0] != '/' {
|
||||
name = "/" + name
|
||||
}
|
||||
return gopath.Clean(name)
|
||||
}
|
||||
|
||||
func parseQueryFiles(r *http.Request, f *files.FileInfo, _ *users.User) ([]string, error) {
|
||||
var fileSlice []string
|
||||
names := strings.Split(r.URL.Query().Get("files"), ",")
|
||||
|
||||
if len(names) == 0 {
|
||||
fileSlice = append(fileSlice, f.Path)
|
||||
} else {
|
||||
for _, name := range names {
|
||||
name, err := url.QueryUnescape(strings.Replace(name, "+", "%2B", -1)) //nolint:govet
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name = slashClean(name)
|
||||
fileSlice = append(fileSlice, filepath.Join(f.Path, name))
|
||||
}
|
||||
}
|
||||
|
||||
return fileSlice, nil
|
||||
}
|
||||
|
||||
// nolint: goconst,nolintlint
|
||||
func parseQueryAlgorithm(r *http.Request) (string, archiver.Writer, error) {
|
||||
// TODO: use enum
|
||||
switch r.URL.Query().Get("algo") {
|
||||
case "zip", "true", "":
|
||||
return ".zip", archiver.NewZip(), nil
|
||||
case "tar":
|
||||
return ".tar", archiver.NewTar(), nil
|
||||
case "targz":
|
||||
return ".tar.gz", archiver.NewTarGz(), nil
|
||||
case "tarbz2":
|
||||
return ".tar.bz2", archiver.NewTarBz2(), nil
|
||||
case "tarxz":
|
||||
return ".tar.xz", archiver.NewTarXz(), nil
|
||||
case "tarlz4":
|
||||
return ".tar.lz4", archiver.NewTarLz4(), nil
|
||||
case "tarsz":
|
||||
return ".tar.sz", archiver.NewTarSz(), nil
|
||||
default:
|
||||
return "", nil, errors.New("format not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
func setContentDisposition(w http.ResponseWriter, r *http.Request, file *files.FileInfo) {
|
||||
func setContentDisposition(w http.ResponseWriter, r *http.Request, fileName string) {
|
||||
if r.URL.Query().Get("inline") == "true" {
|
||||
w.Header().Set("Content-Disposition", "inline")
|
||||
} else {
|
||||
// As per RFC6266 section 4.3
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name))
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(fileName))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,150 +32,201 @@ func setContentDisposition(w http.ResponseWriter, r *http.Request, file *files.F
|
||||
// @Tags Resources
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param path query string true "Path to the file or directory"
|
||||
// @Param files query string false "Comma-separated list of specific files within the directory (optional)"
|
||||
// @Param files query string true "Comma-separated list of specific files within the directory (required)"
|
||||
// @Param inline query bool false "If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults to 'attachment'."
|
||||
// @Param algo query string false "Compression algorithm for archiving multiple files or directories. Options: 'zip', 'tar', 'targz', 'tarbz2', 'tarxz', 'tarlz4', 'tarsz'. Default is 'zip'."
|
||||
// @Param algo query string false "Compression algorithm for archiving multiple files or directories. Options: 'zip' and 'tar.gz'. Default is 'zip'."
|
||||
// @Success 200 {file} file "Raw file or directory content, or archive for multiple files"
|
||||
// @Failure 202 {object} map[string]string "Download permissions required"
|
||||
// @Failure 400 {object} map[string]string "Invalid request path"
|
||||
// @Failure 404 {object} map[string]string "File or directory not found"
|
||||
// @Failure 415 {object} map[string]string "Unsupported file type for preview"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/raw [get]
|
||||
func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
if !d.user.Perm.Download {
|
||||
return http.StatusAccepted, nil
|
||||
}
|
||||
path := r.URL.Query().Get("path")
|
||||
fileInfo, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: filepath.Join(d.user.Scope, path),
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: config.Server.TypeDetectionByHeader,
|
||||
Checker: d.user,
|
||||
})
|
||||
encodedFiles := r.URL.Query().Get("files")
|
||||
// Decode the URL-encoded path
|
||||
files, err := url.QueryUnescape(encodedFiles)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
|
||||
// TODO, how to handle? we removed mode, is it needed?
|
||||
// maybe instead of mode we use bool only two conditions are checked
|
||||
//if files.IsNamedPipe(fileInfo.Mode) {
|
||||
// setContentDisposition(w, r, file)
|
||||
// return 0, nil
|
||||
//}
|
||||
|
||||
if fileInfo.Type == "directory" {
|
||||
return rawDirHandler(w, r, d, fileInfo.FileInfo)
|
||||
}
|
||||
|
||||
return rawFileHandler(w, r, fileInfo.FileInfo)
|
||||
return rawFilesHandler(w, r, d, strings.Split(files, ","))
|
||||
}
|
||||
|
||||
func addFile(ar archiver.Writer, d *requestContext, path, commonPath string) error {
|
||||
if !d.user.Check(path) {
|
||||
func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *zip.Writer) error {
|
||||
realPath, _, _ := files.GetRealPath(d.user.Scope, path)
|
||||
if !d.user.Check(realPath) {
|
||||
return nil
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
info, err := os.Stat(realPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() && !info.Mode().IsRegular() {
|
||||
return nil
|
||||
if info.IsDir() {
|
||||
// Walk through directory contents
|
||||
return filepath.Walk(realPath, func(filePath string, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relPath, err := filepath.Rel(filepath.Dir(realPath), filePath) // Relative path including base dir
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fileInfo.IsDir() {
|
||||
if tarWriter != nil {
|
||||
header := &tar.Header{
|
||||
Name: relPath + "/",
|
||||
Mode: 0755,
|
||||
Typeflag: tar.TypeDir,
|
||||
ModTime: fileInfo.ModTime(),
|
||||
}
|
||||
return tarWriter.WriteHeader(header)
|
||||
}
|
||||
if zipWriter != nil {
|
||||
_, err := zipWriter.Create(relPath + "/")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return addSingleFile(filePath, relPath, zipWriter, tarWriter)
|
||||
})
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
// Add a single file if it's not a directory
|
||||
return addSingleFile(realPath, filepath.Base(realPath), zipWriter, tarWriter)
|
||||
}
|
||||
|
||||
func addSingleFile(realPath, archivePath string, zipWriter *zip.Writer, tarWriter *tar.Writer) error {
|
||||
file, err := os.Open(realPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if path != commonPath {
|
||||
filename := strings.TrimPrefix(path, commonPath)
|
||||
filename = strings.TrimPrefix(filename, string(filepath.Separator))
|
||||
err = ar.Write(archiver.File{
|
||||
FileInfo: archiver.FileInfo{
|
||||
FileInfo: info,
|
||||
CustomName: filename,
|
||||
},
|
||||
ReadCloser: file,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := os.Stat(realPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
names, err := file.Readdirnames(0)
|
||||
if tarWriter != nil {
|
||||
header, err := tar.FileInfoHeader(info, realPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = archivePath
|
||||
if err = tarWriter.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(tarWriter, file)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
fPath := filepath.Join(path, name)
|
||||
err = addFile(ar, d, fPath, commonPath)
|
||||
if err != nil {
|
||||
log.Printf("Failed to archive %s: %v", fPath, err)
|
||||
}
|
||||
if zipWriter != nil {
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = archivePath
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(writer, file)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext, fileList []string) (int, error) {
|
||||
filePath := fileList[0]
|
||||
fileName := filepath.Base(filePath)
|
||||
realPath, isDir, err := files.GetRealPath(d.user.Scope, filePath)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
if len(fileList) == 1 && !isDir {
|
||||
fd, err2 := os.Open(realPath)
|
||||
if err2 != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
// Get file information
|
||||
fileInfo, err3 := fd.Stat()
|
||||
if err3 != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Set headers and serve the file
|
||||
setContentDisposition(w, r, fileName)
|
||||
w.Header().Set("Cache-Control", "private")
|
||||
|
||||
// Serve the content
|
||||
http.ServeContent(w, r, fileName, fileInfo.ModTime(), fd)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
algo := r.URL.Query().Get("algo")
|
||||
var extension string
|
||||
switch algo {
|
||||
case "zip", "true", "":
|
||||
extension = ".zip"
|
||||
case "tar.gz":
|
||||
extension = ".tar.gz"
|
||||
default:
|
||||
return http.StatusInternalServerError, errors.New("format not implemented")
|
||||
}
|
||||
|
||||
baseDirName := filepath.Base(filepath.Dir(realPath))
|
||||
if baseDirName == "" || baseDirName == "/" {
|
||||
baseDirName = "download"
|
||||
}
|
||||
downloadFileName := url.PathEscape(baseDirName + "." + algo)
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+downloadFileName)
|
||||
// Create the archive and stream it directly to the response
|
||||
if extension == ".zip" {
|
||||
err = createZip(w, d, fileList...)
|
||||
} else {
|
||||
err = createTarGz(w, d, fileList...)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func createZip(w io.Writer, d *requestContext, filenames ...string) error {
|
||||
zipWriter := zip.NewWriter(w)
|
||||
defer zipWriter.Close()
|
||||
|
||||
for _, fname := range filenames {
|
||||
err := addFile(fname, d, nil, zipWriter)
|
||||
if err != nil {
|
||||
log.Printf("Failed to add %s to ZIP: %v", fname, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rawDirHandler(w http.ResponseWriter, r *http.Request, d *requestContext, file *files.FileInfo) (int, error) {
|
||||
filenames, err := parseQueryFiles(r, file, d.user)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
extension, ar, err := parseQueryAlgorithm(r)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
func createTarGz(w io.Writer, d *requestContext, filenames ...string) error {
|
||||
gzWriter := gzip.NewWriter(w)
|
||||
defer gzWriter.Close()
|
||||
|
||||
err = ar.Create(w)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
defer ar.Close()
|
||||
|
||||
commonDir := fileutils.CommonPrefix(filepath.Separator, filenames...)
|
||||
|
||||
name := filepath.Base(commonDir)
|
||||
if name == "." || name == "" || name == string(filepath.Separator) {
|
||||
name = file.Name
|
||||
}
|
||||
// Prefix used to distinguish a filelist generated
|
||||
// archive from the full directory archive
|
||||
if len(filenames) > 1 {
|
||||
name = "_" + name
|
||||
}
|
||||
name += extension
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(name))
|
||||
tarWriter := tar.NewWriter(gzWriter)
|
||||
defer tarWriter.Close()
|
||||
|
||||
for _, fname := range filenames {
|
||||
err = addFile(ar, d, fname, commonDir)
|
||||
err := addFile(fname, d, tarWriter, nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to archive %s: %v", fname, err)
|
||||
log.Printf("Failed to add %s to TAR.GZ: %v", fname, err)
|
||||
}
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo) (int, error) {
|
||||
realPath, _, _ := files.GetRealPath(file.Path)
|
||||
fd, err := os.Open(realPath)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
setContentDisposition(w, r, file)
|
||||
|
||||
w.Header().Set("Cache-Control", "private")
|
||||
http.ServeContent(w, r, file.Name, file.ModTime, fd)
|
||||
return 0, nil
|
||||
return nil
|
||||
}
|
||||
+59
-18
@@ -8,13 +8,14 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
// resourceGetHandler retrieves information about a resource.
|
||||
@@ -32,9 +33,13 @@ import (
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/resources [get]
|
||||
func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
|
||||
// TODO source := r.URL.Query().Get("source")
|
||||
path := r.URL.Query().Get("path")
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: filepath.Join(d.user.Scope, path),
|
||||
Modify: d.user.Perm.Modify,
|
||||
@@ -77,7 +82,12 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
// @Router /api/resources [delete]
|
||||
func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
// TODO source := r.URL.Query().Get("source")
|
||||
path := r.URL.Query().Get("path")
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
if path == "/" || !d.user.Perm.Delete {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
@@ -86,7 +96,7 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
fileOpts := files.FileOptions{
|
||||
Path: filepath.Join(d.user.Scope, path),
|
||||
Path: realPath,
|
||||
IsDir: isDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
@@ -129,7 +139,12 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon
|
||||
// @Router /api/resources [post]
|
||||
func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
// TODO source := r.URL.Query().Get("source")
|
||||
path := r.URL.Query().Get("path")
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
if !d.user.Perm.Create || !d.user.Check(path) {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
@@ -141,7 +156,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
}
|
||||
// Directories creation on POST.
|
||||
if strings.HasSuffix(path, "/") {
|
||||
err := files.WriteDirectory(fileOpts)
|
||||
err = files.WriteDirectory(fileOpts)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
@@ -187,7 +202,13 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
// @Router /api/resources [put]
|
||||
func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
// TODO source := r.URL.Query().Get("source")
|
||||
path := r.URL.Query().Get("path")
|
||||
// TODO source := r.URL.Query().Get("source")
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
if !d.user.Perm.Modify || !d.user.Check(path) {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
@@ -232,16 +253,21 @@ func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
// @Router /api/resources [patch]
|
||||
func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
// TODO source := r.URL.Query().Get("source")
|
||||
src := r.URL.Query().Get("from")
|
||||
dst := r.URL.Query().Get("destination")
|
||||
action := r.URL.Query().Get("action")
|
||||
dst, err := url.QueryUnescape(dst)
|
||||
if !d.user.Check(src) || !d.user.Check(dst) {
|
||||
return http.StatusForbidden, nil
|
||||
encodedFrom := r.URL.Query().Get("from")
|
||||
// Decode the URL-encoded path
|
||||
src, err := url.QueryUnescape(encodedFrom)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
dst := r.URL.Query().Get("destination")
|
||||
dst, err = url.QueryUnescape(dst)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
if !d.user.Check(src) || !d.user.Check(dst) {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
if dst == "/" || src == "/" {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
@@ -297,13 +323,14 @@ func delThumbs(ctx context.Context, fileCache FileCache, file *files.FileInfo) e
|
||||
|
||||
func patchAction(ctx context.Context, action, src, dst string, d *requestContext, fileCache FileCache, isSrcDir bool) error {
|
||||
switch action {
|
||||
// TODO: use enum
|
||||
case "copy":
|
||||
if !d.user.Perm.Create {
|
||||
return errors.ErrPermissionDenied
|
||||
}
|
||||
return files.CopyResource(src, dst, isSrcDir)
|
||||
err := files.CopyResource(src, dst, isSrcDir)
|
||||
return err
|
||||
case "rename", "move":
|
||||
|
||||
if !d.user.Perm.Rename {
|
||||
return errors.ErrPermissionDenied
|
||||
}
|
||||
@@ -377,9 +404,23 @@ func diskUsage(w http.ResponseWriter, r *http.Request, d *requestContext) (int,
|
||||
}
|
||||
|
||||
func inspectIndex(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Query().Get("path")
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
// Decode the URL-encoded path
|
||||
path, _ := url.QueryUnescape(encodedPath)
|
||||
isDir := r.URL.Query().Get("isDir") == "true"
|
||||
index := files.GetIndex(config.Server.Root)
|
||||
info, _ := index.GetReducedMetadata(path, isDir)
|
||||
renderJSON(w, r, info) // nolint:errcheck
|
||||
}
|
||||
|
||||
func mockData(w http.ResponseWriter, r *http.Request) {
|
||||
d := r.URL.Query().Get("numDirs")
|
||||
f := r.URL.Query().Get("numFiles")
|
||||
NumDirs, err := strconv.Atoi(d)
|
||||
numFiles, err2 := strconv.Atoi(f)
|
||||
if err != nil || err2 != nil {
|
||||
return
|
||||
}
|
||||
mockDir := files.CreateMockData(NumDirs, numFiles)
|
||||
renderJSON(w, r, mockDir) // nolint:errcheck
|
||||
}
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/version"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/version"
|
||||
|
||||
httpSwagger "github.com/swaggo/http-swagger" // http-swagger middleware
|
||||
)
|
||||
@@ -96,6 +96,7 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
|
||||
api.HandleFunc("GET /preview", withUser(previewHandler))
|
||||
if version.Version == "testing" || version.Version == "untracked" {
|
||||
api.HandleFunc("GET /inspectIndex", inspectIndex)
|
||||
api.HandleFunc("GET /mockData", mockData)
|
||||
}
|
||||
|
||||
// Share routes
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
// searchHandler handles search requests for files based on the provided query.
|
||||
@@ -49,7 +49,7 @@ import (
|
||||
// @Param query query string true "Search query"
|
||||
// @Param scope query string false "path within user scope to search, for example '/first/second' to search within the second directory only"
|
||||
// @Param SessionId header string false "User session ID, add unique value to prevent collisions"
|
||||
// @Success 200 {array} files.searchResult "List of search results"
|
||||
// @Success 200 {array} files.SearchResult "List of search results"
|
||||
// @Failure 400 {object} map[string]string "Bad Request"
|
||||
// @Router /api/search [get]
|
||||
func searchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
type settingsData struct {
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/share"
|
||||
)
|
||||
|
||||
// shareListHandler returns a list of all share links.
|
||||
|
||||
@@ -7,14 +7,13 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/version"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/version"
|
||||
)
|
||||
|
||||
var templateRenderer *TemplateRenderer
|
||||
@@ -52,7 +51,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
|
||||
"BaseURL": config.Server.BaseURL,
|
||||
"Version": version.Version,
|
||||
"CommitSHA": version.CommitSHA,
|
||||
"StaticURL": path.Join(config.Server.BaseURL, "static"),
|
||||
"StaticURL": config.Server.BaseURL + "static",
|
||||
"Signup": settings.Config.Auth.Signup,
|
||||
"NoAuth": config.Auth.Method == "noauth",
|
||||
"AuthMethod": config.Auth.Method,
|
||||
@@ -62,6 +61,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
|
||||
"EnableThumbs": config.Server.EnableThumbnails,
|
||||
"ResizePreview": config.Server.ResizePreview,
|
||||
"EnableExec": config.Server.EnableExec,
|
||||
"ReCaptchaHost": config.Auth.Recaptcha.Host,
|
||||
}
|
||||
|
||||
if config.Frontend.Files != "" {
|
||||
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
"golang.org/x/text/cases"
|
||||
"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"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
libErrors "github.com/gtsteffaniak/filebrowser/errors"
|
||||
libErrors "github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
)
|
||||
|
||||
func errToStatus(err error) int {
|
||||
|
||||
+43
-5
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/dsoprea/go-exif/v3"
|
||||
"github.com/marusama/semaphore/v2"
|
||||
|
||||
exifcommon "github.com/dsoprea/go-exif/v3/common"
|
||||
)
|
||||
@@ -21,12 +20,32 @@ var ErrUnsupportedFormat = errors.New("unsupported image format")
|
||||
|
||||
// Service
|
||||
type Service struct {
|
||||
sem semaphore.Semaphore
|
||||
sem chan struct{}
|
||||
}
|
||||
|
||||
// New initializes the service with a specified number of workers (concurrency limit).
|
||||
func New(workers int) *Service {
|
||||
return &Service{
|
||||
sem: semaphore.New(workers),
|
||||
sem: make(chan struct{}, workers), // Buffered channel to limit concurrency.
|
||||
}
|
||||
}
|
||||
|
||||
// acquire blocks until a worker is available or the context is canceled.
|
||||
func (s *Service) acquire(ctx context.Context) error {
|
||||
select {
|
||||
case s.sem <- struct{}{}: // Reserve a worker.
|
||||
return nil
|
||||
case <-ctx.Done(): // Context canceled or deadline exceeded.
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// release frees up a worker slot.
|
||||
func (s *Service) release() {
|
||||
select {
|
||||
case <-s.sem: // Free a worker slot.
|
||||
default:
|
||||
// Shouldn't happen, but guard against releasing more than acquired.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,10 +155,10 @@ func WithQuality(quality Quality) Option {
|
||||
}
|
||||
|
||||
func (s *Service) Resize(ctx context.Context, in io.Reader, width, height int, out io.Writer, options ...Option) error {
|
||||
if err := s.sem.Acquire(ctx, 1); err != nil {
|
||||
if err := s.acquire(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.sem.Release(1)
|
||||
defer s.release()
|
||||
|
||||
format, wrappedReader, err := s.detectFormat(in)
|
||||
if err != nil {
|
||||
@@ -242,3 +261,22 @@ func getEmbeddedThumbnail(in io.Reader) ([]byte, io.Reader, error) {
|
||||
thm, err := ifd.Thumbnail()
|
||||
return thm, wrappedReader, err
|
||||
}
|
||||
|
||||
// CreateThumbnail takes raw image data and creates a thumbnail image.
|
||||
func CreateThumbnail(rawData io.Reader, width, height int) (image.Image, error) {
|
||||
// Decode the raw image to get an image.Image.
|
||||
img, _, err := image.Decode(rawData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode image: %w", err)
|
||||
}
|
||||
|
||||
// Resize the image to create a thumbnail using the specified dimensions.
|
||||
thumb := imaging.Fit(img, width, height, imaging.Lanczos)
|
||||
|
||||
// Optionally, convert the thumbnail to grayscale if needed.
|
||||
// Uncomment the line below if you want the result to be grayscale.
|
||||
// thumb = imaging.Grayscale(thumb)
|
||||
|
||||
// Return the resized thumbnail image.
|
||||
return thumb, nil
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/cmd"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -3,7 +3,7 @@ package runner
|
||||
import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
// ParseCommand parses the command taking in account if the current
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
// Runner is a commands runner.
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
var Config Settings
|
||||
|
||||
@@ -3,7 +3,7 @@ package settings
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
func TestSettings_MakeUserDir(t *testing.T) {
|
||||
|
||||
@@ -3,7 +3,7 @@ package settings
|
||||
import (
|
||||
"crypto/rand"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
const DefaultUsersHomeBasePath = "/users"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
// StorageBackend is a settings storage backend.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
|
||||
@@ -3,7 +3,7 @@ package share
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
)
|
||||
|
||||
// StorageBackend is the interface to implement for a share storage.
|
||||
|
||||
@@ -2,8 +2,8 @@ package bolt
|
||||
|
||||
import (
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/gtsteffaniak/filebrowser/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
)
|
||||
|
||||
type authBackend struct {
|
||||
|
||||
@@ -3,10 +3,10 @@ package bolt
|
||||
import (
|
||||
"github.com/asdine/storm/v3"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/share"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
// NewStorage creates a storage.Storage based on Bolt DB.
|
||||
|
||||
@@ -2,7 +2,7 @@ package bolt
|
||||
|
||||
import (
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
type settingsBackend struct {
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/asdine/storm/v3/q"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/share"
|
||||
)
|
||||
|
||||
type shareBackend struct {
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
"github.com/gtsteffaniak/filebrowser/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
type usersBackend struct {
|
||||
|
||||
@@ -3,7 +3,7 @@ package bolt
|
||||
import (
|
||||
"github.com/asdine/storm/v3"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
)
|
||||
|
||||
func get(db *storm.DB, name string, to interface{}) error {
|
||||
|
||||
@@ -7,14 +7,14 @@ import (
|
||||
"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"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage/bolt"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
// Storage is a storage powered by a Backend which makes the necessary
|
||||
|
||||
@@ -114,17 +114,11 @@ const docTemplate = `{
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Path to the file or directory",
|
||||
"name": "path",
|
||||
"description": "Comma-separated list of specific files within the directory (required)",
|
||||
"name": "files",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Comma-separated list of specific files within the directory (optional)",
|
||||
"name": "files",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults to 'attachment'.",
|
||||
@@ -133,7 +127,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Compression algorithm for archiving multiple files or directories. Options: 'zip', 'tar', 'targz', 'tarbz2', 'tarxz', 'tarlz4', 'tarsz'. Default is 'zip'.",
|
||||
"description": "Compression algorithm for archiving multiple files or directories. Options: 'zip' and 'tar.gz'. Default is 'zip'.",
|
||||
"name": "algo",
|
||||
"in": "query"
|
||||
}
|
||||
@@ -172,15 +166,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"415": {
|
||||
"description": "Unsupported file type for preview",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
@@ -594,7 +579,7 @@ const docTemplate = `{
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/files.searchResult"
|
||||
"$ref": "#/definitions/files.SearchResult"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1201,7 +1186,7 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"files.searchResult": {
|
||||
"files.SearchResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
|
||||
@@ -103,17 +103,11 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Path to the file or directory",
|
||||
"name": "path",
|
||||
"description": "Comma-separated list of specific files within the directory (required)",
|
||||
"name": "files",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Comma-separated list of specific files within the directory (optional)",
|
||||
"name": "files",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults to 'attachment'.",
|
||||
@@ -122,7 +116,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Compression algorithm for archiving multiple files or directories. Options: 'zip', 'tar', 'targz', 'tarbz2', 'tarxz', 'tarlz4', 'tarsz'. Default is 'zip'.",
|
||||
"description": "Compression algorithm for archiving multiple files or directories. Options: 'zip' and 'tar.gz'. Default is 'zip'.",
|
||||
"name": "algo",
|
||||
"in": "query"
|
||||
}
|
||||
@@ -161,15 +155,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"415": {
|
||||
"description": "Unsupported file type for preview",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
@@ -583,7 +568,7 @@
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/files.searchResult"
|
||||
"$ref": "#/definitions/files.SearchResult"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1190,7 +1175,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"files.searchResult": {
|
||||
"files.SearchResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
|
||||
@@ -31,7 +31,7 @@ definitions:
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
files.searchResult:
|
||||
files.SearchResult:
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
@@ -338,14 +338,10 @@ paths:
|
||||
description: Returns the raw content of a file, multiple files, or a directory.
|
||||
Supports downloading files as archives in various formats.
|
||||
parameters:
|
||||
- description: Path to the file or directory
|
||||
in: query
|
||||
name: path
|
||||
required: true
|
||||
type: string
|
||||
- description: Comma-separated list of specific files within the directory (optional)
|
||||
- description: Comma-separated list of specific files within the directory (required)
|
||||
in: query
|
||||
name: files
|
||||
required: true
|
||||
type: string
|
||||
- description: If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults
|
||||
to 'attachment'.
|
||||
@@ -353,8 +349,7 @@ paths:
|
||||
name: inline
|
||||
type: boolean
|
||||
- description: 'Compression algorithm for archiving multiple files or directories.
|
||||
Options: ''zip'', ''tar'', ''targz'', ''tarbz2'', ''tarxz'', ''tarlz4'',
|
||||
''tarsz''. Default is ''zip''.'
|
||||
Options: ''zip'' and ''tar.gz''. Default is ''zip''.'
|
||||
in: query
|
||||
name: algo
|
||||
type: string
|
||||
@@ -383,12 +378,6 @@ paths:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"415":
|
||||
description: Unsupported file type for preview
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
@@ -669,7 +658,7 @@ paths:
|
||||
description: List of search results
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/files.searchResult'
|
||||
$ref: '#/definitions/files.SearchResult'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
)
|
||||
|
||||
// StorageBackend is the interface to implement for a users storage.
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Q&A topics
|
||||
|
||||
## When will there be a stable release?
|
||||
|
||||
Please see [roadmap](roadmap.md) for future plans -- yes a stable release will eventually come.
|
||||
|
||||
## Can you add FileBrowser Quantum features to the original file browser?
|
||||
|
||||
This "Quantum" version fork was created because I wanted certain features that are a dramatic and opinionated departure from the OG filebrowser. If you look at the original filebrowser repo pull requests, you will find there are many basic features that remain open for many months or years with very little attention.
|
||||
|
||||
If the original filebrowser maintainers were more active and if I didn't have to worry about spending months or years playing politics about the concequences of these drastic changes, I would contribute to the original repository.
|
||||
|
||||
However, **I will not make an modifications to the original filebrowser**, for these reasons:
|
||||
|
||||
1. My changes are opinionated and I want full control over the experience (and consequences) of the changes. For example I removed the terminal, runners, command line flags, and more. These are changes that would probably be highly contested. I think the experience is better without them, or with the changes -- and I hope you agree.
|
||||
2. Contributing to that open source project takes a long time, I may never see my changes actually make it in and don't want to waste my time trying for years, I only have so much time.
|
||||
3. This project was originally a fork, but that quickly changed. There are hundereds of thousands of changes and complete departures from the original codebase. I can't simply "port" the features I write on this repo over to the OG file browser.
|
||||
|
||||
Both of these repos being open source means YOU can migrate these features if you want! Feel free to spend the effort and do so for the community. I am not the only one capable of doing it and I encourage this if you have the time, energy, and knowhow to do it.
|
||||
|
||||
## I notice a lot of things that don't work like the original file browser repo, how can I get this fixed?
|
||||
|
||||
Please open issues and/or pull requests from a forked repo if you notice issues that should be fixed. Some changes are intentional and I may have left things broken. For example, I am not fully confident user rules work in 100% of places. When you notice things, please let me know and I will check if its an intentional change or a bug.
|
||||
|
||||
## Is there a way to donate or support this project?
|
||||
|
||||
Nope... not yet! It's still "unfinished" in my opinion, so I don't want to ask for any money from it. But if you have a strong desire to donate, email info@quantumx-apps.com to get in touch.
|
||||
|
||||
## Is there an email or phone I can contact?
|
||||
|
||||
Yes - Please contact info@quantumx-apps.com for any off-github topics. If its related to a specific application or repo such as this filebrowser, please open an issue or pull request instead. Email is only for corrospondance unrelated to technical changes or issues.
|
||||
|
||||
## Can I fork this repo and use it?
|
||||
|
||||
This repo has the same license as the original filebrowser, apache-2.0. Feel free to use in any way that follows the license. I have no issues with anything personally -- its open source please do as you like. However, since this is a fork of the OG repo, I am not sure what the consequences are for a fork of this repo.
|
||||
|
||||
## Who are the maintainers for FileBrowser Quantum?
|
||||
|
||||
Right now, just me as a personal hobby and some small contributions from the community. Once I can release a confident stable version, I plan to publicize this application more on social media. Hopefully, in the future I could pick up some extra contributors.
|
||||
|
||||
I'm not looking for contributors at the moment, but if you want to me a contributor feel free to email me at info@quantumx-apps.com to see about getting contributor access.
|
||||
|
||||
## Are there plans to charge for this product?
|
||||
|
||||
No, this repo and project will always be free to use.
|
||||
|
||||
## Is there a discord for this fork?
|
||||
|
||||
Not yet, generally most interactions should happen on github for now.
|
||||
|
||||
+38
-15
@@ -1,23 +1,46 @@
|
||||
# Planned Roadmap
|
||||
|
||||
upcoming 0.3.x releases, ordered by priority:
|
||||
Upcoming 0.3.x releases, ordered by priority:
|
||||
- Bring Themes and Branding back.
|
||||
- openoffice support https://github.com/filebrowser/filebrowser/pull/2954
|
||||
- More filetype previews: eg. raw img, office, photoshop, vector, 3d files.
|
||||
- Introduce jobs as replacement to runners.
|
||||
- Add Job status to the sidebar
|
||||
- index status.
|
||||
- Job status from users
|
||||
- upload status
|
||||
- Opentelemetry metrics
|
||||
- user access,
|
||||
- file access
|
||||
- download count
|
||||
- last login
|
||||
- more sign in support
|
||||
- LDAP
|
||||
- 2FA
|
||||
- SSO
|
||||
|
||||
- More filetype icons and refreshed icons.
|
||||
- more filetype previews - eg. office, photoshop, vector, 3d files.
|
||||
- Enable mobile search with same features as desktop
|
||||
- Enable mobile search with same features as desktop
|
||||
- Theme configuration from settings
|
||||
- introduce jobs as replacement to runners.
|
||||
- Add Job status to the sidebar
|
||||
- index status.
|
||||
- Job status from users
|
||||
- upload status
|
||||
- opentelemetry metrics
|
||||
Upcoming 0.4.x release:
|
||||
- Support for multiple filesystem sources https://github.com/filebrowser/filebrowser/issues/2514
|
||||
- Onboarding process to add sources and configure them on first run.
|
||||
- More indexing flexability
|
||||
- option not to index hidden files/folders
|
||||
- options folders to include/exclude from indexing
|
||||
- implement more indexing runners for more efficienct filesystem watching
|
||||
- tags support
|
||||
|
||||
Stable release (v1.0.0) - Planned 2025:
|
||||
- Once under the hood changes for things like multiple sources, jobs support, etc
|
||||
- More robust backend and frontend testing
|
||||
- Currently a stable release does not exist primarily because things are still changing, configuration changes are happening frequently and will for the next
|
||||
- Rebrand to QuantumX App suite umbrella branding and github repo change.
|
||||
|
||||
Unplanned Future releases:
|
||||
- multiple sources https://github.com/filebrowser/filebrowser/issues/2514
|
||||
- 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
|
||||
- support more source types such as minio, s3, and backblaze sources https://github.com/filebrowser/filebrowser/issues/2544
|
||||
- Activity Log
|
||||
- Comments support
|
||||
- Trash Support
|
||||
- starred/pinned files
|
||||
- event based notifications
|
||||
@@ -25,6 +25,7 @@
|
||||
"css-vars-ponyfill": "^2.4.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"material-icons": "^1.10.5",
|
||||
"material-symbols": "^0.27.2",
|
||||
"normalize.css": "^8.0.1",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"vue": "^3.4.21",
|
||||
|
||||
+89
-70
@@ -1,11 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
{{ if .ReCaptcha }}
|
||||
<script src="{{ .ReCaptchaHost }}/recaptcha/api.js?render=explicit" data-vite-ignore></script>
|
||||
<script src="{{ .ReCaptchaHost }}/recaptcha/api.js?render=explicit" data-vite-ignore></script>
|
||||
{{ end }}
|
||||
|
||||
<title>{{ if .Name }}{{ .Name }}{{ else }}FileBrowser Quantum{{ end }}</title>
|
||||
@@ -19,7 +20,7 @@
|
||||
<!-- Add to home screen for Safari on iOS/iPadOS -->
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="assets">
|
||||
<link rel="apple-touch-icon" href="{{ .StaticURL }}/img/icons/apple-touch-icon.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ .StaticURL }}/img/icons/android-chrome-256x256.png">
|
||||
|
||||
<!-- Add to home screen for Windows -->
|
||||
<meta name="msapplication-TileImage" content="{{ .StaticURL }}/img/icons/mstile-144x144.png">
|
||||
@@ -27,94 +28,111 @@
|
||||
|
||||
<!-- Inject Some Variables and generate the manifest json -->
|
||||
<script>
|
||||
window.FileBrowser = JSON.parse('{{ .globalVars }}');
|
||||
|
||||
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
|
||||
window.FileBrowser = JSON.parse('{{ .globalVars }}');
|
||||
var dynamicManifest = {
|
||||
"name": window.FileBrowser.Name || 'FileBrowser Quantum',
|
||||
"short_name": window.FileBrowser.Name || 'FileBrowser',
|
||||
"icons": [
|
||||
{
|
||||
"src": fullStaticURL + "/img/icons/android-chrome-256x256.png",
|
||||
"src": window.location.origin + "{{ .StaticURL }}/img/icons/android-chrome-256x256.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"src": window.location.origin + "{{ .StaticURL }}/img/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
],
|
||||
"start_url": fullStaticURL,
|
||||
"start_url": window.location.origin + "{{ .BaseURL }}",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": window.FileBrowser.Color || "#455a64"
|
||||
}
|
||||
|
||||
const stringManifest = JSON.stringify(dynamicManifest);
|
||||
const blob = new Blob([stringManifest], {type: 'application/json'});
|
||||
const blob = new Blob([stringManifest], { type: 'application/json' });
|
||||
const manifestURL = URL.createObjectURL(blob);
|
||||
document.querySelector('#manifestPlaceholder').setAttribute('href', manifestURL);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
z-index: 9999;
|
||||
transition: .1s ease opacity;
|
||||
-webkit-transition: .1s ease opacity;
|
||||
}
|
||||
|
||||
#loading.done {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#loading .spinner {
|
||||
width: 70px;
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#loading .spinner > div {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: #333;
|
||||
border-radius: 100%;
|
||||
display: inline-block;
|
||||
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
|
||||
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
|
||||
}
|
||||
|
||||
#loading .spinner .bounce1 {
|
||||
-webkit-animation-delay: -0.32s;
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
#loading .spinner .bounce2 {
|
||||
-webkit-animation-delay: -0.16s;
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes sk-bouncedelay {
|
||||
0%, 80%, 100% { -webkit-transform: scale(0) }
|
||||
40% { -webkit-transform: scale(1.0) }
|
||||
}
|
||||
|
||||
@keyframes sk-bouncedelay {
|
||||
0%, 80%, 100% {
|
||||
-webkit-transform: scale(0);
|
||||
transform: scale(0);
|
||||
} 40% {
|
||||
-webkit-transform: scale(1.0);
|
||||
transform: scale(1.0);
|
||||
#loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
z-index: 9999;
|
||||
transition: .1s ease opacity;
|
||||
-webkit-transition: .1s ease opacity;
|
||||
}
|
||||
|
||||
#loading.done {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#loading .spinner {
|
||||
width: 70px;
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#loading .spinner>div {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: #333;
|
||||
border-radius: 100%;
|
||||
display: inline-block;
|
||||
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
|
||||
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
|
||||
}
|
||||
|
||||
#loading .spinner .bounce1 {
|
||||
-webkit-animation-delay: -0.32s;
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
#loading .spinner .bounce2 {
|
||||
-webkit-animation-delay: -0.16s;
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes sk-bouncedelay {
|
||||
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
-webkit-transform: scale(0)
|
||||
}
|
||||
|
||||
40% {
|
||||
-webkit-transform: scale(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sk-bouncedelay {
|
||||
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
-webkit-transform: scale(0);
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
40% {
|
||||
-webkit-transform: scale(1.0);
|
||||
transform: scale(1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -138,7 +156,8 @@
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
|
||||
{{ if .CSS }}
|
||||
<link rel="stylesheet" href="{{ .StaticURL }}/custom.css" >
|
||||
<link rel="stylesheet" href="{{ .StaticURL }}/custom.css">
|
||||
{{ end }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "FileBrowser",
|
||||
"short_name": "FileBrowser",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./img/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./static/img/icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": "./",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#455a64"
|
||||
}
|
||||
+21
-15
@@ -6,7 +6,7 @@ import { notify } from "@/notify";
|
||||
// Notify if errors occur
|
||||
export async function fetchFiles(url, content = false) {
|
||||
try {
|
||||
let path = removePrefix(url, "files");
|
||||
let path = encodeURIComponent(removePrefix(url, "files"));
|
||||
const apiPath = getApiPath("api/resources",{path: path, content: content});
|
||||
const res = await fetchURL(apiPath);
|
||||
const data = await res.json();
|
||||
@@ -24,7 +24,8 @@ async function resourceAction(url, method, content) {
|
||||
if (content) {
|
||||
opts.body = content;
|
||||
}
|
||||
const apiPath = getApiPath("api/resources", { path: url });
|
||||
let path = encodeURIComponent(removePrefix(url, "files"));
|
||||
const apiPath = getApiPath("api/resources", { path: path });
|
||||
const res = await fetchURL(apiPath, opts);
|
||||
return res;
|
||||
} catch (err) {
|
||||
@@ -35,7 +36,8 @@ async function resourceAction(url, method, content) {
|
||||
|
||||
export async function remove(url) {
|
||||
try {
|
||||
return await resourceAction(url, "DELETE");
|
||||
let path = encodeURIComponent(removePrefix(url, "files"));
|
||||
return await resourceAction(path, "DELETE");
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error deleting resource");
|
||||
throw err;
|
||||
@@ -44,26 +46,29 @@ export async function remove(url) {
|
||||
|
||||
export async function put(url, content = "") {
|
||||
try {
|
||||
return await resourceAction(url, "PUT", content);
|
||||
let path = encodeURIComponent(removePrefix(url, "files"));
|
||||
return await resourceAction(path, "PUT", content);
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error putting resource");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function download(format, ...files) {
|
||||
export function download(format, files) {
|
||||
if (format != "zip") {
|
||||
format = "tar.gz"
|
||||
}
|
||||
try {
|
||||
let path = "";
|
||||
let fileargs = "";
|
||||
if (files.length === 1) {
|
||||
path = removePrefix(files[0], "files")
|
||||
fileargs = decodeURI(removePrefix(files[0], "files"))
|
||||
} else {
|
||||
for (let file of files) {
|
||||
fileargs += removePrefix(file,"files") + ",";
|
||||
fileargs += decodeURI(removePrefix(file,"files")) + ",";
|
||||
}
|
||||
fileargs = fileargs.substring(0, fileargs.length - 1);
|
||||
}
|
||||
const apiPath = getApiPath("api/raw", { path: path, files: fileargs, algo: format });
|
||||
const apiPath = getApiPath("api/raw", { files: encodeURIComponent(fileargs), algo: format });
|
||||
const url = window.origin+apiPath
|
||||
window.open(url);
|
||||
} catch (err) {
|
||||
@@ -128,9 +133,11 @@ export async function moveCopy(items, action = "copy", overwrite = false, rename
|
||||
}
|
||||
try {
|
||||
for (let item of items) {
|
||||
let toPath = encodeURIComponent(removePrefix(decodeURI(item.to), "files"));
|
||||
let fromPath = encodeURIComponent(removePrefix(decodeURI(item.from), "files"));
|
||||
let localParams = { ...params };
|
||||
localParams.destination = item.to;
|
||||
localParams.from = item.from;
|
||||
localParams.destination = toPath;
|
||||
localParams.from = fromPath;
|
||||
const apiPath = getApiPath("api/resources", localParams);
|
||||
promises.push(fetch(apiPath, { method: "PATCH" }));
|
||||
}
|
||||
@@ -153,10 +160,9 @@ export async function checksum(url, algo) {
|
||||
}
|
||||
|
||||
export function getDownloadURL(path, inline) {
|
||||
|
||||
try {
|
||||
const params = {
|
||||
path: removePrefix(path,"files"),
|
||||
files: encodeURIComponent(removePrefix(decodeURI(path),"files")),
|
||||
...(inline && { inline: "true" }),
|
||||
};
|
||||
const apiPath = getApiPath("api/raw", params);
|
||||
@@ -170,7 +176,7 @@ export function getDownloadURL(path, inline) {
|
||||
export function getPreviewURL(path, size, modified) {
|
||||
try {
|
||||
const params = {
|
||||
path: path,
|
||||
path: encodeURIComponent(removePrefix(decodeURI(path),"files")),
|
||||
size: size,
|
||||
key: Date.parse(modified),
|
||||
inline: "true",
|
||||
@@ -189,7 +195,7 @@ export function getSubtitlesURL(file) {
|
||||
for (const sub of file.subtitles) {
|
||||
const params = {
|
||||
inline: "true",
|
||||
path: sub
|
||||
path: encodeURIComponent(removePrefix(sub,"files"))
|
||||
};
|
||||
const apiPath = getApiPath("api/raw", params);
|
||||
return window.origin+apiPath
|
||||
|
||||
@@ -61,7 +61,6 @@ export async function fetchJSON(url, opts) {
|
||||
|
||||
export function adjustedData(data, url) {
|
||||
data.url = url;
|
||||
|
||||
if (data.type === "directory") {
|
||||
if (!data.url.endsWith("/")) data.url += "/";
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ export default {
|
||||
.button-group {
|
||||
margin: 1em;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 1em;
|
||||
overflow: hidden;
|
||||
@@ -93,7 +94,6 @@ button {
|
||||
border: none;
|
||||
background: #f5f5f5;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
/* Add borders */
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
left: `${left}px`,
|
||||
}"
|
||||
class="button"
|
||||
:class="{ 'dark-mode': isDarkMode, mobile: isMobile }"
|
||||
:class="{ 'dark-mode': isDarkMode, centered: centered }"
|
||||
>
|
||||
<div v-if="selectedCount > 0" class="button selected-count-header">
|
||||
<span>{{ selectedCount }} selected</span>
|
||||
@@ -108,8 +108,8 @@ export default {
|
||||
user() {
|
||||
return state.user;
|
||||
},
|
||||
isMobile() {
|
||||
return getters.isMobile();
|
||||
centered() {
|
||||
return getters.isMobile() || ( !this.posX || !this.posY );
|
||||
},
|
||||
showContext() {
|
||||
if (getters.currentPromptName() == "ContextMenu" && state.prompts != []) {
|
||||
@@ -122,14 +122,12 @@ export default {
|
||||
// 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)
|
||||
);
|
||||
},
|
||||
@@ -161,8 +159,24 @@ export default {
|
||||
},
|
||||
setPositions() {
|
||||
const contextProps = getters.currentPrompt().props;
|
||||
this.posX = contextProps.posX;
|
||||
this.posY = contextProps.posY;
|
||||
let tempX = contextProps.posX;
|
||||
let tempY = contextProps.posY;
|
||||
// Assuming the screen width and height (adjust values based on your context)
|
||||
const screenWidth = window.innerWidth; // or any fixed width depending on your app's layout
|
||||
const screenHeight = window.innerHeight; // or any fixed height depending on your app's layout
|
||||
|
||||
// if x is too close to the right edge, move it to the left by 400px
|
||||
if (tempX > screenWidth - 200) {
|
||||
tempX -= 200;
|
||||
}
|
||||
|
||||
// if y is too close to the bottom edge, move it up by 400px
|
||||
if (tempY > screenHeight - 400) {
|
||||
tempY -= 400;
|
||||
}
|
||||
|
||||
this.posX = tempX;
|
||||
this.posY = tempY;
|
||||
},
|
||||
toggleMultipleSelection() {
|
||||
mutations.setMultiple(!state.multiple);
|
||||
@@ -187,9 +201,10 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#context-menu.mobile {
|
||||
#context-menu.centered {
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<span>
|
||||
<!-- Material Icon -->
|
||||
<i v-if="isMaterialIcon" :class="classes" class="icon"> {{ materialIcon }} </i>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getTypeInfo } from "@/utils/mimetype";
|
||||
|
||||
export default {
|
||||
name: "Icon",
|
||||
props: {
|
||||
mimetype: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
materialIcon: "",
|
||||
classes: "",
|
||||
svgPath: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isMaterialIcon() {
|
||||
return this.materialIcon !== "";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getIconForType(mimetype) {
|
||||
return getTypeInfo(mimetype);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const result = this.getIconForType(this.mimetype);
|
||||
this.classes = result.classes || "material-icons"; // Default class
|
||||
this.color = result.color || "lightgray"; // Default color
|
||||
this.materialIcon = result.materialIcon || "";
|
||||
this.svgPath = result.svgPath || ""; // For SVG file paths
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-icons [aria-label^="."] {
|
||||
opacity: 0.33;
|
||||
}
|
||||
|
||||
.file-icons [aria-label$=".bak"] {
|
||||
opacity: 0.33;
|
||||
}
|
||||
|
||||
.svg-icons {
|
||||
display: flex;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.5rem;
|
||||
/* Default size */
|
||||
fill: currentColor;
|
||||
/* Uses inherited color */
|
||||
}
|
||||
|
||||
.purple-icons {
|
||||
color: purple;
|
||||
}
|
||||
|
||||
/* Icon Colors */
|
||||
.blue-icons {
|
||||
color: var(--icon-blue);
|
||||
}
|
||||
|
||||
.lightblue-icons {
|
||||
color: lightskyblue;
|
||||
}
|
||||
|
||||
.orange-icons {
|
||||
color: lightcoral;
|
||||
}
|
||||
|
||||
.tan-icons {
|
||||
color: tan;
|
||||
}
|
||||
|
||||
.plum-icons {
|
||||
color: plum;
|
||||
}
|
||||
|
||||
.red-icons {
|
||||
color: rgb(246, 70, 70);
|
||||
}
|
||||
|
||||
.beige-icons {
|
||||
color: beige;
|
||||
}
|
||||
|
||||
.deep-blue-icons {
|
||||
color: rgb(29, 95, 191);
|
||||
}
|
||||
|
||||
.green-icons {
|
||||
color: rgb(23, 128, 74);
|
||||
}
|
||||
|
||||
.red-orange-icons {
|
||||
color: rgb(255, 147, 111);
|
||||
}
|
||||
|
||||
.gray-icons {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.skyblue-icons {
|
||||
color: rgb(42, 170, 242);
|
||||
}
|
||||
|
||||
.lightgray-icons {
|
||||
color: lightgray;
|
||||
}
|
||||
|
||||
.yellow-icons {
|
||||
color: yellow;
|
||||
}
|
||||
</style>
|
||||
@@ -29,122 +29,57 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Search results for mobile -->
|
||||
<div v-if="isMobile && active" id="result" :class="{ hidden: !active }" ref="result">
|
||||
<div id="result-list">
|
||||
<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" class="search-entry">
|
||||
<router-link :to="createPath(s.path)">
|
||||
<i v-if="s.type == 'directory'" class="material-icons folder-icons">
|
||||
folder
|
||||
</i>
|
||||
<i v-else-if="s.type == 'audio'" class="material-icons audio-icons">
|
||||
volume_up
|
||||
</i>
|
||||
<i v-else-if="s.type == 'image'" class="material-icons image-icons">
|
||||
photo
|
||||
</i>
|
||||
<i v-else-if="s.type == 'video'" class="material-icons video-icons">
|
||||
movie
|
||||
</i>
|
||||
<i v-else-if="s.type == 'archive'" class="material-icons archive-icons">
|
||||
archive
|
||||
</i>
|
||||
<i v-else class="material-icons file-icons"> insert_drive_file </i>
|
||||
<span class="text-container">
|
||||
{{ basePath(s.path, s.type == "directory") }}<b>{{ baseName(s.path) }}</b>
|
||||
</span>
|
||||
<div class="filesize">{{ humanSize(s.size) }}</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- Loading icon when search is ongoing -->
|
||||
<p v-show="isEmpty && isRunning" id="renew">
|
||||
<i class="material-icons spin">autorenew</i>
|
||||
</p>
|
||||
<!-- Message when no results are found -->
|
||||
<div v-show="isEmpty && !isRunning">
|
||||
<div class="searchPrompt" v-show="isEmpty && !isRunning">
|
||||
<p>{{ noneMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isEmpty">
|
||||
<!-- Reset filters button -->
|
||||
<button
|
||||
class="mobile-boxes"
|
||||
v-if="value.length === 0 && !showBoxes"
|
||||
@click="resetSearchFilters()"
|
||||
>
|
||||
Reset filters
|
||||
</button>
|
||||
<!-- Box types when no search input is present -->
|
||||
<div v-if="value.length === 0 && showBoxes">
|
||||
<div class="boxes">
|
||||
<h3>{{ $t("search.types") }}</h3>
|
||||
<div>
|
||||
<div
|
||||
class="mobile-boxes"
|
||||
tabindex="0"
|
||||
v-for="(v, k) in boxes"
|
||||
:key="k"
|
||||
role="button"
|
||||
@click="addToTypes('type:' + k)"
|
||||
:aria-label="v.label"
|
||||
>
|
||||
<i class="material-icons">{{ v.icon }}</i>
|
||||
<p>{{ v.label }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search results for desktop -->
|
||||
<div v-show="!isMobile && active" id="result-desktop" ref="result">
|
||||
<div v-show="active" id="results" ref="result">
|
||||
<div class="searchContext">Search Context: {{ getContext }}</div>
|
||||
<div id="result-list">
|
||||
<div>
|
||||
<div v-if="!isMobile && active">
|
||||
<!-- Button groups for filtering search results -->
|
||||
<ButtonGroup
|
||||
:buttons="folderSelect"
|
||||
@button-clicked="addToTypes"
|
||||
@remove-button-clicked="removeFromTypes"
|
||||
@disableAll="folderSelectClicked()"
|
||||
@enableAll="resetButtonGroups()"
|
||||
/>
|
||||
<ButtonGroup
|
||||
:buttons="typeSelect"
|
||||
@button-clicked="addToTypes"
|
||||
@remove-button-clicked="removeFromTypes"
|
||||
:isDisabled="isTypeSelectDisabled"
|
||||
/>
|
||||
<!-- Inputs for filtering by file size -->
|
||||
<div class="sizeConstraints">
|
||||
<div class="sizeInputWrapper">
|
||||
<p>Smaller Than:</p>
|
||||
<input
|
||||
class="sizeInput"
|
||||
v-model="smallerThan"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="number"
|
||||
/>
|
||||
<p>MB</p>
|
||||
</div>
|
||||
<div class="sizeInputWrapper">
|
||||
<p>Larger Than:</p>
|
||||
<input
|
||||
class="sizeInput"
|
||||
v-model="largerThan"
|
||||
type="number"
|
||||
placeholder="number"
|
||||
/>
|
||||
<p>MB</p>
|
||||
<div v-if="active">
|
||||
<div v-if="isMobile">
|
||||
<ButtonGroup
|
||||
:buttons="toggleOptionButton"
|
||||
@button-clicked="enableOptions"
|
||||
@remove-button-clicked="disableOptions"
|
||||
/>
|
||||
</div>
|
||||
<div v-show="showOptions">
|
||||
<!-- Button groups for filtering search results -->
|
||||
<ButtonGroup
|
||||
:buttons="folderSelect"
|
||||
@button-clicked="addToTypes"
|
||||
@remove-button-clicked="removeFromTypes"
|
||||
@disableAll="folderSelectClicked()"
|
||||
@enableAll="resetButtonGroups()"
|
||||
/>
|
||||
<ButtonGroup
|
||||
:buttons="typeSelect"
|
||||
@button-clicked="addToTypes"
|
||||
@remove-button-clicked="removeFromTypes"
|
||||
:isDisabled="isTypeSelectDisabled"
|
||||
/>
|
||||
<!-- Inputs for filtering by file size -->
|
||||
<div class="sizeConstraints">
|
||||
<div class="sizeInputWrapper">
|
||||
<p>Smaller Than:</p>
|
||||
<input
|
||||
class="sizeInput"
|
||||
v-model="smallerThan"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="number"
|
||||
/>
|
||||
<p>MB</p>
|
||||
</div>
|
||||
<div class="sizeInputWrapper">
|
||||
<p>Larger Than:</p>
|
||||
<input
|
||||
class="sizeInput"
|
||||
v-model="largerThan"
|
||||
type="number"
|
||||
placeholder="number"
|
||||
/>
|
||||
<p>MB</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -181,28 +116,14 @@
|
||||
<!-- List of search results -->
|
||||
<ul v-show="results.length > 0">
|
||||
<li v-for="(s, k) in results" :key="k" class="search-entry">
|
||||
<router-link :to="createPath(s.path)">
|
||||
<i v-if="s.type == 'directory'" class="material-icons folder-icons">
|
||||
folder
|
||||
</i>
|
||||
<i v-else-if="s.type == 'audio'" class="material-icons audio-icons">
|
||||
volume_up
|
||||
</i>
|
||||
<i v-else-if="s.type == 'image'" class="material-icons image-icons">
|
||||
photo
|
||||
</i>
|
||||
<i v-else-if="s.type == 'video'" class="material-icons video-icons">
|
||||
movie
|
||||
</i>
|
||||
<i v-else-if="s.type == 'archive'" class="material-icons archive-icons">
|
||||
archive
|
||||
</i>
|
||||
<i v-else class="material-icons file-icons"> insert_drive_file </i>
|
||||
<a :href="getRelative(s.path)">
|
||||
<Icon :mimetype="s.type" />
|
||||
<span class="text-container">
|
||||
{{ basePath(s.path, s.type == "directory") }}<b>{{ baseName(s.path) }}</b>
|
||||
{{ basePath(s.path, s.type === "directory")
|
||||
}}<b>{{ baseName(s.path) }}</b>
|
||||
</span>
|
||||
<div class="filesize">{{ humanSize(s.size) }}</div>
|
||||
</router-link>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -215,7 +136,7 @@ import ButtonGroup from "./ButtonGroup.vue";
|
||||
import { search } from "@/api";
|
||||
import { getters, mutations, state } from "@/store";
|
||||
import { getHumanReadableFilesize } from "@/utils/filesizes";
|
||||
import { getApiPath } from "@/utils/url";
|
||||
import Icon from "@/components/Icon.vue";
|
||||
|
||||
var boxes = {
|
||||
folder: { label: "folders", icon: "folder" },
|
||||
@@ -230,6 +151,7 @@ var boxes = {
|
||||
export default {
|
||||
components: {
|
||||
ButtonGroup,
|
||||
Icon,
|
||||
},
|
||||
name: "search",
|
||||
data: function () {
|
||||
@@ -251,11 +173,13 @@ export default {
|
||||
{ label: "Documents", value: "type:doc" },
|
||||
{ label: "Archives", value: "type:archive" },
|
||||
],
|
||||
toggleOptionButton: [{ label: "Show Options" }],
|
||||
value: "",
|
||||
ongoing: false,
|
||||
results: [],
|
||||
reload: false,
|
||||
scrollable: null,
|
||||
hiddenOptions: true,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -291,6 +215,9 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showOptions() {
|
||||
return !this.hiddenOptions || !this.isMobile;
|
||||
},
|
||||
isMobile() {
|
||||
return state.isMobile;
|
||||
},
|
||||
@@ -316,7 +243,6 @@ export default {
|
||||
if (this.ongoing) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return this.value === ""
|
||||
? this.$t("search.typeToSearch")
|
||||
: this.$t("search.pressToSearch");
|
||||
@@ -339,8 +265,19 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
createPath(path) {
|
||||
return getApiPath(`files/${path}`)
|
||||
getRelative(path) {
|
||||
return window.location.href + "/" + path;
|
||||
},
|
||||
getIcon(mimetype) {
|
||||
return getMaterialIconForType(mimetype);
|
||||
},
|
||||
enableOptions() {
|
||||
this.hiddenOptions = false;
|
||||
this.toggleOptionButton = [{ label: "Hide Options" }];
|
||||
},
|
||||
disableOptions() {
|
||||
this.hiddenOptions = true;
|
||||
this.toggleOptionButton = [{ label: "Show Options" }];
|
||||
},
|
||||
humanSize(size) {
|
||||
return getHumanReadableFilesize(size);
|
||||
@@ -373,7 +310,6 @@ export default {
|
||||
},
|
||||
close(event) {
|
||||
this.value = "";
|
||||
|
||||
event.stopPropagation();
|
||||
mutations.closeHovers();
|
||||
},
|
||||
@@ -395,6 +331,7 @@ export default {
|
||||
},
|
||||
resetSearchFilters() {
|
||||
this.searchTypes = "";
|
||||
this.hiddenOptions = true;
|
||||
},
|
||||
removeFromTypes(string) {
|
||||
if (string == null || string == "") {
|
||||
@@ -461,7 +398,7 @@ export default {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#result-desktop > #result-list {
|
||||
#results > #result-list {
|
||||
max-height: 80vh;
|
||||
width: 35em;
|
||||
overflow: scroll;
|
||||
@@ -471,7 +408,7 @@ export default {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
#result-desktop {
|
||||
#results {
|
||||
-webkit-animation: SlideDown 0.5s forwards;
|
||||
animation: SlideDown 0.5s forwards;
|
||||
border-radius: 1m;
|
||||
@@ -496,7 +433,7 @@ export default {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#search.active #result-desktop ul li a {
|
||||
#search.active #results ul li a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.3em 0;
|
||||
@@ -504,8 +441,8 @@ export default {
|
||||
}
|
||||
|
||||
#search #result-list.active {
|
||||
width: 65em !important;
|
||||
max-width: 85vw !important;
|
||||
width: 1000px;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@@ -729,8 +666,8 @@ body.rtl #search .boxes h3 {
|
||||
|
||||
.sizeConstraints {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-content: center;
|
||||
margin: 1em;
|
||||
justify-content: center;
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
<template>
|
||||
<div
|
||||
class="image-ex-container"
|
||||
ref="container"
|
||||
@touchstart="touchStart"
|
||||
@touchmove="touchMove"
|
||||
@dblclick="zoomAuto"
|
||||
@mousedown="mousedownStart"
|
||||
@mousemove="mouseMove"
|
||||
@mouseup="mouseUp"
|
||||
@wheel="wheelMove"
|
||||
>
|
||||
<div class="image-ex-container" ref="container" @touchstart="touchStart" @touchmove="touchMove" @dblclick="zoomAuto"
|
||||
@mousedown="mousedownStart" @mousemove="mouseMove" @mouseup="mouseUp" @wheel="wheelMove">
|
||||
<div v-if="!isLoaded">Loading image...</div>
|
||||
|
||||
<img
|
||||
v-if="!isTiff && isLoaded"
|
||||
:src="src"
|
||||
class="image-ex-img"
|
||||
ref="imgex"
|
||||
@load="onLoad"
|
||||
/>
|
||||
<img v-if="!isTiff && isLoaded" :src="src" class="image-ex-img" ref="imgex" @load="onLoad" />
|
||||
<canvas v-else-if="isLoaded" ref="imgex" class="image-ex-img"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
@@ -92,7 +77,7 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
src: function () {
|
||||
if (this.src == undefined || this.$refs.imgex == undefined) {
|
||||
if (!this.src || !this.$refs.imgex) {
|
||||
mutations.setLoading("preview-img", false);
|
||||
return;
|
||||
}
|
||||
@@ -102,15 +87,18 @@ export default {
|
||||
} else {
|
||||
this.$refs.imgex.src = this.src;
|
||||
}
|
||||
|
||||
this.scale = 1;
|
||||
this.setZoom();
|
||||
this.setCenter();
|
||||
mutations.setLoading("preview-img", false);
|
||||
this.showSpinner = false;
|
||||
this.scale = 1; // Reset zoom level
|
||||
this.position.relative = { x: 0, y: 0 }; // Reset position
|
||||
this.showSpinner = true; // Show spinner while loading
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onLoad() {
|
||||
this.imageLoaded = true;
|
||||
this.setCenter(); // Center the image after loading
|
||||
this.showSpinner = false;
|
||||
mutations.setLoading("preview-img", false);
|
||||
},
|
||||
checkIfTiff(src) {
|
||||
const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
|
||||
const suff = src.split(".").pop().toLowerCase();
|
||||
@@ -144,16 +132,18 @@ export default {
|
||||
}
|
||||
}, 100),
|
||||
setCenter() {
|
||||
let container = this.$refs.container;
|
||||
let img = this.$refs.imgex;
|
||||
const container = this.$refs.container;
|
||||
const img = this.$refs.imgex;
|
||||
|
||||
if (!container || !img || !img.clientWidth || !img.clientHeight) {
|
||||
return; // Exit if dimensions are unavailable
|
||||
}
|
||||
|
||||
this.position.center.x = Math.floor((container.clientWidth - img.clientWidth) / 2);
|
||||
this.position.center.y = Math.floor(
|
||||
(container.clientHeight - img.clientHeight) / 2
|
||||
);
|
||||
this.position.center.y = Math.floor((container.clientHeight - img.clientHeight) / 2);
|
||||
|
||||
img.style.left = this.position.center.x + "px";
|
||||
img.style.top = this.position.center.y + "px";
|
||||
img.style.left = `${this.position.center.x}px`;
|
||||
img.style.top = `${this.position.center.y}px`;
|
||||
},
|
||||
mousedownStart(event) {
|
||||
this.lastX = null;
|
||||
@@ -247,11 +237,13 @@ export default {
|
||||
this.$refs.imgex.style.transform = `translate(${this.position.relative.x}px, ${this.position.relative.y}px) scale(${this.scale})`;
|
||||
},
|
||||
wheelMove(event) {
|
||||
event.preventDefault()
|
||||
this.scale += -Math.sign(event.deltaY) * this.zoomStep;
|
||||
this.setZoom();
|
||||
},
|
||||
setZoom() {
|
||||
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})`;
|
||||
},
|
||||
@@ -264,17 +256,23 @@ export default {
|
||||
|
||||
<style>
|
||||
.image-ex-container {
|
||||
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 */
|
||||
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 */
|
||||
max-width: 100%;
|
||||
/* Image max width */
|
||||
max-height: 100%;
|
||||
/* Image max height */
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<component
|
||||
:is="quickNav ? 'a' : 'div'"
|
||||
:href="quickNav ? getUrl() : undefined"
|
||||
<a
|
||||
:href="getUrl()"
|
||||
:class="{
|
||||
item: true,
|
||||
activebutton: isMaximized && isSelected,
|
||||
}"
|
||||
:id="getID"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:draggable="isDraggable"
|
||||
@@ -16,21 +16,26 @@
|
||||
:data-type="type"
|
||||
:aria-label="name"
|
||||
:aria-selected="isSelected"
|
||||
@contextmenu="onRightClick"
|
||||
@click="quickNav ? toggleClick() : itemClick($event)"
|
||||
@contextmenu="onRightClick($event)"
|
||||
@click="click($event)"
|
||||
@touchstart="addSelected($event)"
|
||||
@touchmove="handleTouchMove($event)"
|
||||
@touchend="cancelContext($event)"
|
||||
@mouseup="cancelContext($event)"
|
||||
>
|
||||
<div @click="toggleClick" :class="{ activetitle: isMaximized && isSelected }">
|
||||
<img
|
||||
v-if="readOnly === undefined && type === 'image' && isThumbsEnabled && isInView"
|
||||
v-if="
|
||||
readOnly === undefined &&
|
||||
type.startsWith('image') &&
|
||||
isThumbsEnabled &&
|
||||
isInView
|
||||
"
|
||||
v-lazy="thumbnailUrl"
|
||||
:class="{ activeimg: isMaximized && isSelected }"
|
||||
ref="thumbnail"
|
||||
/>
|
||||
<i
|
||||
:class="{ iconActive: isMaximized && isSelected }"
|
||||
v-else
|
||||
class="material-icons"
|
||||
></i>
|
||||
<Icon v-else :mimetype="type" />
|
||||
</div>
|
||||
|
||||
<div class="text" :class="{ activecontent: isMaximized && isSelected }">
|
||||
@@ -40,7 +45,7 @@
|
||||
<time :datetime="modified">{{ humanTime() }}</time>
|
||||
</p>
|
||||
</div>
|
||||
</component>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@@ -77,14 +82,23 @@ import * as upload from "@/utils/upload";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
import { baseURL } from "@/utils/constants";
|
||||
import { router } from "@/router";
|
||||
import { url } from "@/utils";
|
||||
import Icon from "@/components/Icon.vue";
|
||||
|
||||
export default {
|
||||
name: "item",
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isThumbnailInView: false,
|
||||
isMaximized: false,
|
||||
touches: 0,
|
||||
touchStartX: 0,
|
||||
touchStartY: 0,
|
||||
isLongPress: false,
|
||||
isSwipe: false,
|
||||
};
|
||||
},
|
||||
props: [
|
||||
@@ -99,6 +113,9 @@ export default {
|
||||
"path",
|
||||
],
|
||||
computed: {
|
||||
getID() {
|
||||
return url.base64Encode(encodeURIComponent(this.name));
|
||||
},
|
||||
quickNav() {
|
||||
return state.user.singleClick && !state.multiple;
|
||||
},
|
||||
@@ -146,6 +163,7 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// Prevent default navigation for left-clicks
|
||||
const observer = new IntersectionObserver(this.handleIntersect, {
|
||||
root: null,
|
||||
rootMargin: "0px",
|
||||
@@ -159,14 +177,45 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTouchMove(event) {
|
||||
if (!state.isSafari) return
|
||||
const touch = event.touches[0];
|
||||
const deltaX = Math.abs(touch.clientX - this.touchStartX);
|
||||
const deltaY = Math.abs(touch.clientY - this.touchStartY);
|
||||
// Set a threshold for movement to detect a swipe
|
||||
const movementThreshold = 10; // Adjust as needed
|
||||
if (deltaX > movementThreshold || deltaY > movementThreshold) {
|
||||
this.isSwipe = true;
|
||||
this.cancelContext(); // Cancel long press if swipe is detected
|
||||
}
|
||||
},
|
||||
handleTouchEnd() {
|
||||
if (!state.isSafari) return
|
||||
this.cancelContext(); // Clear timeout
|
||||
this.isSwipe = false; // Reset swipe state
|
||||
},
|
||||
cancelContext() {
|
||||
if (this.contextTimeout) {
|
||||
clearTimeout(this.contextTimeout);
|
||||
this.contextTimeout = null;
|
||||
}
|
||||
this.isLongPress = false;
|
||||
},
|
||||
updateHashAndNavigate(path) {
|
||||
// Update hash in the browser without full page reload
|
||||
window.location.hash = path;
|
||||
|
||||
// Optional: Trigger native navigation
|
||||
window.location.href = this.getRelative(path);
|
||||
},
|
||||
getUrl() {
|
||||
return baseURL.slice(0, -1) + this.url;
|
||||
},
|
||||
onRightClick(event) {
|
||||
event.preventDefault(); // Prevent default context menu
|
||||
|
||||
// If no items are selected, select the right-clicked item
|
||||
if (getters.selectedCount() === 0) {
|
||||
if (!state.multiple) {
|
||||
mutations.resetSelected();
|
||||
mutations.addSelected(this.index);
|
||||
}
|
||||
mutations.showHover({
|
||||
@@ -278,17 +327,37 @@ export default {
|
||||
|
||||
action(overwrite, rename);
|
||||
},
|
||||
itemClick(event) {
|
||||
if (this.singleClick && !state.multiple) this.open();
|
||||
else this.click(event);
|
||||
addSelected(event) {
|
||||
if (!state.isSafari) return
|
||||
const touch = event.touches[0];
|
||||
this.touchStartX = touch.clientX;
|
||||
this.touchStartY = touch.clientY;
|
||||
this.isLongPress = false; // Reset state
|
||||
this.isSwipe = false; // Reset swipe detection
|
||||
if (!state.multiple) {
|
||||
this.contextTimeout = setTimeout(() => {
|
||||
if (!this.isSwipe) {
|
||||
mutations.resetSelected();
|
||||
mutations.addSelected(this.index);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
click(event) {
|
||||
if (!this.singleClick && getters.selectedCount() !== 0) event.preventDefault();
|
||||
if (event.button === 0) {
|
||||
// Left-click
|
||||
event.preventDefault();
|
||||
if (this.quickNav) {
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.user.singleClick && getters.selectedCount() !== 0 && event.button === 0) {
|
||||
event.preventDefault();
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.touches = 0;
|
||||
}, 500);
|
||||
|
||||
this.touches++;
|
||||
if (this.touches > 1) {
|
||||
this.open();
|
||||
@@ -319,13 +388,23 @@ export default {
|
||||
|
||||
return;
|
||||
}
|
||||
if (!this.singleClick && !event.ctrlKey && !event.metaKey && !state.multiple)
|
||||
if (!state.user.singleClick && !event.ctrlKey && !event.metaKey && !state.multiple) {
|
||||
mutations.resetSelected();
|
||||
}
|
||||
mutations.addSelected(this.index);
|
||||
},
|
||||
open() {
|
||||
router.push({ path: this.url });
|
||||
location.hash = state.req.items[this.index].name;
|
||||
const newurl = url.removePrefix(this.url);
|
||||
router.push({ path: newurl });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.item {
|
||||
-webkit-touch-callout: none; /* Disable the default long press preview */
|
||||
user-select: none; /* Optional: Disable text selection for better UX */
|
||||
}
|
||||
</style>
|
||||
@@ -28,12 +28,7 @@ export default {
|
||||
return {
|
||||
formats: {
|
||||
zip: "zip",
|
||||
tar: "tar",
|
||||
targz: "tar.gz",
|
||||
tarbz2: "tar.bz2",
|
||||
tarxz: "tar.xz",
|
||||
tarlz4: "tar.lz4",
|
||||
tarsz: "tar.sz",
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
<strong>{{ $t("prompts.size") }}:</strong>
|
||||
<span id="content_length"></span> {{ humanSize }}
|
||||
</p>
|
||||
<p v-if="!dir || selected.length > 1">
|
||||
<strong>Type:</strong>
|
||||
<span id="content_length"></span> {{ type }}
|
||||
</p>
|
||||
<p v-if="selected.length < 2" :title="modTime">
|
||||
<strong>{{ $t("prompts.lastModified") }}:</strong> {{ humanTime }}
|
||||
</p>
|
||||
@@ -124,6 +128,11 @@ export default {
|
||||
? state.req.name
|
||||
: state.req.items[this.selected[0]].name;
|
||||
},
|
||||
type() {
|
||||
return getters.selectedCount() === 0
|
||||
? state.req.type
|
||||
: state.req.items[this.selected[0]].type;
|
||||
},
|
||||
dir() {
|
||||
return (
|
||||
getters.selectedCount() > 1 ||
|
||||
|
||||
@@ -112,7 +112,7 @@ export default {
|
||||
}
|
||||
action(overwrite, rename);
|
||||
} catch (e) {
|
||||
notify.error(e);
|
||||
notify.showError(e);
|
||||
}
|
||||
return;
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("buttons.share") }}</h2>
|
||||
</div>
|
||||
<div class="searchContext">Path: {{ getContext }}</div>
|
||||
<div class="searchContext">Path: {{ subpath }}</div>
|
||||
|
||||
<template v-if="listing">
|
||||
<div class="card-content">
|
||||
@@ -167,26 +167,14 @@ export default {
|
||||
}
|
||||
return state.req.items[this.selected[0]].url;
|
||||
},
|
||||
getContext() {
|
||||
const prefix = `/files/`;
|
||||
let path = state.route.path.replace(prefix, "./");
|
||||
if (getters.selectedCount() === 1) {
|
||||
path = path + state.req.items[this.selected[0]].name;
|
||||
}
|
||||
return decodeURIComponent(path);
|
||||
},
|
||||
},
|
||||
async beforeMount() {
|
||||
try {
|
||||
const prefix = `/files`;
|
||||
let path = state.route.path.startsWith(prefix)
|
||||
? state.route.path.slice(prefix.length)
|
||||
: state.route.path;
|
||||
path = decodeURIComponent(path);
|
||||
if (path == "") {
|
||||
path = "/";
|
||||
let path = "." + getters.routePath("files");
|
||||
if (getters.selectedCount() === 1) {
|
||||
path = path + state.req.items[this.selected[0]].name;
|
||||
}
|
||||
this.subpath = path;
|
||||
this.subpath = decodeURIComponent(path);
|
||||
// get last element of the path
|
||||
const links = await shareApi.get(this.subpath);
|
||||
this.links = links;
|
||||
|
||||
@@ -63,6 +63,7 @@ export default {
|
||||
overflow: auto;
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -78,9 +79,6 @@ export default {
|
||||
padding-bottom: 4em;
|
||||
background-color: rgb(255 255 255 / 50%) !important;
|
||||
}
|
||||
#sidebar.dark-mode {
|
||||
background-color: rgb(37 49 55 / 33%) !important;
|
||||
}
|
||||
|
||||
#sidebar.sticky {
|
||||
z-index: 3;
|
||||
|
||||
@@ -78,7 +78,7 @@ over
|
||||
/* Main Content */
|
||||
main {
|
||||
position: fixed;
|
||||
padding: 1em;
|
||||
padding: .5em;
|
||||
padding-top: 4em;
|
||||
overflow: scroll;
|
||||
top: 0;
|
||||
@@ -203,17 +203,16 @@ input.sizeInput:disabled {
|
||||
|
||||
.sizeInputWrapper {
|
||||
border-radius: 1em;
|
||||
margin-left: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
margin: .5em;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
background-color: rgb(245, 245, 245);
|
||||
padding: 0.25em;
|
||||
height: 3em;
|
||||
background-color: #f5f5f5;
|
||||
padding: .25em;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
border: 1px solid #ccc;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sizeInput {
|
||||
|
||||
@@ -264,11 +264,35 @@
|
||||
}
|
||||
|
||||
/* Result desktop */
|
||||
.dark-mode #result-desktop #result-list {
|
||||
.dark-mode #results #result-list {
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
/* Result desktop background */
|
||||
.dark-mode #result-desktop {
|
||||
.dark-mode #results {
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
/* Use the class .dark-mode to apply styles conditionally */
|
||||
.dark-mode {
|
||||
background: #141D24 !important;
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.dark-mode-header {
|
||||
color: white;
|
||||
background: #141D24;
|
||||
}
|
||||
|
||||
/* Header with backdrop-filter support */
|
||||
@supports (backdrop-filter: none) {
|
||||
.dark-mode-header {
|
||||
background-color: rgb(37 49 55 / 33%) !important;
|
||||
backdrop-filter: blur(16px) invert(0.1);
|
||||
}
|
||||
}
|
||||
|
||||
#sidebar.dark-mode {
|
||||
background-color: #141D24 !important;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
@import 'material-icons/iconfont/filled.css';
|
||||
@import 'material-icons/iconfont/outlined.css';
|
||||
@import 'material-symbols/index.css';
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
@@ -168,6 +170,3 @@
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
@@ -9,7 +9,6 @@ header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: rgb(255 255 255 / 50%) !important;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
@@ -49,25 +48,3 @@ header img {
|
||||
header .action span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Icon Colors */
|
||||
.folder-icons {
|
||||
color: var(--icon-blue);
|
||||
}
|
||||
|
||||
.video-icons {
|
||||
color: lightskyblue;
|
||||
}
|
||||
|
||||
.image-icons {
|
||||
color: lightcoral;
|
||||
}
|
||||
|
||||
.archive-icons {
|
||||
color: tan;
|
||||
}
|
||||
|
||||
.audio-icons {
|
||||
color: plum;
|
||||
}
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
/* Icons */
|
||||
|
||||
/* General */
|
||||
|
||||
.file-icons [aria-label^="."] { opacity: 0.33 }
|
||||
.file-icons [aria-label$=".bak"] { opacity: 0.33 }
|
||||
|
||||
.file-icons [data-type=audio] i::before { content: 'volume_up' }
|
||||
.file-icons [data-type=blob] i::before { content: 'insert_drive_file' }
|
||||
.file-icons [data-type=image] i::before { content: 'image' }
|
||||
.file-icons [data-type=pdf] i::before { content: 'description' }
|
||||
.file-icons [data-type=text] i::before { content: 'description' }
|
||||
.file-icons [data-type=video] i::before { content: 'movie' }
|
||||
.file-icons [data-type=invalid_link] i::before { content: 'link_off' }
|
||||
|
||||
/* #f90 - Image */
|
||||
|
||||
.file-icons [aria-label$=".ai"] i::before,
|
||||
.file-icons [aria-label$=".odg"] i::before,
|
||||
.file-icons [aria-label$=".xcf"] i::before
|
||||
{ content: 'image' }
|
||||
|
||||
/* #f90 - Presentation */
|
||||
|
||||
.file-icons [aria-label$=".odp"] i::before,
|
||||
.file-icons [aria-label$=".ppt"] i::before,
|
||||
.file-icons [aria-label$=".pptx"] i::before
|
||||
{ content: 'slideshow' }
|
||||
|
||||
/* #0f0 - Spreadsheet/Database */
|
||||
|
||||
.file-icons [aria-label$=".csv"] i::before,
|
||||
.file-icons [aria-label$=".db"] i::before,
|
||||
.file-icons [aria-label$=".odb"] i::before,
|
||||
.file-icons [aria-label$=".ods"] i::before,
|
||||
.file-icons [aria-label$=".xls"] i::before,
|
||||
.file-icons [aria-label$=".xlsx"] i::before
|
||||
{ content: 'border_all' }
|
||||
|
||||
/* #00f - Document */
|
||||
|
||||
.file-icons [aria-label$=".doc"] i::before,
|
||||
.file-icons [aria-label$=".docx"] i::before,
|
||||
.file-icons [aria-label$=".log"] i::before,
|
||||
.file-icons [aria-label$=".odt"] i::before,
|
||||
.file-icons [aria-label$=".rtf"] i::before
|
||||
{ content: 'description' }
|
||||
|
||||
/* #999 - Code */
|
||||
|
||||
.file-icons [aria-label$=".c"] i::before,
|
||||
.file-icons [aria-label$=".cpp"] i::before,
|
||||
.file-icons [aria-label$=".cs"] i::before,
|
||||
.file-icons [aria-label$=".css"] i::before,
|
||||
.file-icons [aria-label$=".go"] i::before,
|
||||
.file-icons [aria-label$=".h"] i::before,
|
||||
.file-icons [aria-label$=".html"] i::before,
|
||||
.file-icons [aria-label$=".java"] i::before,
|
||||
.file-icons [aria-label$=".js"] i::before,
|
||||
.file-icons [aria-label$=".json"] i::before,
|
||||
.file-icons [aria-label$=".kt"] i::before,
|
||||
.file-icons [aria-label$=".php"] i::before,
|
||||
.file-icons [aria-label$=".py"] i::before,
|
||||
.file-icons [aria-label$=".rb"] i::before,
|
||||
.file-icons [aria-label$=".rs"] i::before,
|
||||
.file-icons [aria-label$=".vue"] i::before,
|
||||
.file-icons [aria-label$=".xml"] i::before,
|
||||
.file-icons [aria-label$=".yml"] i::before
|
||||
{ content: 'code' }
|
||||
|
||||
/* #999 - Executable */
|
||||
|
||||
.file-icons [aria-label$=".apk"] i::before,
|
||||
.file-icons [aria-label$=".bat"] i::before,
|
||||
.file-icons [aria-label$=".exe"] i::before,
|
||||
.file-icons [aria-label$=".jar"] i::before,
|
||||
.file-icons [aria-label$=".ps1"] i::before,
|
||||
.file-icons [aria-label$=".sh"] i::before
|
||||
{ content: 'web_asset' }
|
||||
|
||||
/* #999 - Installer */
|
||||
|
||||
.file-icons [aria-label$=".deb"] i::before,
|
||||
.file-icons [aria-label$=".msi"] i::before,
|
||||
.file-icons [aria-label$=".pkg"] i::before,
|
||||
.file-icons [aria-label$=".rpm"] i::before
|
||||
{ content: 'archive' }
|
||||
|
||||
/* #999 - Compressed */
|
||||
|
||||
.file-icons [aria-label$=".7z"] i::before,
|
||||
.file-icons [aria-label$=".bz2"] i::before,
|
||||
.file-icons [aria-label$=".cab"] i::before,
|
||||
.file-icons [aria-label$=".gz"] i::before,
|
||||
.file-icons [aria-label$=".rar"] i::before,
|
||||
.file-icons [aria-label$=".tar"] i::before,
|
||||
.file-icons [aria-label$=".xz"] i::before,
|
||||
.file-icons [aria-label$=".zip"] i::before,
|
||||
.file-icons [aria-label$=".zst"] i::before
|
||||
{ content: 'folder_zip' }
|
||||
|
||||
/* #999 - Disk */
|
||||
|
||||
.file-icons [aria-label$=".ccd"] i::before,
|
||||
.file-icons [aria-label$=".dmg"] i::before,
|
||||
.file-icons [aria-label$=".iso"] i::before,
|
||||
.file-icons [aria-label$=".mdf"] i::before,
|
||||
.file-icons [aria-label$=".vdi"] i::before,
|
||||
.file-icons [aria-label$=".vhd"] i::before,
|
||||
.file-icons [aria-label$=".vmdk"] i::before,
|
||||
.file-icons [aria-label$=".wim"] i::before
|
||||
{ content: 'album' }
|
||||
|
||||
/* #999 - Font */
|
||||
|
||||
.file-icons [aria-label$=".otf"] i::before,
|
||||
.file-icons [aria-label$=".ttf"] i::before,
|
||||
.file-icons [aria-label$=".woff"] i::before,
|
||||
.file-icons [aria-label$=".woff2"] i::before
|
||||
{ content: 'font_download' }
|
||||
|
||||
/* Colors */
|
||||
|
||||
/* General */
|
||||
|
||||
.file-icons [data-type=audio] i { color: var(--icon-yellow) }
|
||||
.file-icons [data-type=image] i { color: var(--icon-orange) }
|
||||
.file-icons [data-type=video] i { color: var(--icon-violet) }
|
||||
.file-icons [data-type=invalid_link] i { color: var(--icon-red) }
|
||||
|
||||
/* #f00 - Adobe/Oracle */
|
||||
|
||||
.file-icons [aria-label$=".ai"] i,
|
||||
.file-icons [aria-label$=".java"] i,
|
||||
.file-icons [aria-label$=".jar"] i,
|
||||
.file-icons [aria-label$=".psd"] i,
|
||||
.file-icons [aria-label$=".rb"] i,
|
||||
.file-icons [data-type=pdf] i
|
||||
{ color: var(--icon-red) }
|
||||
|
||||
/* #f90 - Image/Presentation */
|
||||
|
||||
.file-icons [aria-label$=".html"] i,
|
||||
.file-icons [aria-label$=".odg"] i,
|
||||
.file-icons [aria-label$=".odp"] i,
|
||||
.file-icons [aria-label$=".ppt"] i,
|
||||
.file-icons [aria-label$=".pptx"] i,
|
||||
.file-icons [aria-label$=".vue"] i,
|
||||
.file-icons [aria-label$=".xcf"] i
|
||||
{ color: var(--icon-orange) }
|
||||
|
||||
/* #ff0 - Various */
|
||||
|
||||
.file-icons [aria-label$=".css"] i,
|
||||
.file-icons [aria-label$=".js"] i,
|
||||
.file-icons [aria-label$=".json"] i,
|
||||
.file-icons [aria-label$=".zip"] i
|
||||
{ color: var(--icon-yellow) }
|
||||
|
||||
/* #0f0 - Spreadsheet/Google */
|
||||
|
||||
.file-icons [aria-label$=".apk"] i,
|
||||
.file-icons [aria-label$=".dex"] i,
|
||||
.file-icons [aria-label$=".go"] i,
|
||||
.file-icons [aria-label$=".ods"] i,
|
||||
.file-icons [aria-label$=".xls"] i,
|
||||
.file-icons [aria-label$=".xlsx"] i
|
||||
{ color: var(--icon-green) }
|
||||
|
||||
/* #00f - Document/Microsoft/Apple/Closed */
|
||||
|
||||
.file-icons [aria-label$=".aac"] i,
|
||||
.file-icons [aria-label$=".bat"] i,
|
||||
.file-icons [aria-label$=".cab"] i,
|
||||
.file-icons [aria-label$=".cs"] i,
|
||||
.file-icons [aria-label$=".dmg"] i,
|
||||
.file-icons [aria-label$=".doc"] i,
|
||||
.file-icons [aria-label$=".docx"] i,
|
||||
.file-icons [aria-label$=".emf"] i,
|
||||
.file-icons [aria-label$=".exe"] i,
|
||||
.file-icons [aria-label$=".ico"] i,
|
||||
.file-icons [aria-label$=".mp2"] i,
|
||||
.file-icons [aria-label$=".mp3"] i,
|
||||
.file-icons [aria-label$=".mp4"] i,
|
||||
.file-icons [aria-label$=".mpg"] i,
|
||||
.file-icons [aria-label$=".msi"] i,
|
||||
.file-icons [aria-label$=".odt"] i,
|
||||
.file-icons [aria-label$=".ps1"] i,
|
||||
.file-icons [aria-label$=".rtf"] i,
|
||||
.file-icons [aria-label$=".vob"] i,
|
||||
.file-icons [aria-label$=".wim"] i
|
||||
{ color: var(--icon-blue) }
|
||||
|
||||
/* #60f - Various */
|
||||
|
||||
.file-icons [aria-label$=".iso"] i,
|
||||
.file-icons [aria-label$=".php"] i,
|
||||
.file-icons [aria-label$=".rar"] i
|
||||
{ color: var(--icon-violet) }
|
||||
|
||||
/* Overrides */
|
||||
|
||||
.file-icons [data-dir=true] i { color: var(--icon-blue) }
|
||||
.file-icons [data-dir=true] i::before { content: 'folder' }
|
||||
.file-icons [aria-selected=true] i { color: var(--item-selected) }
|
||||
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user