v0.3.3 release (#257)

This commit is contained in:
Graham Steffaniak 2024-12-16 19:01:55 -05:00 committed by GitHub
parent 266a76459d
commit d2a3e50d37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
103 changed files with 1020 additions and 705 deletions

View File

@ -61,7 +61,7 @@ jobs:
JSON="${{ steps.meta.outputs.tags }}" JSON="${{ steps.meta.outputs.tags }}"
# Use jq to remove 'v' from the version field # Use jq to remove 'v' from the version field
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/') JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT echo "CLEANED_TAG=$JSON" >> $GITHUB_OUTPUT
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
@ -72,5 +72,5 @@ jobs:
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
file: ./Dockerfile file: ./Dockerfile
push: true push: true
tags: ${{ steps.modify-json.outputs.cleaned_tag }} tags: ${{ steps.modify-json.outputs.CLEANED_TAG }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View File

@ -10,7 +10,7 @@ permissions:
jobs: jobs:
push_release_to_registry: push_release_to_registry:
name: Push dev release name: Push release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@ -35,16 +35,16 @@ jobs:
JSON="${{ steps.meta.outputs.tags }}" JSON="${{ steps.meta.outputs.tags }}"
# Use jq to remove 'v' from the version field # Use jq to remove 'v' from the version field
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/') JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
echo "CLEANED_TAG=$JSON" >> $GITHUB_ENV echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: .
build-args: | build-args: |
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
context: .
platforms: linux/amd64 platforms: linux/amd64
file: ./Dockerfile file: ./Dockerfile
push: true push: true
tags: ${{ env.CLEANED_TAG }} tags: ${{ steps.modify-json.outputs.cleaned_tag }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View File

@ -2,6 +2,25 @@
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). 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.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 ## v0.3.2
**New Features** **New Features**
@ -186,7 +205,7 @@ This change focuses on minimizing and simplifying build process.
- The shell feature has been deprecated. - The shell feature has been deprecated.
- Custom commands can be executed within the Docker container if needed. - Custom commands can be executed within the Docker container if needed.
- The JSON config file is no longer used. - 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. - The only allowed flag is specifying the config file.
- Removed old code for migrating database versions. - Removed old code for migrating database versions.
- Eliminated all unused `cmd` code. - Eliminated all unused `cmd` code.

View File

@ -20,8 +20,12 @@ RUN npm run build-docker
FROM alpine:latest FROM alpine:latest
ENV FILEBROWSER_NO_EMBEDED="true" ENV FILEBROWSER_NO_EMBEDED="true"
RUN apk --no-cache add ca-certificates mailcap 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. # exposing default port for auto discovery.
EXPOSE 80 EXPOSE 80
COPY --from=nbuild /app/dist/ ./http/dist/
ENTRYPOINT [ "./filebrowser" ] ENTRYPOINT [ "./filebrowser" ]

View File

@ -8,7 +8,7 @@ WORKDIR /app
COPY ./frontend/package.json ./ COPY ./frontend/package.json ./
RUN npm i --maxsockets 1 RUN npm i --maxsockets 1
RUN npx playwright install --with-deps firefox RUN npx playwright install --with-deps firefox
COPY [ "backend/filebrowser.yaml", "./" ] COPY [ "backend/config.yaml", "./" ]
COPY ./frontend/ ./frontend COPY ./frontend/ ./frontend
WORKDIR /app/frontend WORKDIR /app/frontend
RUN npm run build-docker RUN npm run build-docker

View File

@ -10,7 +10,7 @@
</p> </p>
> [!Note] > [!Note]
> 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. > 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.
> [!WARNING] > [!WARNING]
> - There is no stable version yet. Always check release notes for bug fixes on functionality that may have been changed. If you notice any unexpected behavior -- please open an issue to have it fixed soon. > - There is no stable version 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.
@ -22,18 +22,18 @@ FileBrowser Quantum is a fork of the file browser opensource project with the fo
- Real-time search results as you type - Real-time search results as you type
- Search supports file/folder sizes and many file type filters. - Search supports file/folder sizes and many file type filters.
- Enhanced interactive results that show file/folder sizes. - Enhanced interactive results that show file/folder sizes.
1. [x] Revamped and simplified GUI navbar and sidebar menu. 2. [x] Revamped and simplified GUI navbar and sidebar menu.
- Additional compact view mode as well as refreshed view mode - Additional compact view mode as well as refreshed view mode
styles. styles.
- Many graphical and user experience improvements. - Many graphical and user experience improvements.
- right-click context menu - right-click context menu
1. [x] Revamped and simplified configuration via `filebrowser.yml` config file. 3. [x] Revamped and simplified configuration via `filebrowser.yml` config file.
1. [x] Better listing browsing 4. [x] Better listing browsing
- Switching view modes is instant - Switching view modes is instant
- Folder sizes are shown as well - Folder sizes are shown as well
- Changing Sort order is instant - Changing Sort order is instant
- The entire directory is loaded in 1/3 the time - The entire directory is loaded in 1/3 the time
1. [x] Developer API support 5. [x] Developer API support
- Can create long-live API Tokens. - Can create long-live API Tokens.
- Helpful Swagger page available at `/swagger` endpoint. - Helpful Swagger page available at `/swagger` endpoint.
@ -94,7 +94,13 @@ Using docker:
1. docker run (no persistent db): 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: 1. docker compose:
@ -106,11 +112,14 @@ services:
filebrowser: filebrowser:
volumes: volumes:
- '/path/to/folder:/srv' # required (for now not configurable) - '/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. # optional if you want db to persist - configure a path under "database" dir in config file.
- './filebrowser.yaml:/filebrowser.yaml' # required - './database:/home/filebrowser/database'
- './config.yaml:/home/filebrowser/config.yaml'
ports: ports:
- '80:80' - '80:80'
image: gtstef/filebrowser image: gtstef/filebrowser
# optionally run as non-root filebrowser user
#user: filebrowser
restart: always restart: always
``` ```
@ -121,8 +130,9 @@ services:
filebrowser: filebrowser:
volumes: volumes:
- 'storage:/srv' # required (for now not configurable) - '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. # optional if you want db to persist - configure a path under "database" dir in config file.
- './filebrowser.yaml:/filebrowser.yaml' # required - './database:/home/filebrowser/database'
- './config.yaml:/home/filebrowser/config.yaml'
ports: ports:
- '80:80' - '80:80'
image: gtstef/filebrowser image: gtstef/filebrowser
@ -139,14 +149,14 @@ volumes:
Not using docker (not recommended), download your binary from releases and run with your custom config file: 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 ## Command Line Usage
There are very few commands available. There are 3 actions done via the command line: There are very few commands available. There are 3 actions done via the command line:
1. Running the program, as shown in the install step. The only argument used is the config file if you choose to override the default "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` 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"]` 3. Updating the DB, which currently only supports adding users via `./filebrowser set -u username,password [-a] [-s "example/scope"]`
@ -172,8 +182,8 @@ Failed Request
## Configuration ## Configuration
All configuration is now done via a single configuration file: All configuration is now done via a single configuration file:
`filebrowser.yaml`, here is an example of minimal [configuration `config.yaml`, here is an example of minimal [configuration
file](./backend/filebrowser.yaml). file](./backend/config.yaml).
View the [Configuration Help Page](./docs/configuration.md) for available View the [Configuration Help Page](./docs/configuration.md) for available
configuration options and other help. configuration options and other help.

View File

@ -3,7 +3,7 @@ package auth
import ( import (
"net/http" "net/http"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
// Auther is the authentication interface. // Auther is the authentication interface.

View File

@ -9,9 +9,9 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"github.com/gtsteffaniak/filebrowser/errors" "github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
type hookCred struct { type hookCred struct {

View File

@ -7,8 +7,8 @@ import (
"os" "os"
"strings" "strings"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
type jsonCred struct { type jsonCred struct {

View File

@ -3,8 +3,8 @@ package auth
import ( import (
"net/http" "net/http"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
// MethodNoAuth is used to identify no auth. // MethodNoAuth is used to identify no auth.

View File

@ -4,10 +4,10 @@ import (
"net/http" "net/http"
"os" "os"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/errors" "github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
// MethodProxyAuth is used to identify no auth. // MethodProxyAuth is used to identify no auth.

View File

@ -1,7 +1,7 @@
package auth package auth
import ( import (
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
// StorageBackend is a storage backend for auth storage. // StorageBackend is a storage backend for auth storage.

View File

@ -7,17 +7,17 @@ import (
"os" "os"
"strings" "strings"
"github.com/gtsteffaniak/filebrowser/diskcache" "github.com/gtsteffaniak/filebrowser/backend/diskcache"
"github.com/gtsteffaniak/filebrowser/files" "github.com/gtsteffaniak/filebrowser/backend/files"
fbhttp "github.com/gtsteffaniak/filebrowser/http" fbhttp "github.com/gtsteffaniak/filebrowser/backend/http"
"github.com/gtsteffaniak/filebrowser/img" "github.com/gtsteffaniak/filebrowser/backend/img"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/storage" "github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/swagger/docs" "github.com/gtsteffaniak/filebrowser/backend/swagger/docs"
"github.com/swaggo/swag" "github.com/swaggo/swag"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/version" "github.com/gtsteffaniak/filebrowser/backend/version"
) )
func getStore(config string) (*storage.Storage, bool) { func getStore(config string) (*storage.Storage, bool) {
@ -47,7 +47,7 @@ func StartFilebrowser() {
var help bool var help bool
// Override the default usage output to use generalUsage() // Override the default usage output to use generalUsage()
flag.Usage = 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") flag.BoolVar(&help, "h", false, "Get help about commands")
// Parse global flags (before subcommands) // 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.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.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(&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 // Parse subcommand flags only if a subcommand is specified
if len(os.Args) > 1 { if len(os.Args) > 1 {

View File

@ -5,10 +5,10 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/storage" "github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
) )
func init() { func init() {

View File

@ -6,10 +6,10 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/storage" "github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
) )
func init() { func init() {

View File

@ -5,10 +5,10 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/storage" "github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
) )
func init() { func init() {

View File

@ -1,7 +1,7 @@
package cmd package cmd
import ( import (
"github.com/gtsteffaniak/filebrowser/storage" "github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )

View File

@ -8,7 +8,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
var usersCmd = &cobra.Command{ var usersCmd = &cobra.Command{

View File

@ -3,9 +3,9 @@ package cmd
import ( import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/storage" "github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
) )
func init() { func init() {

View File

@ -1,8 +1,8 @@
package cmd package cmd
import ( import (
"github.com/gtsteffaniak/filebrowser/storage" "github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )

View File

@ -3,9 +3,9 @@ package cmd
import ( import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/storage" "github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
) )
func init() { func init() {

View File

@ -8,9 +8,9 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/storage" "github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
) )
func init() { func init() {

View File

@ -3,8 +3,8 @@ package cmd
import ( import (
"log" "log"
"github.com/gtsteffaniak/filebrowser/storage" "github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )

View File

@ -3,9 +3,9 @@ package cmd
import ( import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/gtsteffaniak/filebrowser/storage" "github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
) )
func init() { func init() {

View File

@ -10,8 +10,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/gtsteffaniak/filebrowser/storage" "github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
) )
func mustGetString(flags *pflag.FlagSet, flag string) string { func mustGetString(flags *pflag.FlagSet, flag string) string {

View File

@ -1,7 +1,7 @@
server: server:
port: 80 port: 80
baseURL: "/" baseURL: "/"
root: "./srv" root: "/srv"
auth: auth:
method: password method: password
signup: false signup: false

Binary file not shown.

View File

@ -24,11 +24,24 @@ var documentTypes = []string{
".pdf", // Portable Document Format ".pdf", // Portable Document Format
".odt", // OpenDocument Text ".odt", // OpenDocument Text
".rtf", // Rich Text Format ".rtf", // Rich Text Format
".conf",
".bash_history",
".gitignore",
".htpasswd",
".profile",
".dockerignore",
".editorconfig",
// Presentation Formats // Presentation Formats
".ppt", ".pptx", // Microsoft PowerPoint ".ppt", ".pptx", // Microsoft PowerPoint
".odp", // OpenDocument Presentation ".odp", // OpenDocument Presentation
// google docs
".gdoc",
// google sheet
".gsheet",
// Spreadsheet Formats // Spreadsheet Formats
".xls", ".xlsx", // Microsoft Excel ".xls", ".xlsx", // Microsoft Excel
".ods", // OpenDocument Spreadsheet ".ods", // OpenDocument Spreadsheet
@ -102,6 +115,16 @@ type SearchOptions struct {
Terms []string 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 { func ParseSearch(value string) SearchOptions {
opts := SearchOptions{ opts := SearchOptions{
Conditions: map[string]bool{ Conditions: map[string]bool{
@ -199,8 +222,6 @@ func IsMatchingType(extension string, matchType string) bool {
switch matchType { switch matchType {
case "doc": case "doc":
return isDoc(extension) return isDoc(extension)
case "pdf":
return extension == ".pdf"
case "text": case "text":
return isText(extension) return isText(extension)
case "archive": case "archive":

View File

@ -22,7 +22,7 @@ func TestIsMatchingType(t *testing.T) {
extension string extension string
expectedType string expectedType string
}{ }{
{".pdf", "pdf"}, {".pdf", "doc"},
{".doc", "doc"}, {".doc", "doc"},
{".docx", "doc"}, {".docx", "doc"},
{".json", "text"}, {".json", "text"},

View File

@ -11,6 +11,7 @@ import (
"io" "io"
"mime" "mime"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
@ -20,11 +21,11 @@ import (
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/gtsteffaniak/filebrowser/errors" "github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/fileutils" "github.com/gtsteffaniak/filebrowser/backend/fileutils"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
) )
var ( var (
@ -201,7 +202,8 @@ func DeleteFiles(absPath string, opts FileOptions) error {
return err return err
} }
index := GetIndex(rootPath) index := GetIndex(rootPath)
err = index.RefreshFileInfo(opts) refreshConfig := FileOptions{Path: filepath.Dir(opts.Path), IsDir: true}
err = index.RefreshFileInfo(refreshConfig)
if err != nil { if err != nil {
return err return err
} }
@ -341,74 +343,29 @@ func getContent(path string) (string, error) {
return stringContent, nil return stringContent, nil
} }
// detectType detects the file type. // DetectType detects the MIME type of a file and updates the ItemInfo struct.
func (i *ItemInfo) detectType(path string, modify, saveContent, readHeader bool) error { func (i *ItemInfo) DetectType(path string, saveContent bool) {
name := i.Name name := i.Name
var contentErr error
ext := filepath.Ext(name) 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 { // Attempt MIME detection by file extension
if IsMatchingType(ext, fileType) { i.Type = strings.Split(mime.TypeByExtension(ext), ";")[0]
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
}
}
}
if i.Type == "" { if i.Type == "" {
i.Type = "blob" i.Type = extendedMimeTypeCheck(ext)
if saveContent {
return contentErr
} }
} if i.Type == "blob" {
realpath, _, _ := GetRealPath(path)
return nil // Read only the first 512 bytes for efficient MIME detection
} file, err := os.Open(realpath)
// readFirstBytes reads the first bytes of the file.
func (i *ItemInfo) readFirstBytes(path string) []byte {
file, err := os.Open(path)
if err != nil { if err != nil {
i.Type = "blob"
return nil } else {
}
defer file.Close() defer file.Close()
buffer := make([]byte, 512)
buffer := make([]byte, 512) //nolint:gomnd n, _ := file.Read(buffer) // Ignore errors from Read
n, err := file.Read(buffer) i.Type = strings.Split(http.DetectContentType(buffer[:n]), ";")[0]
if err != nil && err != io.EOF { }
i.Type = "blob"
return nil
} }
return buffer[:n]
} }
// TODO add subtitles back // TODO add subtitles back

View File

@ -9,8 +9,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
) )
type Index struct { type Index struct {
@ -132,7 +132,7 @@ func (si *Index) indexDirectory(adjustedPath string, quick, recursive bool) erro
dirInfos = append(dirInfos, *itemInfo) dirInfos = append(dirInfos, *itemInfo)
si.NumDirs++ si.NumDirs++
} else { } else {
_ = itemInfo.detectType(combinedPath+file.Name(), true, false, false) itemInfo.DetectType(combinedPath+file.Name(), false)
itemInfo.Size = file.Size() itemInfo.Size = file.Size()
fileInfos = append(fileInfos, *itemInfo) fileInfos = append(fileInfos, *itemInfo)
totalSize += itemInfo.Size totalSize += itemInfo.Size

View File

@ -4,7 +4,7 @@ import (
"log" "log"
"time" "time"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
) )
// schedule in minutes // schedule in minutes

View File

@ -7,7 +7,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
) )
func BenchmarkFillIndex(b *testing.B) { func BenchmarkFillIndex(b *testing.B) {

View File

@ -6,7 +6,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
) )
var ( var (
@ -14,19 +14,19 @@ var (
maxSearchResults = 100 maxSearchResults = 100
) )
type searchResult struct { type SearchResult struct {
Path string `json:"path"` Path string `json:"path"`
Type string `json:"type"` Type string `json:"type"`
Size int64 `json:"size"` 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 // Remove slashes
scope = si.makeIndexPath(scope) scope = si.makeIndexPath(scope)
runningHash := utils.GenerateRandomHash(4) runningHash := utils.GenerateRandomHash(4)
sessionInProgress.Store(sourceSession, runningHash) // Store the value in the sync.Map sessionInProgress.Store(sourceSession, runningHash) // Store the value in the sync.Map
searchOptions := ParseSearch(search) searchOptions := ParseSearch(search)
results := make(map[string]searchResult, 0) results := make(map[string]SearchResult, 0)
count := 0 count := 0
var directories []string var directories []string
cachedDirs, ok := utils.SearchResultsCache.Get(si.Root + scope).([]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) matches := reducedDir.containsSearchTerm(searchTerm, searchOptions)
if matches { if matches {
results[scopedPath] = searchResult{Path: scopedPath, Type: "directory", Size: dir.Size} results[scopedPath] = SearchResult{Path: scopedPath, Type: "directory", Size: dir.Size}
count++ count++
} }
// search files first // search files first
@ -75,14 +75,14 @@ func (si *Index) Search(search string, scope string, sourceSession string) []sea
value, found := sessionInProgress.Load(sourceSession) value, found := sessionInProgress.Load(sourceSession)
if !found || value != runningHash { if !found || value != runningHash {
si.mu.Unlock() si.mu.Unlock()
return []searchResult{} return []SearchResult{}
} }
if count > maxSearchResults { if count > maxSearchResults {
break break
} }
matches := item.containsSearchTerm(searchTerm, searchOptions) matches := item.containsSearchTerm(searchTerm, searchOptions)
if matches { 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++ 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 "/" // 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 { for _, v := range results {
sortedKeys = append(sortedKeys, v) sortedKeys = append(sortedKeys, v)
} }

View File

@ -119,12 +119,12 @@ func TestSearchIndexes(t *testing.T) {
tests := []struct { tests := []struct {
search string search string
scope string scope string
expectedResult []searchResult expectedResult []SearchResult
}{ }{
{ {
search: "audio", search: "audio",
scope: "/new/", scope: "/new/",
expectedResult: []searchResult{ expectedResult: []SearchResult{
{ {
Path: "test/audio.wav", Path: "test/audio.wav",
Type: "audio", Type: "audio",
@ -135,7 +135,7 @@ func TestSearchIndexes(t *testing.T) {
{ {
search: "test", search: "test",
scope: "/", scope: "/",
expectedResult: []searchResult{ expectedResult: []SearchResult{
{ {
Path: "test/", Path: "test/",
Type: "directory", Type: "directory",
@ -151,7 +151,7 @@ func TestSearchIndexes(t *testing.T) {
{ {
search: "archive", search: "archive",
scope: "/", scope: "/",
expectedResult: []searchResult{ expectedResult: []SearchResult{
{ {
Path: "firstDir/archive.zip", Path: "firstDir/archive.zip",
Type: "archive", Type: "archive",
@ -167,7 +167,7 @@ func TestSearchIndexes(t *testing.T) {
{ {
search: "arch", search: "arch",
scope: "/firstDir", scope: "/firstDir",
expectedResult: []searchResult{ expectedResult: []SearchResult{
{ {
Path: "archive.zip", Path: "archive.zip",
Type: "archive", Type: "archive",
@ -178,7 +178,7 @@ func TestSearchIndexes(t *testing.T) {
{ {
search: "isdir", search: "isdir",
scope: "/", scope: "/",
expectedResult: []searchResult{ expectedResult: []SearchResult{
{ {
Path: "firstDir/thisIsDir/", Path: "firstDir/thisIsDir/",
Type: "directory", Type: "directory",
@ -189,7 +189,7 @@ func TestSearchIndexes(t *testing.T) {
{ {
search: "IsDir type:largerThan=1", search: "IsDir type:largerThan=1",
scope: "/", scope: "/",
expectedResult: []searchResult{ expectedResult: []SearchResult{
{ {
Path: "firstDir/thisIsDir/", Path: "firstDir/thisIsDir/",
Type: "directory", Type: "directory",
@ -200,7 +200,7 @@ func TestSearchIndexes(t *testing.T) {
{ {
search: "video", search: "video",
scope: "/", scope: "/",
expectedResult: []searchResult{ expectedResult: []SearchResult{
{ {
Path: "new/test/video.MP4", Path: "new/test/video.MP4",
Type: "video", Type: "video",

View File

@ -3,7 +3,7 @@ package files
import ( import (
"path/filepath" "path/filepath"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
) )
// UpdateFileMetadata updates the FileInfo for the specified directory in the index. // UpdateFileMetadata updates the FileInfo for the specified directory in the index.

View File

@ -1,4 +1,4 @@
module github.com/gtsteffaniak/filebrowser module github.com/gtsteffaniak/filebrowser/backend
go 1.22.5 go 1.22.5
@ -7,7 +7,8 @@ require (
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/dsoprea/go-exif/v3 v3.0.1 github.com/dsoprea/go-exif/v3 v3.0.1
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568
github.com/goccy/go-yaml v1.15.6 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/golang-jwt/jwt/v4 v4.5.1
github.com/google/go-cmp v0.6.0 github.com/google/go-cmp v0.6.0
github.com/shirou/gopsutil/v3 v3.24.5 github.com/shirou/gopsutil/v3 v3.24.5
@ -17,9 +18,9 @@ require (
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/swaggo/http-swagger v1.3.4 github.com/swaggo/http-swagger v1.3.4
github.com/swaggo/swag v1.16.4 github.com/swaggo/swag v1.16.4
golang.org/x/crypto v0.29.0 golang.org/x/crypto v0.30.0
golang.org/x/image v0.22.0 golang.org/x/image v0.23.0
golang.org/x/text v0.20.0 golang.org/x/text v0.21.0
) )
require ( require (
@ -43,9 +44,9 @@ require (
github.com/swaggo/files v1.0.1 // indirect github.com/swaggo/files v1.0.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/bbolt v1.3.11 // indirect go.etcd.io/bbolt v1.3.11 // indirect
golang.org/x/net v0.31.0 // indirect golang.org/x/net v0.32.0 // indirect
golang.org/x/sys v0.27.0 // indirect golang.org/x/sys v0.28.0 // indirect
golang.org/x/tools v0.27.0 // indirect golang.org/x/tools v0.28.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@ -30,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/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 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 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.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.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
@ -47,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/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 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/goccy/go-yaml v1.15.6 h1:gy5kf1yjMia3/c3wWD+u1z3lU5XlhpT8FZGaLJU9cOA= github.com/goccy/go-yaml v1.15.7 h1:L7XuKpd/A66X4w/dlk08lVfiIADdy79a1AzRoIefC98=
github.com/goccy/go-yaml v1.15.6/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 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 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 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= github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
@ -114,11 +116,11 @@ go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.etcd.io/bbolt v1.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-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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= 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.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.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4= 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.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 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
@ -133,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-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.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.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= 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-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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -153,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.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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -163,13 +165,13 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.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.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.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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= 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-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.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= 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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=

View File

@ -7,7 +7,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
func createApiKeyHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { func createApiKeyHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {

View File

@ -16,11 +16,11 @@ import (
"github.com/golang-jwt/jwt/v4/request" "github.com/golang-jwt/jwt/v4/request"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"github.com/gtsteffaniak/filebrowser/errors" "github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/share" "github.com/gtsteffaniak/filebrowser/backend/share"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
) )
var ( var (

View File

@ -11,10 +11,10 @@ import (
"time" "time"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
"github.com/gtsteffaniak/filebrowser/files" "github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/runner" "github.com/gtsteffaniak/filebrowser/backend/runner"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
type requestContext struct { type requestContext struct {

View File

@ -8,15 +8,15 @@ import (
"time" "time"
"github.com/asdine/storm/v3" "github.com/asdine/storm/v3"
"github.com/gtsteffaniak/filebrowser/diskcache" "github.com/gtsteffaniak/filebrowser/backend/diskcache"
"github.com/gtsteffaniak/filebrowser/files" "github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/img" "github.com/gtsteffaniak/filebrowser/backend/img"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/share" "github.com/gtsteffaniak/filebrowser/backend/share"
"github.com/gtsteffaniak/filebrowser/storage" "github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/storage/bolt" "github.com/gtsteffaniak/filebrowser/backend/storage/bolt"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
) )
func setupTestEnv(t *testing.T) { func setupTestEnv(t *testing.T) {

View File

@ -8,9 +8,10 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/gtsteffaniak/filebrowser/files" "github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/img" "github.com/gtsteffaniak/filebrowser/backend/img"
) )
type ImgService interface { type ImgService interface {
@ -64,7 +65,7 @@ func previewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
return http.StatusBadRequest, fmt.Errorf("can't create preview for directory") return http.StatusBadRequest, fmt.Errorf("can't create preview for directory")
} }
setContentDisposition(w, r, fileInfo.Name) setContentDisposition(w, r, fileInfo.Name)
if fileInfo.Type != "image" { if !strings.HasPrefix(fileInfo.Type, "image") {
return http.StatusNotImplemented, fmt.Errorf("can't create preview for %s type", fileInfo.Type) return http.StatusNotImplemented, fmt.Errorf("can't create preview for %s type", fileInfo.Type)
} }

View File

@ -6,11 +6,11 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/gtsteffaniak/filebrowser/files" "github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/users" "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) { func publicShareHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {

View File

@ -5,6 +5,7 @@ import (
"archive/zip" "archive/zip"
"compress/gzip" "compress/gzip"
"errors" "errors"
"fmt"
"io" "io"
"log" "log"
"net/http" "net/http"
@ -13,7 +14,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/gtsteffaniak/filebrowser/files" "github.com/gtsteffaniak/filebrowser/backend/files"
) )
func setContentDisposition(w http.ResponseWriter, r *http.Request, fileName string) { func setContentDisposition(w http.ResponseWriter, r *http.Request, fileName string) {
@ -44,7 +45,12 @@ func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int,
if !d.user.Perm.Download { if !d.user.Perm.Download {
return http.StatusAccepted, nil return http.StatusAccepted, nil
} }
files := r.URL.Query().Get("files") encodedFiles := r.URL.Query().Get("files")
// Decode the URL-encoded path
files, err := url.QueryUnescape(encodedFiles)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
}
return rawFilesHandler(w, r, d, strings.Split(files, ",")) return rawFilesHandler(w, r, d, strings.Split(files, ","))
} }

View File

@ -13,9 +13,9 @@ import (
"github.com/shirou/gopsutil/v3/disk" "github.com/shirou/gopsutil/v3/disk"
"github.com/gtsteffaniak/filebrowser/errors" "github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/files" "github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
) )
// resourceGetHandler retrieves information about a resource. // resourceGetHandler retrieves information about a resource.
@ -33,9 +33,13 @@ import (
// @Failure 500 {object} map[string]string "Internal server error" // @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/resources [get] // @Router /api/resources [get]
func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
// TODO source := r.URL.Query().Get("source") // 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{ fileInfo, err := files.FileInfoFaster(files.FileOptions{
Path: filepath.Join(d.user.Scope, path), Path: filepath.Join(d.user.Scope, path),
Modify: d.user.Perm.Modify, Modify: d.user.Perm.Modify,
@ -78,7 +82,12 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
// @Router /api/resources [delete] // @Router /api/resources [delete]
func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
// TODO source := r.URL.Query().Get("source") // 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 { if path == "/" || !d.user.Perm.Delete {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
@ -87,7 +96,7 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon
return http.StatusNotFound, err return http.StatusNotFound, err
} }
fileOpts := files.FileOptions{ fileOpts := files.FileOptions{
Path: filepath.Join(d.user.Scope, path), Path: realPath,
IsDir: isDir, IsDir: isDir,
Modify: d.user.Perm.Modify, Modify: d.user.Perm.Modify,
Expand: false, Expand: false,
@ -130,7 +139,12 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon
// @Router /api/resources [post] // @Router /api/resources [post]
func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
// TODO source := r.URL.Query().Get("source") // 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) { if !d.user.Perm.Create || !d.user.Check(path) {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
@ -142,7 +156,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
} }
// Directories creation on POST. // Directories creation on POST.
if strings.HasSuffix(path, "/") { if strings.HasSuffix(path, "/") {
err := files.WriteDirectory(fileOpts) err = files.WriteDirectory(fileOpts)
if err != nil { if err != nil {
return errToStatus(err), err return errToStatus(err), err
} }
@ -188,7 +202,13 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
// @Router /api/resources [put] // @Router /api/resources [put]
func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
// TODO source := r.URL.Query().Get("source") // TODO source := r.URL.Query().Get("source")
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) { if !d.user.Perm.Modify || !d.user.Check(path) {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
@ -233,16 +253,21 @@ func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContex
// @Router /api/resources [patch] // @Router /api/resources [patch]
func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
// TODO source := r.URL.Query().Get("source") // 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") action := r.URL.Query().Get("action")
dst, err := url.QueryUnescape(dst) encodedFrom := r.URL.Query().Get("from")
if !d.user.Check(src) || !d.user.Check(dst) { // Decode the URL-encoded path
return http.StatusForbidden, nil 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 { if err != nil {
return errToStatus(err), err return errToStatus(err), err
} }
if !d.user.Check(src) || !d.user.Check(dst) {
return http.StatusForbidden, nil
}
if dst == "/" || src == "/" { if dst == "/" || src == "/" {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
@ -298,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 { func patchAction(ctx context.Context, action, src, dst string, d *requestContext, fileCache FileCache, isSrcDir bool) error {
switch action { switch action {
// TODO: use enum
case "copy": case "copy":
if !d.user.Perm.Create { if !d.user.Perm.Create {
return errors.ErrPermissionDenied return errors.ErrPermissionDenied
} }
return files.CopyResource(src, dst, isSrcDir) err := files.CopyResource(src, dst, isSrcDir)
return err
case "rename", "move": case "rename", "move":
if !d.user.Perm.Rename { if !d.user.Perm.Rename {
return errors.ErrPermissionDenied return errors.ErrPermissionDenied
} }
@ -378,7 +404,9 @@ func diskUsage(w http.ResponseWriter, r *http.Request, d *requestContext) (int,
} }
func inspectIndex(w http.ResponseWriter, r *http.Request) { 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" isDir := r.URL.Query().Get("isDir") == "true"
index := files.GetIndex(config.Server.Root) index := files.GetIndex(config.Server.Root)
info, _ := index.GetReducedMetadata(path, isDir) info, _ := index.GetReducedMetadata(path, isDir)

View File

@ -10,9 +10,9 @@ import (
"os" "os"
"text/template" "text/template"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/storage" "github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/version" "github.com/gtsteffaniak/filebrowser/backend/version"
httpSwagger "github.com/swaggo/http-swagger" // http-swagger middleware httpSwagger "github.com/swaggo/http-swagger" // http-swagger middleware
) )

View File

@ -4,8 +4,8 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/gtsteffaniak/filebrowser/files" "github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
) )
// searchHandler handles search requests for files based on the provided query. // searchHandler handles search requests for files based on the provided query.
@ -49,7 +49,7 @@ import (
// @Param query query string true "Search query" // @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 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" // @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" // @Failure 400 {object} map[string]string "Bad Request"
// @Router /api/search [get] // @Router /api/search [get]
func searchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { func searchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {

View File

@ -4,8 +4,8 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
type settingsData struct { type settingsData struct {

View File

@ -12,8 +12,8 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"github.com/gtsteffaniak/filebrowser/errors" "github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/share" "github.com/gtsteffaniak/filebrowser/backend/share"
) )
// shareListHandler returns a list of all share links. // shareListHandler returns a list of all share links.

View File

@ -12,9 +12,9 @@ import (
"strings" "strings"
"text/template" "text/template"
"github.com/gtsteffaniak/filebrowser/auth" "github.com/gtsteffaniak/filebrowser/backend/auth"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/version" "github.com/gtsteffaniak/filebrowser/backend/version"
) )
var templateRenderer *TemplateRenderer var templateRenderer *TemplateRenderer

View File

@ -12,10 +12,10 @@ import (
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/gtsteffaniak/filebrowser/errors" "github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/files" "github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/storage" "github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
var ( var (

View File

@ -5,7 +5,7 @@ import (
"net/http" "net/http"
"os" "os"
libErrors "github.com/gtsteffaniak/filebrowser/errors" libErrors "github.com/gtsteffaniak/filebrowser/backend/errors"
) )
func errToStatus(err error) int { func errToStatus(err error) int {

View File

@ -261,3 +261,22 @@ func getEmbeddedThumbnail(in io.Reader) ([]byte, io.Reader, error) {
thm, err := ifd.Thumbnail() thm, err := ifd.Thumbnail()
return thm, wrappedReader, err 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
}

View File

@ -1,7 +1,7 @@
package main package main
import ( import (
"github.com/gtsteffaniak/filebrowser/cmd" "github.com/gtsteffaniak/filebrowser/backend/cmd"
) )
func main() { func main() {

View File

@ -3,7 +3,7 @@ package runner
import ( import (
"os/exec" "os/exec"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
) )
// ParseCommand parses the command taking in account if the current // ParseCommand parses the command taking in account if the current

View File

@ -7,9 +7,9 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"github.com/gtsteffaniak/filebrowser/files" "github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
// Runner is a commands runner. // Runner is a commands runner.

View File

@ -7,7 +7,7 @@ import (
"strings" "strings"
"github.com/goccy/go-yaml" "github.com/goccy/go-yaml"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
var Config Settings var Config Settings

View File

@ -3,7 +3,7 @@ package settings
import ( import (
"testing" "testing"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
func TestSettings_MakeUserDir(t *testing.T) { func TestSettings_MakeUserDir(t *testing.T) {

View File

@ -3,7 +3,7 @@ package settings
import ( import (
"crypto/rand" "crypto/rand"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
const DefaultUsersHomeBasePath = "/users" const DefaultUsersHomeBasePath = "/users"

View File

@ -1,8 +1,8 @@
package settings package settings
import ( import (
"github.com/gtsteffaniak/filebrowser/errors" "github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
// StorageBackend is a settings storage backend. // StorageBackend is a settings storage backend.

View File

@ -1,7 +1,7 @@
package settings package settings
import ( import (
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
type Settings struct { type Settings struct {

View File

@ -3,7 +3,7 @@ package share
import ( import (
"time" "time"
"github.com/gtsteffaniak/filebrowser/errors" "github.com/gtsteffaniak/filebrowser/backend/errors"
) )
// StorageBackend is the interface to implement for a share storage. // StorageBackend is the interface to implement for a share storage.

View File

@ -2,8 +2,8 @@ package bolt
import ( import (
"github.com/asdine/storm/v3" "github.com/asdine/storm/v3"
"github.com/gtsteffaniak/filebrowser/auth" "github.com/gtsteffaniak/filebrowser/backend/auth"
"github.com/gtsteffaniak/filebrowser/errors" "github.com/gtsteffaniak/filebrowser/backend/errors"
) )
type authBackend struct { type authBackend struct {

View File

@ -3,10 +3,10 @@ package bolt
import ( import (
"github.com/asdine/storm/v3" "github.com/asdine/storm/v3"
"github.com/gtsteffaniak/filebrowser/auth" "github.com/gtsteffaniak/filebrowser/backend/auth"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/share" "github.com/gtsteffaniak/filebrowser/backend/share"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
) )
// NewStorage creates a storage.Storage based on Bolt DB. // NewStorage creates a storage.Storage based on Bolt DB.

View File

@ -2,7 +2,7 @@ package bolt
import ( import (
"github.com/asdine/storm/v3" "github.com/asdine/storm/v3"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
) )
type settingsBackend struct { type settingsBackend struct {

View File

@ -4,8 +4,8 @@ import (
"github.com/asdine/storm/v3" "github.com/asdine/storm/v3"
"github.com/asdine/storm/v3/q" "github.com/asdine/storm/v3/q"
"github.com/gtsteffaniak/filebrowser/errors" "github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/share" "github.com/gtsteffaniak/filebrowser/backend/share"
) )
type shareBackend struct { type shareBackend struct {

View File

@ -6,9 +6,9 @@ import (
"github.com/asdine/storm/v3" "github.com/asdine/storm/v3"
"github.com/gtsteffaniak/filebrowser/errors" "github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
) )
type usersBackend struct { type usersBackend struct {

View File

@ -3,7 +3,7 @@ package bolt
import ( import (
"github.com/asdine/storm/v3" "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 { func get(db *storm.DB, name string, to interface{}) error {

View File

@ -7,14 +7,14 @@ import (
"path/filepath" "path/filepath"
"github.com/asdine/storm/v3" "github.com/asdine/storm/v3"
"github.com/gtsteffaniak/filebrowser/auth" "github.com/gtsteffaniak/filebrowser/backend/auth"
"github.com/gtsteffaniak/filebrowser/errors" "github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/files" "github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/share" "github.com/gtsteffaniak/filebrowser/backend/share"
"github.com/gtsteffaniak/filebrowser/storage/bolt" "github.com/gtsteffaniak/filebrowser/backend/storage/bolt"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/utils" "github.com/gtsteffaniak/filebrowser/backend/utils"
) )
// Storage is a storage powered by a Backend which makes the necessary // Storage is a storage powered by a Backend which makes the necessary

View File

@ -579,7 +579,7 @@ const docTemplate = `{
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/files.searchResult" "$ref": "#/definitions/files.SearchResult"
} }
} }
}, },
@ -1186,7 +1186,7 @@ const docTemplate = `{
} }
} }
}, },
"files.searchResult": { "files.SearchResult": {
"type": "object", "type": "object",
"properties": { "properties": {
"path": { "path": {

View File

@ -568,7 +568,7 @@
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/files.searchResult" "$ref": "#/definitions/files.SearchResult"
} }
} }
}, },
@ -1175,7 +1175,7 @@
} }
} }
}, },
"files.searchResult": { "files.SearchResult": {
"type": "object", "type": "object",
"properties": { "properties": {
"path": { "path": {

View File

@ -31,7 +31,7 @@ definitions:
type: type:
type: string type: string
type: object type: object
files.searchResult: files.SearchResult:
properties: properties:
path: path:
type: string type: string
@ -658,7 +658,7 @@ paths:
description: List of search results description: List of search results
schema: schema:
items: items:
$ref: '#/definitions/files.searchResult' $ref: '#/definitions/files.SearchResult'
type: array type: array
"400": "400":
description: Bad Request description: Bad Request

View File

@ -4,7 +4,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/gtsteffaniak/filebrowser/errors" "github.com/gtsteffaniak/filebrowser/backend/errors"
) )
// StorageBackend is the interface to implement for a users storage. // StorageBackend is the interface to implement for a users storage.

View File

@ -1,18 +1,17 @@
# Planned Roadmap # Planned Roadmap
upcoming 0.3.x releases, ordered by priority: upcoming 0.3.x releases, ordered by priority:
- more indexing flexability
- More filetype icons and refreshed icons. - option not to index hidden files/folders
- more filetype previews - eg. office, photoshop, vector, 3d files. - options folders to include/exclude from indexing
- Enable mobile search with same features as desktop - implement more indexing runners for more efficienct filesystem watching
- Enable mobile search with same features as desktop - more filetype previews: eg. raw img, office, photoshop, vector, 3d files.
- Theme configuration from settings - introduce jobs as replacement to runners.
- introduce jobs as replacement to runners.
- Add Job status to the sidebar - Add Job status to the sidebar
- index status. - index status.
- Job status from users - Job status from users
- upload status - upload status
- opentelemetry metrics - opentelemetry metrics
Unplanned Future releases: Unplanned Future releases:
- multiple sources https://github.com/filebrowser/filebrowser/issues/2514 - multiple sources https://github.com/filebrowser/filebrowser/issues/2514

View File

@ -25,6 +25,7 @@
"css-vars-ponyfill": "^2.4.3", "css-vars-ponyfill": "^2.4.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"material-icons": "^1.10.5", "material-icons": "^1.10.5",
"material-symbols": "^0.27.2",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"qrcode.vue": "^3.4.1", "qrcode.vue": "^3.4.1",
"vue": "^3.4.21", "vue": "^3.4.21",

View File

@ -6,7 +6,7 @@ import { notify } from "@/notify";
// Notify if errors occur // Notify if errors occur
export async function fetchFiles(url, content = false) { export async function fetchFiles(url, content = false) {
try { try {
let path = removePrefix(url, "files"); let path = encodeURIComponent(removePrefix(url, "files"));
const apiPath = getApiPath("api/resources",{path: path, content: content}); const apiPath = getApiPath("api/resources",{path: path, content: content});
const res = await fetchURL(apiPath); const res = await fetchURL(apiPath);
const data = await res.json(); const data = await res.json();
@ -24,7 +24,8 @@ async function resourceAction(url, method, content) {
if (content) { if (content) {
opts.body = 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); const res = await fetchURL(apiPath, opts);
return res; return res;
} catch (err) { } catch (err) {
@ -35,7 +36,8 @@ async function resourceAction(url, method, content) {
export async function remove(url) { export async function remove(url) {
try { try {
return await resourceAction(url, "DELETE"); let path = encodeURIComponent(removePrefix(url, "files"));
return await resourceAction(path, "DELETE");
} catch (err) { } catch (err) {
notify.showError(err.message || "Error deleting resource"); notify.showError(err.message || "Error deleting resource");
throw err; throw err;
@ -44,7 +46,8 @@ export async function remove(url) {
export async function put(url, content = "") { export async function put(url, content = "") {
try { try {
return await resourceAction(url, "PUT", content); let path = encodeURIComponent(removePrefix(url, "files"));
return await resourceAction(path, "PUT", content);
} catch (err) { } catch (err) {
notify.showError(err.message || "Error putting resource"); notify.showError(err.message || "Error putting resource");
throw err; throw err;
@ -58,14 +61,14 @@ export function download(format, files) {
try { try {
let fileargs = ""; let fileargs = "";
if (files.length === 1) { if (files.length === 1) {
fileargs = removePrefix(files[0], "files") fileargs = decodeURI(removePrefix(files[0], "files"))
} else { } else {
for (let file of files) { for (let file of files) {
fileargs += removePrefix(file,"files") + ","; fileargs += decodeURI(removePrefix(file,"files")) + ",";
} }
fileargs = fileargs.substring(0, fileargs.length - 1); fileargs = fileargs.substring(0, fileargs.length - 1);
} }
const apiPath = getApiPath("api/raw", { files: fileargs, algo: format }); const apiPath = getApiPath("api/raw", { files: encodeURIComponent(fileargs), algo: format });
const url = window.origin+apiPath const url = window.origin+apiPath
window.open(url); window.open(url);
} catch (err) { } catch (err) {
@ -130,9 +133,11 @@ export async function moveCopy(items, action = "copy", overwrite = false, rename
} }
try { try {
for (let item of items) { for (let item of items) {
let toPath = encodeURIComponent(removePrefix(decodeURI(item.to), "files"));
let fromPath = encodeURIComponent(removePrefix(decodeURI(item.from), "files"));
let localParams = { ...params }; let localParams = { ...params };
localParams.destination = item.to; localParams.destination = toPath;
localParams.from = item.from; localParams.from = fromPath;
const apiPath = getApiPath("api/resources", localParams); const apiPath = getApiPath("api/resources", localParams);
promises.push(fetch(apiPath, { method: "PATCH" })); promises.push(fetch(apiPath, { method: "PATCH" }));
} }
@ -157,7 +162,7 @@ export async function checksum(url, algo) {
export function getDownloadURL(path, inline) { export function getDownloadURL(path, inline) {
try { try {
const params = { const params = {
files: removePrefix(path,"files"), files: encodeURIComponent(removePrefix(decodeURI(path),"files")),
...(inline && { inline: "true" }), ...(inline && { inline: "true" }),
}; };
const apiPath = getApiPath("api/raw", params); const apiPath = getApiPath("api/raw", params);
@ -171,7 +176,7 @@ export function getDownloadURL(path, inline) {
export function getPreviewURL(path, size, modified) { export function getPreviewURL(path, size, modified) {
try { try {
const params = { const params = {
path: path, path: encodeURIComponent(removePrefix(decodeURI(path),"files")),
size: size, size: size,
key: Date.parse(modified), key: Date.parse(modified),
inline: "true", inline: "true",
@ -190,7 +195,7 @@ export function getSubtitlesURL(file) {
for (const sub of file.subtitles) { for (const sub of file.subtitles) {
const params = { const params = {
inline: "true", inline: "true",
path: sub path: encodeURIComponent(removePrefix(sub,"files"))
}; };
const apiPath = getApiPath("api/raw", params); const apiPath = getApiPath("api/raw", params);
return window.origin+apiPath return window.origin+apiPath

View File

@ -61,7 +61,6 @@ export async function fetchJSON(url, opts) {
export function adjustedData(data, url) { export function adjustedData(data, url) {
data.url = url; data.url = url;
if (data.type === "directory") { if (data.type === "directory") {
if (!data.url.endsWith("/")) data.url += "/"; if (!data.url.endsWith("/")) data.url += "/";

View File

@ -8,7 +8,7 @@
left: `${left}px`, left: `${left}px`,
}" }"
class="button" class="button"
:class="{ 'dark-mode': isDarkMode, mobile: isMobile }" :class="{ 'dark-mode': isDarkMode, centered: centered }"
> >
<div v-if="selectedCount > 0" class="button selected-count-header"> <div v-if="selectedCount > 0" class="button selected-count-header">
<span>{{ selectedCount }} selected</span> <span>{{ selectedCount }} selected</span>
@ -108,8 +108,8 @@ export default {
user() { user() {
return state.user; return state.user;
}, },
isMobile() { centered() {
return getters.isMobile(); return getters.isMobile() || ( !this.posX || !this.posY );
}, },
showContext() { showContext() {
if (getters.currentPromptName() == "ContextMenu" && state.prompts != []) { if (getters.currentPromptName() == "ContextMenu" && state.prompts != []) {
@ -158,7 +158,6 @@ export default {
return mutations.showHover(value); return mutations.showHover(value);
}, },
setPositions() { setPositions() {
console.log("Setting positions");
const contextProps = getters.currentPrompt().props; const contextProps = getters.currentPrompt().props;
let tempX = contextProps.posX; let tempX = contextProps.posX;
let tempY = contextProps.posY; let tempY = contextProps.posY;
@ -204,7 +203,7 @@ export default {
justify-content: center; justify-content: center;
} }
#context-menu.mobile { #context-menu.centered {
top: 50% !important; top: 50% !important;
left: 50% !important; left: 50% !important;
-webkit-transform: translate(-50%, -50%); -webkit-transform: translate(-50%, -50%);

View File

@ -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>

View File

@ -3,15 +3,30 @@
<!-- Search input section --> <!-- Search input section -->
<div id="input" @click="open"> <div id="input" @click="open">
<!-- Close button visible when search is active --> <!-- Close button visible when search is active -->
<button v-if="active" class="action" @click="close" :aria-label="$t('buttons.close')" <button
:title="$t('buttons.close')"> v-if="active"
class="action"
@click="close"
:aria-label="$t('buttons.close')"
:title="$t('buttons.close')"
>
<i class="material-icons">close</i> <i class="material-icons">close</i>
</button> </button>
<!-- Search icon when search is not active --> <!-- Search icon when search is not active -->
<i v-else class="material-icons">search</i> <i v-else class="material-icons">search</i>
<!-- Input field for search --> <!-- Input field for search -->
<input id="main-input" class="main-input" type="text" @keyup.exact="keyup" @input="submit" ref="input" <input
:autofocus="active" v-model.trim="value" :aria-label="$t('search.search')" :placeholder="$t('search.search')" /> id="main-input"
class="main-input"
type="text"
@keyup.exact="keyup"
@input="submit"
ref="input"
:autofocus="active"
v-model.trim="value"
:aria-label="$t('search.search')"
:placeholder="$t('search.search')"
/>
</div> </div>
<!-- Search results for desktop --> <!-- Search results for desktop -->
@ -21,29 +36,52 @@
<div> <div>
<div v-if="active"> <div v-if="active">
<div v-if="isMobile"> <div v-if="isMobile">
<ButtonGroup :buttons="toggleOptionButton" @button-clicked="enableOptions" @remove-button-clicked="disableOptions" /> <ButtonGroup
:buttons="toggleOptionButton"
@button-clicked="enableOptions"
@remove-button-clicked="disableOptions"
/>
</div> </div>
<div v-show="showOptions"> <div v-show="showOptions">
<!-- Button groups for filtering search results --> <!-- Button groups for filtering search results -->
<ButtonGroup :buttons="folderSelect" @button-clicked="addToTypes" @remove-button-clicked="removeFromTypes" <ButtonGroup
@disableAll="folderSelectClicked()" @enableAll="resetButtonGroups()" /> :buttons="folderSelect"
<ButtonGroup :buttons="typeSelect" @button-clicked="addToTypes" @remove-button-clicked="removeFromTypes" @button-clicked="addToTypes"
:isDisabled="isTypeSelectDisabled" /> @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 --> <!-- Inputs for filtering by file size -->
<div class="sizeConstraints"> <div class="sizeConstraints">
<div class="sizeInputWrapper"> <div class="sizeInputWrapper">
<p>Smaller Than:</p> <p>Smaller Than:</p>
<input class="sizeInput" v-model="smallerThan" type="number" min="0" placeholder="number" /> <input
class="sizeInput"
v-model="smallerThan"
type="number"
min="0"
placeholder="number"
/>
<p>MB</p> <p>MB</p>
</div> </div>
<div class="sizeInputWrapper"> <div class="sizeInputWrapper">
<p>Larger Than:</p> <p>Larger Than:</p>
<input class="sizeInput" v-model="largerThan" type="number" placeholder="number" /> <input
class="sizeInput"
v-model="largerThan"
type="number"
placeholder="number"
/>
<p>MB</p> <p>MB</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Loading icon when search is ongoing --> <!-- Loading icon when search is ongoing -->
@ -78,28 +116,14 @@
<!-- List of search results --> <!-- List of search results -->
<ul v-show="results.length > 0"> <ul v-show="results.length > 0">
<li v-for="(s, k) in results" :key="k" class="search-entry"> <li v-for="(s, k) in results" :key="k" class="search-entry">
<router-link :to="s.path"> <a :href="getRelative(s.path)">
<i v-if="s.type == 'directory'" class="material-icons folder-icons"> <Icon :mimetype="s.type" />
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"> <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> </span>
<div class="filesize">{{ humanSize(s.size) }}</div> <div class="filesize">{{ humanSize(s.size) }}</div>
</router-link> </a>
</li> </li>
</ul> </ul>
</div> </div>
@ -112,6 +136,7 @@ import ButtonGroup from "./ButtonGroup.vue";
import { search } from "@/api"; import { search } from "@/api";
import { getters, mutations, state } from "@/store"; import { getters, mutations, state } from "@/store";
import { getHumanReadableFilesize } from "@/utils/filesizes"; import { getHumanReadableFilesize } from "@/utils/filesizes";
import Icon from "@/components/Icon.vue";
var boxes = { var boxes = {
folder: { label: "folders", icon: "folder" }, folder: { label: "folders", icon: "folder" },
@ -126,6 +151,7 @@ var boxes = {
export default { export default {
components: { components: {
ButtonGroup, ButtonGroup,
Icon,
}, },
name: "search", name: "search",
data: function () { data: function () {
@ -147,9 +173,7 @@ export default {
{ label: "Documents", value: "type:doc" }, { label: "Documents", value: "type:doc" },
{ label: "Archives", value: "type:archive" }, { label: "Archives", value: "type:archive" },
], ],
toggleOptionButton: [ toggleOptionButton: [{ label: "Show Options" }],
{ label: "Show Options" },
],
value: "", value: "",
ongoing: false, ongoing: false,
results: [], results: [],
@ -192,7 +216,7 @@ export default {
}, },
computed: { computed: {
showOptions() { showOptions() {
return !this.hiddenOptions || !this.isMobile return !this.hiddenOptions || !this.isMobile;
}, },
isMobile() { isMobile() {
return state.isMobile; return state.isMobile;
@ -241,17 +265,19 @@ export default {
}, },
}, },
methods: { methods: {
getRelative(path) {
return window.location.href + "/" + path;
},
getIcon(mimetype) {
return getMaterialIconForType(mimetype);
},
enableOptions() { enableOptions() {
this.hiddenOptions = false this.hiddenOptions = false;
this.toggleOptionButton = [ this.toggleOptionButton = [{ label: "Hide Options" }];
{ label: "Hide Options" },
];
}, },
disableOptions() { disableOptions() {
this.hiddenOptions = true this.hiddenOptions = true;
this.toggleOptionButton = [ this.toggleOptionButton = [{ label: "Show Options" }];
{ label: "Show Options" },
];
}, },
humanSize(size) { humanSize(size) {
return getHumanReadableFilesize(size); return getHumanReadableFilesize(size);
@ -372,7 +398,7 @@ export default {
word-wrap: break-word; word-wrap: break-word;
} }
#results>#result-list { #results > #result-list {
max-height: 80vh; max-height: 80vh;
width: 35em; width: 35em;
overflow: scroll; overflow: scroll;
@ -518,7 +544,7 @@ body.rtl #search #result {
direction: ltr; direction: ltr;
} }
#search #result>div>*:first-child { #search #result > div > *:first-child {
margin-top: 0; margin-top: 0;
} }
@ -528,7 +554,7 @@ body.rtl #search #result {
} }
/* Search Results */ /* Search Results */
body.rtl #search #result ul>* { body.rtl #search #result ul > * {
direction: ltr; direction: ltr;
text-align: left; text-align: left;
} }

View File

@ -1,11 +1,11 @@
<template> <template>
<component <a
:is="quickNav ? 'a' : 'div'" :href="getUrl()"
:href="quickNav ? getUrl() : undefined"
:class="{ :class="{
item: true, item: true,
activebutton: isMaximized && isSelected, activebutton: isMaximized && isSelected,
}" }"
:id="getID"
role="button" role="button"
tabindex="0" tabindex="0"
:draggable="isDraggable" :draggable="isDraggable"
@ -17,20 +17,21 @@
:aria-label="name" :aria-label="name"
:aria-selected="isSelected" :aria-selected="isSelected"
@contextmenu="onRightClick" @contextmenu="onRightClick"
@click="quickNav ? toggleClick() : itemClick($event)" @click="click($event)"
> >
<div @click="toggleClick" :class="{ activetitle: isMaximized && isSelected }"> <div @click="toggleClick" :class="{ activetitle: isMaximized && isSelected }">
<img <img
v-if="readOnly === undefined && type === 'image' && isThumbsEnabled && isInView" v-if="
readOnly === undefined &&
type.startsWith('image') &&
isThumbsEnabled &&
isInView
"
v-lazy="thumbnailUrl" v-lazy="thumbnailUrl"
:class="{ activeimg: isMaximized && isSelected }" :class="{ activeimg: isMaximized && isSelected }"
ref="thumbnail" ref="thumbnail"
/> />
<i <Icon v-else :mimetype="type" />
:class="{ iconActive: isMaximized && isSelected }"
v-else
class="material-icons"
></i>
</div> </div>
<div class="text" :class="{ activecontent: isMaximized && isSelected }"> <div class="text" :class="{ activecontent: isMaximized && isSelected }">
@ -40,7 +41,7 @@
<time :datetime="modified">{{ humanTime() }}</time> <time :datetime="modified">{{ humanTime() }}</time>
</p> </p>
</div> </div>
</component> </a>
</template> </template>
<style> <style>
@ -77,9 +78,14 @@ import * as upload from "@/utils/upload";
import { state, getters, mutations } from "@/store"; // Import your custom store import { state, getters, mutations } from "@/store"; // Import your custom store
import { baseURL } from "@/utils/constants"; import { baseURL } from "@/utils/constants";
import { router } from "@/router"; import { router } from "@/router";
import { url } from "@/utils";
import Icon from "@/components/Icon.vue";
export default { export default {
name: "item", name: "item",
components: {
Icon,
},
data() { data() {
return { return {
isThumbnailInView: false, isThumbnailInView: false,
@ -99,6 +105,9 @@ export default {
"path", "path",
], ],
computed: { computed: {
getID() {
return url.base64Encode(encodeURIComponent(this.name));
},
quickNav() { quickNav() {
return state.user.singleClick && !state.multiple; return state.user.singleClick && !state.multiple;
}, },
@ -146,6 +155,7 @@ export default {
}, },
}, },
mounted() { mounted() {
// Prevent default navigation for left-clicks
const observer = new IntersectionObserver(this.handleIntersect, { const observer = new IntersectionObserver(this.handleIntersect, {
root: null, root: null,
rootMargin: "0px", rootMargin: "0px",
@ -159,6 +169,13 @@ export default {
} }
}, },
methods: { methods: {
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() { getUrl() {
return baseURL.slice(0, -1) + this.url; return baseURL.slice(0, -1) + this.url;
}, },
@ -166,7 +183,8 @@ export default {
event.preventDefault(); // Prevent default context menu event.preventDefault(); // Prevent default context menu
// If no items are selected, select the right-clicked item // If no items are selected, select the right-clicked item
if (getters.selectedCount() === 0) { if (!state.multiple) {
mutations.resetSelected();
mutations.addSelected(this.index); mutations.addSelected(this.index);
} }
mutations.showHover({ mutations.showHover({
@ -278,13 +296,18 @@ export default {
action(overwrite, rename); action(overwrite, rename);
}, },
itemClick(event) {
if (this.singleClick && !state.multiple) this.open();
else this.click(event);
},
click(event) { click(event) {
if (!this.singleClick && getters.selectedCount() !== 0) event.preventDefault(); if (event.button === 0) {
// Left-click
event.preventDefault();
if (this.quickNav) {
this.open();
}
}
if (!this.singleClick && getters.selectedCount() !== 0 && event.button === 0) {
event.preventDefault();
}
setTimeout(() => { setTimeout(() => {
this.touches = 0; this.touches = 0;
}, 500); }, 500);
@ -319,12 +342,15 @@ export default {
return; return;
} }
if (!this.singleClick && !event.ctrlKey && !event.metaKey && !state.multiple) if (!this.singleClick && !event.ctrlKey && !event.metaKey && !state.multiple) {
mutations.resetSelected(); mutations.resetSelected();
}
mutations.addSelected(this.index); mutations.addSelected(this.index);
}, },
open() { open() {
router.push({ path: this.url }); location.hash = state.req.items[this.index].name;
const newurl = url.removePrefix(this.url);
router.push({ path: newurl });
}, },
}, },
}; };

View File

@ -16,6 +16,10 @@
<strong>{{ $t("prompts.size") }}:</strong> <strong>{{ $t("prompts.size") }}:</strong>
<span id="content_length"></span> {{ humanSize }} <span id="content_length"></span> {{ humanSize }}
</p> </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"> <p v-if="selected.length < 2" :title="modTime">
<strong>{{ $t("prompts.lastModified") }}:</strong> {{ humanTime }} <strong>{{ $t("prompts.lastModified") }}:</strong> {{ humanTime }}
</p> </p>
@ -124,6 +128,11 @@ export default {
? state.req.name ? state.req.name
: state.req.items[this.selected[0]].name; : state.req.items[this.selected[0]].name;
}, },
type() {
return getters.selectedCount() === 0
? state.req.type
: state.req.items[this.selected[0]].type;
},
dir() { dir() {
return ( return (
getters.selectedCount() > 1 || getters.selectedCount() > 1 ||

View File

@ -3,7 +3,7 @@
<div class="card-title"> <div class="card-title">
<h2>{{ $t("buttons.share") }}</h2> <h2>{{ $t("buttons.share") }}</h2>
</div> </div>
<div class="searchContext">Path: {{ getContext }}</div> <div class="searchContext">Path: {{ subpath }}</div>
<template v-if="listing"> <template v-if="listing">
<div class="card-content"> <div class="card-content">
@ -167,26 +167,14 @@ export default {
} }
return state.req.items[this.selected[0]].url; 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() { async beforeMount() {
try { try {
const prefix = `/files`; let path = "." + getters.routePath("files");
let path = state.route.path.startsWith(prefix) if (getters.selectedCount() === 1) {
? state.route.path.slice(prefix.length) path = path + state.req.items[this.selected[0]].name;
: state.route.path;
path = decodeURIComponent(path);
if (path == "") {
path = "/";
} }
this.subpath = path; this.subpath = decodeURIComponent(path);
// get last element of the path // get last element of the path
const links = await shareApi.get(this.subpath); const links = await shareApi.get(this.subpath);
this.links = links; this.links = links;

View File

@ -63,6 +63,7 @@ export default {
overflow: auto; overflow: auto;
margin-bottom: 0px !important; margin-bottom: 0px !important;
} }
#sidebar { #sidebar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -78,9 +79,6 @@ export default {
padding-bottom: 4em; padding-bottom: 4em;
background-color: rgb(255 255 255 / 50%) !important; background-color: rgb(255 255 255 / 50%) !important;
} }
#sidebar.dark-mode {
background-color: rgb(37 49 55 / 33%) !important;
}
#sidebar.sticky { #sidebar.sticky {
z-index: 3; z-index: 3;

View File

@ -78,7 +78,7 @@ over
/* Main Content */ /* Main Content */
main { main {
position: fixed; position: fixed;
padding: 1em; padding: .5em;
padding-top: 4em; padding-top: 4em;
overflow: scroll; overflow: scroll;
top: 0; top: 0;

View File

@ -272,3 +272,27 @@
.dark-mode #results { .dark-mode #results {
background-color: var(--background); 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;
}

View File

@ -1,4 +1,6 @@
@import 'material-icons/iconfont/filled.css'; @import 'material-icons/iconfont/filled.css';
@import 'material-icons/iconfont/outlined.css';
@import 'material-symbols/index.css';
@font-face { @font-face {
font-family: 'Roboto'; 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; 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;
}

View File

@ -9,7 +9,6 @@ header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
background-color: rgb(255 255 255 / 50%) !important;
padding: 0.5em; padding: 0.5em;
} }
@ -49,25 +48,3 @@ header img {
header .action span { header .action span {
display: none; 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;
}

View File

@ -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) }

View File

@ -7,7 +7,6 @@
@import "./base.css"; @import "./base.css";
@import "./header.css"; @import "./header.css";
@import "./listing.css"; @import "./listing.css";
@import "./listing-icons.css";
@import "./dashboard.css"; @import "./dashboard.css";
@import "./login.css"; @import "./login.css";
@import './mobile.css'; @import './mobile.css';

View File

@ -121,7 +121,7 @@ const router = createRouter({
// Helper function to check if a route resolves to itself // Helper function to check if a route resolves to itself
function isSameRoute(to: RouteLocation, from: RouteLocation) { function isSameRoute(to: RouteLocation, from: RouteLocation) {
return to.path === from.path && JSON.stringify(to.params) === JSON.stringify(from.params); return to.path === from.path && JSON.stringify(to.params) === JSON.stringify(from.params) && to.hash === from.hash;
} }
router.beforeResolve(async (to, from, next) => { router.beforeResolve(async (to, from, next) => {

View File

@ -105,6 +105,9 @@ export const getters = {
if (getters.currentView() == "settings") { if (getters.currentView() == "settings") {
visible = !getters.isMobile(); visible = !getters.isMobile();
} }
if (getters.currentView() == "share") {
visible = false
}
if (typeof getters.currentPromptName() === "string" && !getters.isStickySidebar()) { if (typeof getters.currentPromptName() === "string" && !getters.isStickySidebar()) {
visible = false; visible = false;
} }

View File

@ -0,0 +1,5 @@
import * as url from "./url.js";
export {
url,
};

View File

@ -0,0 +1,198 @@
export function getTypeInfo(mimeType) {
if (mimeType === "directory" || mimeType === "application/vnd.google-apps.folder") {
return {
classes: "blue-icons material-icons",
materialIcon: "folder",
simpleType: "directory",
};
}
if (mimeType.startsWith("image/")) {
return {
classes: "orange-icons material-icons",
materialIcon: "photo",
simpleType: "image",
};
}
if (
mimeType.startsWith("audio/") ||
mimeType === "application/vnd.google-apps.audio"
) {
return {
classes: "plum-icons material-icons",
materialIcon: "volume_up",
simpleType: "audio",
};
}
if (
mimeType.startsWith("video/") ||
mimeType === "application/vnd.google-apps.video"
) {
return {
classes: "skyblue-icons material-icons",
materialIcon: "movie",
simpleType: "video",
};
}
if (mimeType.startsWith("font/")) {
return {
classes: "gray-icons material-icons",
materialIcon: "font_download",
simpleType: "font",
};
}
if (
mimeType === "application/zip" ||
mimeType === "application/x-7z-compressed" ||
mimeType === "application/x-bzip" ||
mimeType === "application/x-rar-compressed" ||
mimeType === "application/x-tar" ||
mimeType === "application/gzip" ||
mimeType === "application/x-xz" ||
mimeType === "application/x-zip-compressed" ||
mimeType === "application/x-gzip"
) {
return {
classes: "tan-icons material-icons",
materialIcon: "archive",
simpleType: "archive",
};
}
if (mimeType === "application/pdf") {
return {
classes: "red-icons material-icons",
materialIcon: "picture_as_pdf",
simpleType: "pdf",
};
}
if (
mimeType === "application/msword" ||
mimeType ===
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" ||
mimeType === "application/vnd.google-apps.document" ||
mimeType === "text/rtf" ||
mimeType === "application/rtf"
) {
return {
classes: "deep-blue-icons material-icons",
materialIcon: "description",
simpleType: "document",
};
}
if (
mimeType === "application/vnd.ms-excel" ||
mimeType ===
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
mimeType === "application/vnd.google-apps.spreadsheet"
) {
return {
classes: "green-icons material-icons",
materialIcon: "border_all",
simpleType: "document",
};
}
if (mimeType === "text/csv") {
return {
classes: "green-icons material-icons",
materialIcon: "border_all",
simpleType: "document",
};
}
if (
mimeType === "application/vnd.ms-powerpoint" ||
mimeType ===
"application/vnd.openxmlformats-officedocument.presentationml.presentation" ||
mimeType === "application/vnd.google-apps.presentation"
) {
return {
classes: "red-orange-icons material-icons",
materialIcon: "slideshow",
simpleType: "document",
};
}
if (mimeType === "text/plain" || mimeType === "text/markdown") {
return {
classes: "beige-icons material-icons",
materialIcon: "description",
simpleType: "text",
};
}
if (mimeType === "application/json" || mimeType === "application/xml") {
return {
classes: "yellow-icons material-icons",
materialIcon: "code",
simpleType: "text",
};
}
if (
mimeType === "application/octet-stream" ||
mimeType === "application/x-executable" ||
mimeType === "application/vnd.google-apps.unknown"
) {
return {
classes: "gray-icons material-icons",
materialIcon: "memory",
simpleType: "binary",
};
}
if (mimeType === "application/javascript" || mimeType === "text/javascript") {
return {
classes: "yellow-icons material-symbols-outlined",
materialIcon: "javascript",
simpleType: "text",
};
}
if (
mimeType === "application/x-python" ||
mimeType === "text/html" ||
mimeType === "text/css" ||
mimeType === "application/vnd.google-apps.sites"
) {
return {
classes: "gray-icons material-symbols-outlined",
materialIcon: "code_blocks",
simpleType: "text",
};
}
if (
mimeType === "application/x-disk-image" ||
mimeType === "application/x-iso-image" ||
mimeType === "application/x-apple-diskimage"
) {
return {
classes: "gray-icons material-symbols-outlined",
materialIcon: "deployed_code",
simpleType: "binary",
};
}
if (mimeType === "invalid_link") {
return {
classes: "lightgray-icons material-icons",
materialIcon: "link_off",
simpleType: "invalid_link",
};
}
// Default fallback
return {
classes: "lightgray-icons material-icons",
materialIcon: "description",
simpleType: "blob",
};
}

View File

@ -46,7 +46,7 @@ export default {
getApiPath getApiPath
}; };
export function removePrefix(path, prefix) { export function removePrefix(path, prefix = "") {
if (path === undefined) { if (path === undefined) {
return "" return ""
} }
@ -54,9 +54,12 @@ export function removePrefix(path, prefix) {
prefix = "/" + trimSlashes(prefix) prefix = "/" + trimSlashes(prefix)
} }
const combined = trimSlashes(baseURL) + prefix const combined = trimSlashes(baseURL) + prefix
const combined2 = "/" + combined
// Remove combined (baseURL + prefix) from the start of the path if present // Remove combined (baseURL + prefix) from the start of the path if present
if (path.startsWith(combined)) { if (path.startsWith(combined)) {
path = path.slice(combined.length); path = path.slice(combined.length);
} else if (path.startsWith(combined2)) {
path = path.slice(combined2.length);
} else if (path.startsWith(prefix)) { } else if (path.startsWith(prefix)) {
// Fallback: remove only the prefix if the combined string isn't present // Fallback: remove only the prefix if the combined string isn't present
path = path.slice(prefix.length); path = path.slice(prefix.length);
@ -110,3 +113,7 @@ export function removeLeadingSlash(str) {
export function trimSlashes(str) { export function trimSlashes(str) {
return removeLeadingSlash(removeTrailingSlash(str)) return removeLeadingSlash(removeTrailingSlash(str))
} }
export function base64Encode(str) {
return btoa(unescape(encodeURIComponent(str)));
}

View File

@ -24,7 +24,7 @@ import Preview from "@/views/files/Preview.vue";
import ListingView from "@/views/files/ListingView.vue"; import ListingView from "@/views/files/ListingView.vue";
import Editor from "@/views/files/Editor.vue"; import Editor from "@/views/files/Editor.vue";
import { state, mutations, getters } from "@/store"; import { state, mutations, getters } from "@/store";
import { pathsMatch } from "@/utils/url.js"; import { url } from "@/utils";
import { notify } from "@/notify"; import { notify } from "@/notify";
//import { removePrefix } from "@/utils/url.js"; //import { removePrefix } from "@/utils/url.js";
@ -41,6 +41,8 @@ export default {
return { return {
error: null, error: null,
width: window.innerWidth, width: window.innerWidth,
lastPath: "",
lastHash: "",
}; };
}, },
computed: { computed: {
@ -66,6 +68,7 @@ export default {
}, },
}, },
mounted() { mounted() {
window.addEventListener("hashchange", this.scrollToHash);
window.addEventListener("keydown", this.keyEvent); window.addEventListener("keydown", this.keyEvent);
}, },
beforeUnmount() { beforeUnmount() {
@ -75,11 +78,26 @@ export default {
mutations.replaceRequest({}); // Use mutation mutations.replaceRequest({}); // Use mutation
}, },
methods: { methods: {
scrollToHash() {
if (window.location.hash === this.lastHash) return;
this.lastHash = window.location.hash
if (window.location.hash) {
const id = url.base64Encode(window.location.hash.slice(1));
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({
behavior: "instant",
block: "center",
});
}
}
},
async fetchData() { async fetchData() {
if (state.route.path === this.lastPath) return;
this.lastHash = ""
// Set loading to true and reset the error. // Set loading to true and reset the error.
mutations.setLoading("files", true); mutations.setLoading("files", true);
this.error = null; this.error = null;
// Reset view information using mutations // Reset view information using mutations
mutations.setReload(false); mutations.setReload(false);
mutations.resetSelected(); mutations.resetSelected();
@ -94,14 +112,14 @@ export default {
if (res.type != "directory") { if (res.type != "directory") {
let content = false; let content = false;
// only check content for blob or text files // only check content for blob or text files
if (res.type == "blob" || res.type == "text") { if (res.type.startsWith("application") || res.type.startsWith("text")) {
content = true; content = true;
} }
res = await filesApi.fetchFiles(getters.routePath(), content); res = await filesApi.fetchFiles(getters.routePath(), content);
} }
data = res; data = res;
// Verify if the fetched path matches the current route // Verify if the fetched path matches the current route
if (pathsMatch(res.path, `/${state.route.params.path}`)) { if (url.pathsMatch(res.path, `/${state.route.params.path}`)) {
document.title = `${res.name} - ${document.title}`; document.title = `${res.name} - ${document.title}`;
} }
} catch (e) { } catch (e) {
@ -112,6 +130,10 @@ export default {
mutations.replaceRequest(data); mutations.replaceRequest(data);
mutations.setLoading("files", false); mutations.setLoading("files", false);
} }
setTimeout(() => {
this.scrollToHash();
}, 25);
this.lastPath = state.route.path;
}, },
keyEvent(event) { keyEvent(event) {
// F1! // F1!

View File

@ -115,7 +115,6 @@ export default {
if (!getters.isLoggedIn()) { if (!getters.isLoggedIn()) {
return; return;
} }
mutations.resetSelected();
mutations.setMultiple(false); mutations.setMultiple(false);
if (getters.currentPromptName() !== "success") { if (getters.currentPromptName() !== "success") {
mutations.closeHovers(); mutations.closeHovers();
@ -155,29 +154,10 @@ main {
} }
main.moveWithSidebar { main.moveWithSidebar {
padding-left: 21em; padding-left: 20.5em;
} }
main::-webkit-scrollbar { main::-webkit-scrollbar {
display: none; /* Safari and Chrome */ display: none; /* Safari and Chrome */
} }
/* Use the class .dark-mode to apply styles conditionally */
.dark-mode {
background: var(--background) !important;
color: var(--textPrimary);
}
/* Header */
.dark-mode-header {
color: white;
background-color: rgb(255 255 255 / 50%) !important;
}
/* 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);
}
}
</style> </style>

View File

@ -149,6 +149,8 @@ import QrcodeVue from "qrcode.vue";
import Item from "@/components/files/ListingItem.vue"; import Item from "@/components/files/ListingItem.vue";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
import { state, getters, mutations } from "@/store"; import { state, getters, mutations } from "@/store";
import { url } from "@/utils";
import { getTypeInfo } from "@/utils/mimetype";
export default { export default {
name: "share", name: "share",
@ -209,12 +211,11 @@ export default {
}, },
icon() { icon() {
if (state.req.type == "directory") return "folder"; if (state.req.type == "directory") return "folder";
if (state.req.type === "image") return "insert_photo"; if (getTypeInfo(state.req.type).simpleType == "image") return "insert_photo";
if (state.req.type === "audio") return "volume_up"; if (getTypeInfo(state.req.type).simpleType == "audio") return "volume_up";
if (state.req.type === "video") return "movie"; if (getTypeInfo(state.req.type).simpleType == "video") return "movie";
return "insert_drive_file"; return "insert_drive_file";
}, },
humanSize() { humanSize() {
if (state.req.type == "directory") { if (state.req.type == "directory") {
return state.req.items.length; return state.req.items.length;
@ -229,10 +230,13 @@ export default {
return new Date(Date.parse(state.req.modified)).toLocaleString(); return new Date(Date.parse(state.req.modified)).toLocaleString();
}, },
isImage() { isImage() {
return state.req.type === "image"; return getTypeInfo(state.req.type).simpleType === "image";
}, },
isMedia() { isMedia() {
return state.req.type === "video" || state.req.type === "audio"; return (
getTypeInfo(state.req.type).simpleType === "video" ||
getTypeInfo(state.req.type).simpleType === "audio"
);
}, },
}, },
methods: { methods: {
@ -245,7 +249,7 @@ export default {
}); });
}, },
base64(name) { base64(name) {
return window.btoa(unescape(encodeURIComponent(name))); return url.base64Encode(name);
}, },
async fetchData() { async fetchData() {
let urlPath = getters.routePath("share"); let urlPath = getters.routePath("share");
@ -298,7 +302,7 @@ export default {
download() { download() {
if (getters.isSingleFileSelected()) { if (getters.isSingleFileSelected()) {
const share = { const share = {
path: his.subPath, path: this.subPath,
hash: this.hash, hash: this.hash,
token: this.token, token: this.token,
format: null, format: null,

View File

@ -15,7 +15,7 @@
</style> </style>
<script> <script>
import url from "@/utils/url.js"; import { url } from "@/utils";
import router from "@/router"; import router from "@/router";
import { state, mutations, getters } from "@/store"; import { state, mutations, getters } from "@/store";
import { filesApi } from "@/api"; import { filesApi } from "@/api";
@ -227,7 +227,7 @@ export default {
mutations.closeHovers(); mutations.closeHovers();
}, },
base64(name) { base64(name) {
return window.btoa(unescape(encodeURIComponent(name))); return url.base64Encode(name);
}, },
keyEvent(event) { keyEvent(event) {
// No prompts are shown // No prompts are shown

Some files were not shown because too many files have changed in this diff Show More