mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-15 05:31:59 -05:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e68db9858 | ||
|
|
34f6bea461 | ||
|
|
1e35473998 | ||
|
|
bd4d108b58 | ||
|
|
288ea8f59b | ||
|
|
f18c388c88 | ||
|
|
e922a92aa6 | ||
|
|
237769fa9e | ||
|
|
9a33e7b379 | ||
|
|
cd3e111e39 | ||
|
|
9cc30f25e4 | ||
|
|
7ab400eb7f | ||
|
|
4a4eb7ce31 | ||
|
|
f864790175 | ||
|
|
6494ca1991 |
No files matched your search
@@ -6,7 +6,32 @@ on:
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
test_frontend:
|
||||
name: Test Playwright
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i && npm run build
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go build -o filebrowser .
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.playwright
|
||||
push: false
|
||||
push_latest_to_registry:
|
||||
needs: [ test_frontend ]
|
||||
name: Push latest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -14,9 +39,9 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Find latest tag
|
||||
- name: Find latest beta tag
|
||||
run: |
|
||||
echo "LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV
|
||||
echo "LATEST_TAG=$(git describe --tags $(git rev-list --tags="*-beta*" --max-count=1))" >> $GITHUB_ENV
|
||||
echo "latest tag is $LATEST_TAG"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
+28
-19
@@ -1,28 +1,37 @@
|
||||
name: pr-request
|
||||
name: PR Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "dev/v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "beta/v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "stable/v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
jobs:
|
||||
#test_frontend:
|
||||
# name: Push release
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v4
|
||||
# - name: Set up QEMU
|
||||
# uses: docker/setup-qemu-action@v3.0.0
|
||||
# - name: Set up Docker Buildx
|
||||
# uses: docker/setup-buildx-action@v3.0.0
|
||||
# - name: Build
|
||||
# uses: docker/build-push-action@v6
|
||||
# with:
|
||||
# context: .
|
||||
# file: ./Dockerfile.playwright
|
||||
# push: false
|
||||
test_frontend:
|
||||
name: Test Playwright
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i && npm run build
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go build -o filebrowser .
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.playwright
|
||||
push: false
|
||||
push_pr_to_registry:
|
||||
name: Push PR
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
name: dev tests
|
||||
name: regular tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "*"
|
||||
- "**"
|
||||
|
||||
jobs:
|
||||
test-backend:
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
name: beta release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "beta/v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
test_frontend:
|
||||
name: Test Playwright
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i && npm run build
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go build -o filebrowser .
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.playwright
|
||||
push: false
|
||||
create_release_tag:
|
||||
needs: [ test_frontend ]
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
run: |
|
||||
original_branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
|
||||
echo "branch_name=$transformed_branch" >> $GITHUB_OUTPUT
|
||||
tag_name=$(echo "$original_branch" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+')-beta
|
||||
echo "tag_name=$tag_name" >> $GITHUB_OUTPUT
|
||||
id: extract_branch
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
target_commitish: ${{ steps.extract_branch.outputs.branch_name }}
|
||||
token: ${{ secrets.PAT }}
|
||||
tag_name: ${{ steps.extract_branch.outputs.tag_name }}
|
||||
prerelease: false # change this to false when stable gets released
|
||||
make_latest: true # change this to false when stable gets released
|
||||
draft: false
|
||||
generate_release_notes: true
|
||||
name: ${{ steps.extract_branch.outputs.tag_name }}
|
||||
|
||||
push_release_to_registry:
|
||||
needs: [ test_frontend ]
|
||||
name: Push release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: gtstef/filebrowser
|
||||
- name: Strip v from version number
|
||||
id: modify-json
|
||||
run: |
|
||||
JSON="${{ steps.meta.outputs.tags }}"
|
||||
# Use jq to remove 'v' from the version field
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:beta\/v/filebrowser:beta_v/')
|
||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
build-args: |
|
||||
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
||||
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -3,7 +3,7 @@ name: dev release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "dev_v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "dev/v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
run: |
|
||||
JSON="${{ steps.meta.outputs.tags }}"
|
||||
# Use jq to remove 'v' from the version field
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:dev\/v/filebrowser:dev_v/')
|
||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
|
||||
@@ -1,15 +1,40 @@
|
||||
name: version release
|
||||
name: stable release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "stable/v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create_release:
|
||||
test_frontend:
|
||||
name: Test Playwright
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
- uses: actions/setup-node@v4
|
||||
- working-directory: frontend
|
||||
run: npm i && npm run build
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go build -o filebrowser .
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.playwright
|
||||
push: false
|
||||
create_release_tag:
|
||||
needs: [ test_frontend ]
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -19,7 +44,11 @@ jobs:
|
||||
token: ${{ secrets.PAT }}
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
|
||||
run: |
|
||||
original_branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
|
||||
echo "branch_name=$transformed_branch" >> $GITHUB_OUTPUT
|
||||
tag_name=$(echo "$original_branch" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+')-stable
|
||||
echo "tag_name=$tag_name" >> $GITHUB_OUTPUT
|
||||
id: extract_branch
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
@@ -27,15 +56,16 @@ jobs:
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
target_commitish: ${{ steps.extract_branch.outputs.branch }}
|
||||
target_commitish: ${{ steps.extract_branch.outputs.branch_name }}
|
||||
token: ${{ secrets.PAT }}
|
||||
tag_name: ${{ steps.extract_branch.outputs.branch }}
|
||||
tag_name: ${{ steps.extract_branch.outputs.tag_name }}
|
||||
prerelease: false
|
||||
draft: false
|
||||
generate_release_notes: true
|
||||
name: ${{ steps.extract_branch.outputs.branch }}
|
||||
name: ${{ steps.extract_branch.outputs.tag_name }}
|
||||
|
||||
push_release_to_registry:
|
||||
needs: [ test_frontend ]
|
||||
name: Push release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -60,7 +90,7 @@ jobs:
|
||||
run: |
|
||||
JSON="${{ steps.meta.outputs.tags }}"
|
||||
# Use jq to remove 'v' from the version field
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:stable\/v/filebrowser:/')
|
||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -1,9 +1,9 @@
|
||||
name: tag
|
||||
name: tag update
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
- "v[0-9]+.[0-9]+.[0-9]+*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -8,6 +8,7 @@ rice-box.go
|
||||
/frontend/dist
|
||||
/frontend/pkg
|
||||
/frontend/test-results
|
||||
/frontend/loginAuth.json
|
||||
/frontend/package-lock.json
|
||||
/backend/vendor
|
||||
/backend/*.cov
|
||||
|
||||
@@ -2,6 +2,33 @@
|
||||
|
||||
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.7-beta
|
||||
|
||||
**Notes**:
|
||||
- Adding windows builds back to automated process... will replace manually if they throw malicious defender warnings.
|
||||
- Adding playwright tests to all pr's against dev/beta/release branches.
|
||||
- These playwright tests should help keep release more reliably stable.
|
||||
|
||||
**Bugfixes**:
|
||||
- closing with the default bar issue.
|
||||
- tar.gz archive creation issue
|
||||
|
||||
## v0.3.6-beta
|
||||
|
||||
**New Features**
|
||||
- Adds "externalUrl" server config https://github.com/gtsteffaniak/filebrowser/issues/272
|
||||
|
||||
**Notes**:
|
||||
- All views modes to show header bar for sorting.
|
||||
- other small style changes
|
||||
|
||||
**Bugfixes**:
|
||||
- select and info bug after sorting https://github.com/gtsteffaniak/filebrowser/issues/277
|
||||
- downloading from shares with public user
|
||||
- Ctrl and Shift key modifiers work on listing views as expected.
|
||||
- copy/move file/folder error and show errors https://github.com/gtsteffaniak/filebrowser/issues/278
|
||||
- file move/copy context fix.
|
||||
|
||||
## v0.3.5
|
||||
|
||||
**New Features**
|
||||
|
||||
+4
-18
@@ -1,19 +1,5 @@
|
||||
FROM golang:1.23-alpine AS base
|
||||
FROM gtstef/playwright-base
|
||||
WORKDIR /app
|
||||
COPY ./backend ./
|
||||
RUN go build -ldflags="-w -s" -o filebrowser .
|
||||
|
||||
FROM node:slim
|
||||
WORKDIR /app
|
||||
COPY ./frontend/package.json ./
|
||||
RUN npm i --maxsockets 1
|
||||
RUN npx playwright install --with-deps firefox
|
||||
COPY [ "backend/config.yaml", "./" ]
|
||||
COPY ./frontend/ ./frontend
|
||||
WORKDIR /app/frontend
|
||||
RUN npm run build-docker
|
||||
WORKDIR /app
|
||||
COPY --from=base /app/filebrowser* ./
|
||||
RUN cp -R frontend/tests/ srv
|
||||
ENV FILEBROWSER_NO_EMBEDED="true"
|
||||
RUN ./filebrowser & sleep 2 && cd frontend && npx playwright test
|
||||
COPY [ "./backend/filebrowser*", "./"]
|
||||
COPY [ "./frontend/", "./" ]
|
||||
RUN ./filebrowser -c filebrowser-playwright.yaml & sleep 2 && npx playwright test
|
||||
@@ -0,0 +1,5 @@
|
||||
FROM node:22-slim
|
||||
WORKDIR /app
|
||||
COPY ./frontend/package.json ./
|
||||
RUN npm i @playwright/test
|
||||
RUN npx playwright install --with-deps firefox
|
||||
@@ -9,11 +9,9 @@
|
||||
<img width="800" src="https://github.com/user-attachments/assets/b16acd67-0292-437a-a06c-bc83f95758e6" title="Main Screenshot">
|
||||
</p>
|
||||
|
||||
> [!Note]
|
||||
> Starting with v0.3.3, configuration file mapping is different to support non-root user. Now, the default config file name is `config.yaml` and in docker the path is `/home/filebrowser/config.yaml` and `/home/filebrowser/<database_file>`. Please read the usage below to properly update your config to point the new config location. (open an issue for any help needed)
|
||||
|
||||
> [!WARNING]
|
||||
> There is no stable version yet. 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.
|
||||
> (planned for later this year after these are complete: multiple sources support, initial onboarding page, official automated docs website)
|
||||
|
||||
FileBrowser Quantum is a fork of the file browser open-source project with the following changes:
|
||||
|
||||
@@ -155,3 +153,6 @@ Starred/pinned files | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||
Content preview icons | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ |
|
||||
Plugins support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
Chromecast support | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
Share collections of files | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
Can archive selected files | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
Can browse archive files | ❌ | ❌ | ❌ | ❌ | ❌ | ✅
|
||||
@@ -21,17 +21,20 @@ builds:
|
||||
- "7"
|
||||
hooks:
|
||||
post:
|
||||
- upx {{ .Path }} # Compress the binary with UPX
|
||||
- upx --force-macos {{ .Path }} # Compress the binary with UPX
|
||||
|
||||
# Build configuration for windows without arm
|
||||
# - id: windows
|
||||
# ldflags: *ldflags
|
||||
# main: main.go
|
||||
# binary: filebrowser
|
||||
# goos:
|
||||
# - windows
|
||||
# goarch:
|
||||
# - amd64
|
||||
- id: windows
|
||||
ldflags: *ldflags
|
||||
main: main.go
|
||||
binary: filebrowser
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
hooks:
|
||||
post:
|
||||
- upx {{ .Path }} # Compress the binary with UPX
|
||||
|
||||
archives:
|
||||
- name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
server:
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
root: "./tests/playwright-files"
|
||||
auth:
|
||||
method: password
|
||||
signup: false
|
||||
userDefaults:
|
||||
darkMode: true
|
||||
disableSettings: false
|
||||
scope: "."
|
||||
hideDotfiles: true
|
||||
singleClick: false
|
||||
permissions:
|
||||
admin: false
|
||||
create: false
|
||||
rename: false
|
||||
modify: false
|
||||
delete: false
|
||||
share: false
|
||||
download: false
|
||||
@@ -189,7 +189,7 @@ func MoveResource(source, realsrc, realdst string, isSrcDir bool) error {
|
||||
index := GetIndex(source)
|
||||
// refresh info for source and dest
|
||||
err = index.RefreshFileInfo(FileOptions{
|
||||
Path: realsrc,
|
||||
Path: filepath.Dir(realsrc),
|
||||
IsDir: isSrcDir,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -74,7 +73,6 @@ func Test_GetRealPath(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
realPath, isDir, _ := idx.GetRealPath(tt.paths...)
|
||||
fmt.Println(realPath, trimPrefix)
|
||||
adjustedRealPath := strings.TrimPrefix(realPath, trimPrefix)
|
||||
if tt.want.path != adjustedRealPath || tt.want.isDir != isDir {
|
||||
t.Errorf("expected %v:%v but got: %v:%v", tt.want.path, tt.want.isDir, adjustedRealPath, isDir)
|
||||
|
||||
@@ -192,7 +192,6 @@ func (idx *Index) indexDirectory(adjustedPath string, quick, recursive bool) err
|
||||
}
|
||||
|
||||
func (idx *Index) makeIndexPath(subPath string) string {
|
||||
subPath = strings.ReplaceAll(subPath, "\\", "/")
|
||||
if strings.HasPrefix(subPath, "./") {
|
||||
subPath = strings.TrimPrefix(subPath, ".")
|
||||
}
|
||||
@@ -203,6 +202,7 @@ func (idx *Index) makeIndexPath(subPath string) string {
|
||||
subPath = strings.TrimSuffix(subPath, "/")
|
||||
// remove index prefix
|
||||
adjustedPath := strings.TrimPrefix(subPath, idx.Source.Path)
|
||||
adjustedPath = strings.ReplaceAll(adjustedPath, "\\", "/")
|
||||
// remove trailing slash
|
||||
adjustedPath = strings.TrimSuffix(adjustedPath, "/")
|
||||
if !strings.HasPrefix(adjustedPath, "/") {
|
||||
@@ -250,7 +250,6 @@ func (idx *Index) RefreshFileInfo(opts FileOptions) error {
|
||||
Path: opts.Path,
|
||||
IsDir: opts.IsDir,
|
||||
}
|
||||
|
||||
if !refreshOptions.IsDir {
|
||||
refreshOptions.Path = idx.makeIndexPath(filepath.Dir(refreshOptions.Path))
|
||||
refreshOptions.IsDir = true
|
||||
|
||||
@@ -91,9 +91,6 @@ func TestMakeIndexPath(t *testing.T) {
|
||||
{"Trailing slash removed", "/test/", "/test"},
|
||||
{"Subpath without root prefix", "/other/test", "/other/test"},
|
||||
{"Complex nested paths", "/nested/path", "/nested/path"},
|
||||
// Windows
|
||||
{"Mixed slash", "/first\\second", "/first/second"},
|
||||
{"Windows slash", "\\first\\second", "/first/second"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -105,6 +102,27 @@ func TestMakeIndexPath(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
tests = []struct {
|
||||
name string
|
||||
subPath string
|
||||
expected string
|
||||
}{
|
||||
// Windows
|
||||
{"Mixed slash", "/first\\second", "/first/second"},
|
||||
{"Windows slash", "\\first\\second", "/first/second"},
|
||||
{"Windows full path", "C:\\Users\\testfolder\\nestedfolder", "/testfolder/nestedfolder"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
idx := &Index{Source: settings.Source{Path: "C:\\Users"}}
|
||||
result := idx.makeIndexPath(tt.subPath)
|
||||
if result != tt.expected {
|
||||
t.Errorf("makeIndexPath(%q)\ngot %q\nwant %q", tt.name, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeIndexPathRoot(t *testing.T) {
|
||||
|
||||
@@ -26,9 +26,25 @@ func MoveFile(src, dst string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyFile copies a file from source to dest and returns
|
||||
// an error if any.
|
||||
// CopyFile copies a file or directory from source to dest and returns an error if any.
|
||||
func CopyFile(source, dest string) error {
|
||||
// Check if the source exists and whether it's a file or directory.
|
||||
info, err := os.Stat(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
// If the source is a directory, copy it recursively.
|
||||
return copyDirectory(source, dest)
|
||||
}
|
||||
|
||||
// If the source is a file, copy the file.
|
||||
return copySingleFile(source, dest)
|
||||
}
|
||||
|
||||
// copySingleFile handles copying a single file.
|
||||
func copySingleFile(source, dest string) error {
|
||||
// Open the source file.
|
||||
src, err := os.Open(source)
|
||||
if err != nil {
|
||||
@@ -36,7 +52,7 @@ func CopyFile(source, dest string) error {
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// Makes the directory needed to create the dst file.
|
||||
// Create the destination directory if needed.
|
||||
err = os.MkdirAll(filepath.Dir(dest), 0775) //nolint:gomnd
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -68,6 +84,43 @@ func CopyFile(source, dest string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyDirectory handles copying directories recursively.
|
||||
func copyDirectory(source, dest string) error {
|
||||
// Create the destination directory.
|
||||
err := os.MkdirAll(dest, 0775) //nolint:gomnd
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Read the contents of the source directory.
|
||||
entries, err := os.ReadDir(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Iterate over each entry in the directory.
|
||||
for _, entry := range entries {
|
||||
srcPath := filepath.Join(source, entry.Name())
|
||||
destPath := filepath.Join(dest, entry.Name())
|
||||
|
||||
if entry.IsDir() {
|
||||
// Recursively copy subdirectories.
|
||||
err = copyDirectory(srcPath, destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Copy files.
|
||||
err = copySingleFile(srcPath, destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommonPrefix returns the common directory path of provided files.
|
||||
func CommonPrefix(sep byte, paths ...string) string {
|
||||
// Handle special cases.
|
||||
|
||||
+4
-4
@@ -17,7 +17,7 @@ require (
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/swaggo/http-swagger v1.3.4
|
||||
github.com/swaggo/swag v1.16.4
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/image v0.23.0
|
||||
golang.org/x/text v0.21.0
|
||||
)
|
||||
@@ -43,9 +43,9 @@ require (
|
||||
github.com/swaggo/files v1.0.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.3.11 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/tools v0.28.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
+8
-8
@@ -114,8 +114,8 @@ go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
|
||||
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
@@ -133,8 +133,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
@@ -153,8 +153,8 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -168,8 +168,8 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
|
||||
+1
-18
@@ -15,7 +15,7 @@ import (
|
||||
func publicShareHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
file, ok := d.raw.(files.ExtendedFileInfo)
|
||||
if !ok {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type *files.FileInfo")
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type files.FileInfo")
|
||||
}
|
||||
file.Path = strings.TrimPrefix(file.Path, files.RootPaths["default"])
|
||||
return renderJSON(w, r, file)
|
||||
@@ -28,23 +28,6 @@ func publicUserGetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(status), status)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func publicDlHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
file, ok := d.raw.(files.ExtendedFileInfo)
|
||||
if !ok {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type files.FileInfo")
|
||||
}
|
||||
if d.user == nil {
|
||||
return http.StatusUnauthorized, fmt.Errorf("failed to get user")
|
||||
}
|
||||
|
||||
if file.Type == "directory" {
|
||||
return rawFilesHandler(w, r, d, []string{file.Path})
|
||||
}
|
||||
|
||||
return rawFileHandler(w, r, file.FileInfo)
|
||||
}
|
||||
|
||||
// health godoc
|
||||
|
||||
+46
-11
@@ -45,16 +45,25 @@ func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int,
|
||||
if !d.user.Perm.Download {
|
||||
return http.StatusAccepted, nil
|
||||
}
|
||||
|
||||
filePrefix := ""
|
||||
file, ok := d.raw.(files.ExtendedFileInfo)
|
||||
if ok {
|
||||
filePrefix = file.Path
|
||||
}
|
||||
encodedFiles := r.URL.Query().Get("files")
|
||||
// Decode the URL-encoded path
|
||||
files, err := url.QueryUnescape(encodedFiles)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
return rawFilesHandler(w, r, d, strings.Split(files, ","))
|
||||
fileList := strings.Split(files, ",")
|
||||
for i, f := range fileList {
|
||||
fileList[i] = filepath.Join(filePrefix, f)
|
||||
}
|
||||
return rawFilesHandler(w, r, d, fileList)
|
||||
}
|
||||
|
||||
func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *zip.Writer) error {
|
||||
func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *zip.Writer, flatten bool) error {
|
||||
idx := files.GetIndex("default")
|
||||
realPath, _, _ := idx.GetRealPath(d.user.Scope, path)
|
||||
if !d.user.Check(realPath) {
|
||||
@@ -65,16 +74,36 @@ func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *z
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the base name of the top-level folder or file
|
||||
baseName := filepath.Base(realPath)
|
||||
|
||||
if info.IsDir() {
|
||||
// Walk through directory contents
|
||||
return filepath.Walk(realPath, func(filePath string, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relPath, err := filepath.Rel(filepath.Dir(realPath), filePath) // Relative path including base dir
|
||||
|
||||
// Calculate the relative path
|
||||
relPath, err := filepath.Rel(realPath, filePath) // Use realPath directly
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Normalize for tar: convert \ to /
|
||||
relPath = filepath.ToSlash(relPath)
|
||||
|
||||
// Skip adding `.` (current directory)
|
||||
if relPath == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prepend base folder name unless flatten is true
|
||||
if !flatten {
|
||||
relPath = filepath.Join(baseName, relPath)
|
||||
relPath = filepath.ToSlash(relPath) // Ensure normalized separators
|
||||
}
|
||||
|
||||
if fileInfo.IsDir() {
|
||||
if tarWriter != nil {
|
||||
header := &tar.Header{
|
||||
@@ -95,8 +124,8 @@ func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *z
|
||||
})
|
||||
}
|
||||
|
||||
// Add a single file if it's not a directory
|
||||
return addSingleFile(realPath, filepath.Base(realPath), zipWriter, tarWriter)
|
||||
// For a single file, use the base name as the archive path
|
||||
return addSingleFile(realPath, baseName, zipWriter, tarWriter)
|
||||
}
|
||||
|
||||
func addSingleFile(realPath, archivePath string, zipWriter *zip.Writer, tarWriter *tar.Writer) error {
|
||||
@@ -112,11 +141,12 @@ func addSingleFile(realPath, archivePath string, zipWriter *zip.Writer, tarWrite
|
||||
}
|
||||
|
||||
if tarWriter != nil {
|
||||
header, err := tar.FileInfoHeader(info, realPath)
|
||||
header, err := tar.FileInfoHeader(info, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = archivePath
|
||||
// Ensure correct relative path
|
||||
header.Name = filepath.ToSlash(archivePath)
|
||||
if err = tarWriter.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -186,7 +216,8 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
if baseDirName == "" || baseDirName == "/" {
|
||||
baseDirName = "download"
|
||||
}
|
||||
downloadFileName := url.PathEscape(baseDirName + "." + algo)
|
||||
downloadFileName := url.PathEscape(baseDirName + extension)
|
||||
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+downloadFileName)
|
||||
// Create the archive and stream it directly to the response
|
||||
if extension == ".zip" {
|
||||
@@ -206,8 +237,10 @@ func createZip(w io.Writer, d *requestContext, filenames ...string) error {
|
||||
zipWriter := zip.NewWriter(w)
|
||||
defer zipWriter.Close()
|
||||
|
||||
// Check if we have exactly one directory
|
||||
//flatten := len(filenames) == 1
|
||||
for _, fname := range filenames {
|
||||
err := addFile(fname, d, nil, zipWriter)
|
||||
err := addFile(fname, d, nil, zipWriter, false)
|
||||
if err != nil {
|
||||
log.Printf("Failed to add %s to ZIP: %v", fname, err)
|
||||
}
|
||||
@@ -223,8 +256,10 @@ func createTarGz(w io.Writer, d *requestContext, filenames ...string) error {
|
||||
tarWriter := tar.NewWriter(gzWriter)
|
||||
defer tarWriter.Close()
|
||||
|
||||
// Check if we have exactly one directory
|
||||
//flatten := len(filenames) == 1
|
||||
for _, fname := range filenames {
|
||||
err := addFile(fname, d, tarWriter, nil)
|
||||
err := addFile(fname, d, tarWriter, nil, false)
|
||||
if err != nil {
|
||||
log.Printf("Failed to add %s to TAR.GZ: %v", fname, err)
|
||||
}
|
||||
|
||||
@@ -283,10 +283,10 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
return errToStatus(err), err
|
||||
}
|
||||
if !d.user.Check(src) || !d.user.Check(dst) {
|
||||
return http.StatusForbidden, nil
|
||||
return http.StatusForbidden, fmt.Errorf("forbidden: user rules deny access to source or destination")
|
||||
}
|
||||
if dst == "/" || src == "/" {
|
||||
return http.StatusForbidden, nil
|
||||
return http.StatusForbidden, fmt.Errorf("forbidden: source or destination is attempting to modify root")
|
||||
}
|
||||
|
||||
idx := files.GetIndex(source)
|
||||
@@ -307,7 +307,7 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
}
|
||||
// Permission for overwriting the file
|
||||
if overwrite && !d.user.Perm.Modify {
|
||||
return http.StatusForbidden, nil
|
||||
return http.StatusForbidden, fmt.Errorf("forbidden: user does not have permission to overwrite file")
|
||||
}
|
||||
err = d.RunHook(func() error {
|
||||
return patchAction(r.Context(), action, realSrc, realDest, d, fileCache, isSrcDir, source)
|
||||
|
||||
@@ -107,7 +107,7 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
|
||||
|
||||
// Public routes
|
||||
api.HandleFunc("GET /public/publicUser", publicUserGetHandler)
|
||||
api.HandleFunc("GET /public/dl", withHashFile(publicDlHandler))
|
||||
api.HandleFunc("GET /public/dl", withHashFile(rawHandler))
|
||||
api.HandleFunc("GET /public/share", withHashFile(publicShareHandler))
|
||||
|
||||
// Settings routes
|
||||
|
||||
@@ -63,6 +63,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
|
||||
"EnableExec": config.Server.EnableExec,
|
||||
"ReCaptchaHost": config.Auth.Recaptcha.Host,
|
||||
"ExternalLinks": config.Frontend.ExternalLinks,
|
||||
"ExternalUrl": strings.TrimSuffix(config.Server.ExternalUrl, "/"),
|
||||
}
|
||||
|
||||
if config.Frontend.Files != "" {
|
||||
|
||||
@@ -114,6 +114,9 @@ func setDefaults() Settings {
|
||||
Host: "",
|
||||
},
|
||||
},
|
||||
Frontend: Frontend{
|
||||
Name: "FileBrowser Quantum",
|
||||
},
|
||||
UserDefaults: UserDefaults{
|
||||
StickySidebar: true,
|
||||
Scope: ".",
|
||||
|
||||
@@ -53,6 +53,7 @@ type Server struct {
|
||||
UserHomeBasePath string `json:"userHomeBasePath"`
|
||||
CreateUserDir bool `json:"createUserDir"`
|
||||
Sources map[string]Source `json:"sources"`
|
||||
ExternalUrl string `json:"externalUrl"`
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Browser, firefox, expect, Page } from "@playwright/test";
|
||||
|
||||
|
||||
async function globalSetup() {
|
||||
const browser: Browser = await firefox.launch();
|
||||
const context = await browser.newContext();
|
||||
const page: Page = await context.newPage();
|
||||
await page.goto("http://127.0.0.1/login");
|
||||
await page.getByPlaceholder("Username").fill("admin");
|
||||
await page.getByPlaceholder("Password").fill("admin");
|
||||
await page.getByRole("button", { name: "Login" }).click();
|
||||
await page.waitForURL("**/files/", { timeout: 100 });
|
||||
await expect(page).toHaveTitle('FileBrowser Quantum - Files');
|
||||
let cookies = await context.cookies();
|
||||
expect(cookies.find((c) => c.name == "auth")?.value).toBeDefined();
|
||||
await page.context().storageState({ path: "./loginAuth.json" });
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
export default globalSetup
|
||||
@@ -17,10 +17,10 @@
|
||||
"lint": "eslint --ext .js,.vue,ts src",
|
||||
"lint:fix": "eslint --fix src/",
|
||||
"format": "prettier --write .",
|
||||
"integration-test": "npx playwright test",
|
||||
"test": "vitest run "
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": "^1.49.1",
|
||||
"ace-builds": "^1.24.2",
|
||||
"clipboard": "^2.0.4",
|
||||
"css-vars-ponyfill": "^2.4.3",
|
||||
@@ -34,7 +34,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"eslint": "^8.57.0",
|
||||
|
||||
@@ -10,6 +10,8 @@ import { defineConfig, devices } from "@playwright/test";
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
globalSetup: "./global-setup",
|
||||
timeout: 3000,
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
@@ -18,11 +20,13 @@ export default defineConfig({
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
workers: process.env.CI ? 1 : 1, // required for now! todo parallel some tests
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "line",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
actionTimeout: 500,
|
||||
storageState: "loginAuth.json",
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1",
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<script>
|
||||
window.FileBrowser = JSON.parse('{{ .globalVars }}');
|
||||
var dynamicManifest = {
|
||||
"name": window.FileBrowser.Name || '',
|
||||
"name": window.FileBrowser.Name || 'FileBrowser Quantum',
|
||||
"icons": [
|
||||
{
|
||||
"src": window.location.origin + "{{ .StaticURL }}/img/icons/android-chrome-256x256.png",
|
||||
|
||||
+18
-10
@@ -125,30 +125,38 @@ export async function post(url, content = "", overwrite = false, onupload) {
|
||||
}
|
||||
|
||||
export async function moveCopy(items, action = "copy", overwrite = false, rename = false) {
|
||||
let promises = [];
|
||||
let params = {
|
||||
overwrite: overwrite,
|
||||
action: action,
|
||||
rename: rename,
|
||||
}
|
||||
};
|
||||
try {
|
||||
for (let item of items) {
|
||||
// Create an array of fetch calls
|
||||
let promises = items.map((item) => {
|
||||
let toPath = encodeURIComponent(removePrefix(decodeURI(item.to), "files"));
|
||||
let fromPath = encodeURIComponent(removePrefix(decodeURI(item.from), "files"));
|
||||
let localParams = { ...params };
|
||||
localParams.destination = toPath;
|
||||
localParams.from = fromPath;
|
||||
let localParams = { ...params, destination: toPath, from: fromPath };
|
||||
const apiPath = getApiPath("api/resources", localParams);
|
||||
promises.push(fetch(apiPath, { method: "PATCH" }));
|
||||
}
|
||||
return promises;
|
||||
return fetch(apiPath, { method: "PATCH" }).then((response) => {
|
||||
if (!response.ok) {
|
||||
// Throw an error if the fetch fails
|
||||
return response.text().then((text) => {
|
||||
throw new Error(`Failed to move/copy: ${text || response.statusText}`);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
});
|
||||
});
|
||||
|
||||
// Await all promises and ensure errors propagate
|
||||
await Promise.all(promises);
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error moving/copying resources");
|
||||
throw err;
|
||||
throw err; // Re-throw the error to propagate it back to the caller
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function checksum(url, algo) {
|
||||
try {
|
||||
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { fetchURL, fetchJSON, adjustedData } from "./utils";
|
||||
import { notify } from "@/notify";
|
||||
import { getApiPath } from "@/utils/url.js";
|
||||
import { externalUrl } from "@/utils/constants";
|
||||
|
||||
export async function list() {
|
||||
const apiPath = getApiPath("api/shares");
|
||||
@@ -41,5 +42,8 @@ export async function create(path, password = "", expires = "", unit = "hours")
|
||||
}
|
||||
|
||||
export function getShareURL(share) {
|
||||
if (externalUrl) {
|
||||
return externalUrl+getApiPath(`share/${share.hash}`);
|
||||
}
|
||||
return window.origin+getApiPath(`share/${share.hash}`);
|
||||
}
|
||||
@@ -67,8 +67,7 @@ export function adjustedData(data, url) {
|
||||
// Combine folders and files into items
|
||||
data.items = [...(data.folders || []), ...(data.files || [])];
|
||||
|
||||
data.items = data.items.map((item, index) => {
|
||||
item.index = index;
|
||||
data.items = data.items.map((item) => {
|
||||
item.url = `${data.url}${item.name}`;
|
||||
if (item.type === "directory") {
|
||||
item.url += "/";
|
||||
|
||||
@@ -23,10 +23,10 @@ describe('adjustedData', () => {
|
||||
folders: [],
|
||||
files: [],
|
||||
items: [
|
||||
{ name: "folder1", type: "directory", index: 0, url: "http://example.com/unit-testing/files/path/to/directory/folder1/" },
|
||||
{ name: "folder2", type: "directory", index: 1, url: "http://example.com/unit-testing/files/path/to/directory/folder2/" },
|
||||
{ name: "file1.txt", type: "file", index: 2, url: "http://example.com/unit-testing/files/path/to/directory/file1.txt" },
|
||||
{ name: "file2.txt", type: "file", index: 3, url: "http://example.com/unit-testing/files/path/to/directory/file2.txt" },
|
||||
{ name: "folder1", type: "directory", url: "http://example.com/unit-testing/files/path/to/directory/folder1/" },
|
||||
{ name: "folder2", type: "directory", url: "http://example.com/unit-testing/files/path/to/directory/folder2/" },
|
||||
{ name: "file1.txt", type: "file", url: "http://example.com/unit-testing/files/path/to/directory/file1.txt" },
|
||||
{ name: "file2.txt", type: "file", url: "http://example.com/unit-testing/files/path/to/directory/file2.txt" },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ export default {
|
||||
return state.user;
|
||||
},
|
||||
centered() {
|
||||
return getters.isMobile() || ( !this.posX || !this.posY );
|
||||
return getters.isMobile() || !this.posX || !this.posY;
|
||||
},
|
||||
showContext() {
|
||||
if (getters.currentPromptName() == "ContextMenu" && state.prompts != []) {
|
||||
|
||||
@@ -179,7 +179,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
handleTouchMove(event) {
|
||||
if (!state.isSafari) return
|
||||
if (!state.isSafari) return;
|
||||
const touch = event.touches[0];
|
||||
const deltaX = Math.abs(touch.clientX - this.touchStartX);
|
||||
const deltaY = Math.abs(touch.clientY - this.touchStartY);
|
||||
@@ -191,7 +191,7 @@ export default {
|
||||
}
|
||||
},
|
||||
handleTouchEnd() {
|
||||
if (!state.isSafari) return
|
||||
if (!state.isSafari) return;
|
||||
this.cancelContext(); // Clear timeout
|
||||
this.isSwipe = false; // Reset swipe state
|
||||
},
|
||||
@@ -214,8 +214,8 @@ export default {
|
||||
},
|
||||
onRightClick(event) {
|
||||
event.preventDefault(); // Prevent default context menu
|
||||
// If no items are selected, select the right-clicked item
|
||||
if (!state.multiple) {
|
||||
// If one or fewer items are selected, reset the selection
|
||||
if (!state.multiple && getters.selectedCount() < 2) {
|
||||
mutations.resetSelected();
|
||||
mutations.addSelected(this.index);
|
||||
}
|
||||
@@ -247,7 +247,9 @@ export default {
|
||||
getTime() {
|
||||
if (state.user.dateFormat) {
|
||||
// Truncate the fractional seconds to 3 digits (milliseconds)
|
||||
const sanitizedString = this.modified.replace(/\.\d+/, (match) => match.slice(0, 4));
|
||||
const sanitizedString = this.modified.replace(/\.\d+/, (match) =>
|
||||
match.slice(0, 4)
|
||||
);
|
||||
// Parse the sanitized string into a Date object
|
||||
const date = new Date(sanitizedString);
|
||||
return date.toLocaleString();
|
||||
@@ -333,7 +335,7 @@ export default {
|
||||
action(overwrite, rename);
|
||||
},
|
||||
addSelected(event) {
|
||||
if (!state.isSafari) return
|
||||
if (!state.isSafari) return;
|
||||
const touch = event.touches[0];
|
||||
this.touchStartX = touch.clientX;
|
||||
this.touchStartY = touch.clientY;
|
||||
@@ -357,7 +359,11 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.user.singleClick && getters.selectedCount() !== 0 && event.button === 0) {
|
||||
if (
|
||||
!state.user.singleClick &&
|
||||
getters.selectedCount() !== 0 &&
|
||||
event.button === 0
|
||||
) {
|
||||
event.preventDefault();
|
||||
}
|
||||
setTimeout(() => {
|
||||
@@ -393,7 +399,12 @@ export default {
|
||||
|
||||
return;
|
||||
}
|
||||
if (!state.user.singleClick && !event.ctrlKey && !event.metaKey && !state.multiple) {
|
||||
if (
|
||||
!state.user.singleClick &&
|
||||
!event.ctrlKey &&
|
||||
!event.metaKey &&
|
||||
!state.multiple
|
||||
) {
|
||||
mutations.resetSelected();
|
||||
}
|
||||
mutations.addSelected(this.index);
|
||||
@@ -411,4 +422,4 @@ export default {
|
||||
.item {
|
||||
-webkit-touch-callout: none; /* Disable the default long press preview */
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -52,7 +52,8 @@ import FileList from "./FileList.vue";
|
||||
import { filesApi } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
//import { notify } from "@/notify";
|
||||
import { removePrefix } from "@/utils/url";
|
||||
import { notify } from "@/notify";
|
||||
|
||||
export default {
|
||||
name: "copy",
|
||||
@@ -61,6 +62,7 @@ export default {
|
||||
return {
|
||||
current: window.location.pathname,
|
||||
dest: null,
|
||||
items: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -71,57 +73,63 @@ export default {
|
||||
return mutations.closeHovers();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
for (let item of state.selected) {
|
||||
this.items.push({
|
||||
from: state.req.items[item].url,
|
||||
// add to: dest
|
||||
name: state.req.items[item].name,
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copy: async function (event) {
|
||||
event.preventDefault();
|
||||
let items = [];
|
||||
try {
|
||||
// Define the action function
|
||||
let action = async (overwrite, rename) => {
|
||||
const loc = removePrefix(this.dest, "files");
|
||||
for (let item of this.items) {
|
||||
item.to = loc + "/" + item.name;
|
||||
}
|
||||
buttons.loading("copy");
|
||||
await filesApi.moveCopy(this.items, "copy", overwrite, rename);
|
||||
};
|
||||
// Fetch destination files
|
||||
let dstResp = await filesApi.fetchFiles(this.dest);
|
||||
let conflict = upload.checkConflict(this.items, dstResp.items);
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
// Create a new promise for each file.
|
||||
for (let item of state.selected) {
|
||||
items.push({
|
||||
from: state.req.items[item].url,
|
||||
to: this.dest + encodeURIComponent(state.req.items[item].name),
|
||||
name: state.req.items[item].name,
|
||||
});
|
||||
}
|
||||
|
||||
let action = async (overwrite, rename) => {
|
||||
buttons.loading("copy");
|
||||
await filesApi.moveCopy(items, "copy", overwrite, rename);
|
||||
this.$router.push({ path: this.dest });
|
||||
mutations.setReload(true);
|
||||
};
|
||||
|
||||
if (state.route.path === this.dest) {
|
||||
if (conflict) {
|
||||
await new Promise((resolve, reject) => {
|
||||
mutations.showHover({
|
||||
name: "replace-rename",
|
||||
confirm: async (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
event.preventDefault();
|
||||
try {
|
||||
await action(overwrite, rename);
|
||||
resolve(); // Resolve the promise if action succeeds
|
||||
} catch (e) {
|
||||
reject(e); // Reject the promise if an error occurs
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Await the action call for non-conflicting cases
|
||||
await action(overwrite, rename);
|
||||
}
|
||||
mutations.closeHovers();
|
||||
action(false, true);
|
||||
|
||||
return;
|
||||
notify.showSuccess("Successfully copied file/folder, redirecting...");
|
||||
setTimeout(() => {
|
||||
this.$router.push(this.dest);
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
notify.error(error);
|
||||
}
|
||||
|
||||
let dstItems = (await filesApi.fetchFiles(this.dest)).items;
|
||||
let conflict = upload.checkConflict(items, dstItems);
|
||||
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
if (conflict) {
|
||||
mutations.showHover({
|
||||
name: "replace-rename",
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
|
||||
event.preventDefault();
|
||||
mutations.closeHovers();
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
action(overwrite, rename);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<script>
|
||||
import { state, mutations } from "@/store";
|
||||
import url from "@/utils/url.js";
|
||||
import { removePrefix } from "@/utils/url.js";
|
||||
import { filesApi } from "@/api";
|
||||
|
||||
export default {
|
||||
@@ -40,7 +41,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
nav() {
|
||||
return decodeURIComponent(this.current);
|
||||
return removePrefix(decodeURIComponent(this.current), "files");
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -53,6 +53,7 @@ import { filesApi } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
import { notify } from "@/notify";
|
||||
import { removePrefix } from "@/utils/url";
|
||||
|
||||
export default {
|
||||
name: "move",
|
||||
@@ -61,6 +62,7 @@ export default {
|
||||
return {
|
||||
current: window.location.pathname,
|
||||
dest: null,
|
||||
items: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -71,50 +73,66 @@ export default {
|
||||
return mutations.closeHovers();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
for (let item of state.selected) {
|
||||
this.items.push({
|
||||
from: state.req.items[item].url,
|
||||
// add to: dest
|
||||
name: state.req.items[item].name,
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
move: async function (event) {
|
||||
event.preventDefault();
|
||||
let items = [];
|
||||
|
||||
for (let item of state.selected) {
|
||||
items.push({
|
||||
from: state.req.items[item].url,
|
||||
to: this.dest + state.req.items[item].name,
|
||||
name: state.req.items[item].name,
|
||||
});
|
||||
}
|
||||
let action = async (overwrite, rename) => {
|
||||
buttons.loading("move");
|
||||
await filesApi.moveCopy(items, "move", overwrite, rename);
|
||||
buttons.success("move");
|
||||
this.$router.push({ path: this.dest });
|
||||
mutations.closeHovers();
|
||||
};
|
||||
|
||||
let dstItems = (await filesApi.fetchFiles(this.dest)).items;
|
||||
let conflict = upload.checkConflict(items, dstItems);
|
||||
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
try {
|
||||
// Define the action function
|
||||
let action = async (overwrite, rename) => {
|
||||
const loc = removePrefix(this.dest, "files");
|
||||
for (let item of this.items) {
|
||||
item.to = loc + "/" + item.name;
|
||||
}
|
||||
buttons.loading("move");
|
||||
await filesApi.moveCopy(this.items, "move", overwrite, rename);
|
||||
};
|
||||
|
||||
// Fetch destination files
|
||||
let dstResp = await filesApi.fetchFiles(this.dest);
|
||||
let conflict = upload.checkConflict(this.items, dstResp.items);
|
||||
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
if (conflict) {
|
||||
mutations.showHover({
|
||||
name: "replace-rename",
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
event.preventDefault();
|
||||
action(overwrite, rename);
|
||||
},
|
||||
await new Promise((resolve, reject) => {
|
||||
mutations.showHover({
|
||||
name: "replace-rename",
|
||||
confirm: async (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
event.preventDefault();
|
||||
try {
|
||||
await action(overwrite, rename);
|
||||
resolve(); // Resolve the promise if action succeeds
|
||||
} catch (e) {
|
||||
reject(e); // Reject the promise if an error occurs
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
// Await the action call for non-conflicting cases
|
||||
await action(overwrite, rename);
|
||||
}
|
||||
action(overwrite, rename);
|
||||
mutations.closeHovers();
|
||||
notify.showSuccess("Successfully moved file/folder, redirecting...");
|
||||
setTimeout(() => {
|
||||
this.$router.push(this.dest);
|
||||
}, 1000);
|
||||
} catch (e) {
|
||||
// Catch any errors from action or other parts of the flow
|
||||
notify.showError(e);
|
||||
}
|
||||
return;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -93,14 +93,13 @@ export default {
|
||||
this.$router.push({ path: uri });
|
||||
} else if (!this.base) {
|
||||
const res = await filesApi.fetchFiles(url.removeLastDir(uri) + "/");
|
||||
mutations.updateRequest(res);
|
||||
mutations.replaceRequest(res);
|
||||
}
|
||||
|
||||
mutations.closeHovers();
|
||||
} catch (error) {
|
||||
notify.showError(error);
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -206,7 +206,12 @@ export default {
|
||||
if (isPermanent) {
|
||||
res = await shareApi.create(this.subpath, this.password);
|
||||
} else {
|
||||
res = await shareApi.create(this.subpath, this.password, this.time.toString(), this.unit);
|
||||
res = await shareApi.create(
|
||||
this.subpath,
|
||||
this.password,
|
||||
this.time.toString(),
|
||||
this.unit
|
||||
);
|
||||
}
|
||||
|
||||
this.links.push(res);
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<i class="material-icons">settings</i>
|
||||
</div>
|
||||
|
||||
<div class="inner-card">
|
||||
<i v-if="canLogout" @click="logout" class="material-icons">exit_to_app</i>
|
||||
<div class="inner-card logout-button" @click="logout" >
|
||||
<i v-if="canLogout" class="material-icons">exit_to_app</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -59,10 +59,10 @@ body.rtl #listingView {
|
||||
|
||||
#listingView .item i {
|
||||
font-size: 4em;
|
||||
margin-right: 0.1em;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
|
||||
#listingView .item img {
|
||||
width: 4em;
|
||||
height: 4em;
|
||||
@@ -177,11 +177,6 @@ body.rtl #listingView {
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
#listingView.gallery .size,
|
||||
#listingView.gallery .modified {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#listingView.compact {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
@@ -220,15 +215,7 @@ body.rtl #listingView {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#listingView.compact .header .name,
|
||||
#listingView.list .header .name,
|
||||
#listingView.list .item .name,
|
||||
#listingView.compact .item .name {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#listingView.compact .header .name,
|
||||
#listingView.list .header .name {
|
||||
#listingView .header .name {
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
|
||||
@@ -237,14 +224,23 @@ body.rtl #listingView {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#listingView.compact .header .size,
|
||||
#listingView.list .header .size,
|
||||
#listingView .header .size,
|
||||
#listingView.list .item .size,
|
||||
#listingView.compact .item .size {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
#listingView.compact .header i {
|
||||
#listingView .header .name,
|
||||
#listingView.list .item .name,
|
||||
#listingView.compact .item .name {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#listingView.normal .item .text {
|
||||
padding-left: 0.3em;
|
||||
}
|
||||
|
||||
#listingView .header i {
|
||||
font-size: 1.5em;
|
||||
vertical-align: middle;
|
||||
margin-left: .2em;
|
||||
@@ -275,25 +271,21 @@ body.rtl #listingView {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
#listingView.compact .name {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#listingView.compact .header span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#listingView.compact .header i {
|
||||
#listingView .header i {
|
||||
opacity: 0;
|
||||
transition: .1s ease all;
|
||||
}
|
||||
|
||||
#listingView.compact .header p:hover i,
|
||||
#listingView.compact .header .active i {
|
||||
#listingView .header p:hover i,
|
||||
#listingView .header .active i {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#listingView.compact .header .active {
|
||||
#listingView .header .active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -312,11 +304,28 @@ body.rtl #listingView {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
#listingView .item[aria-selected=true] {
|
||||
#listingView.compact .item[aria-selected=true],
|
||||
#listingView.normal .item[aria-selected=true],
|
||||
#listingView.list .item[aria-selected=true] {
|
||||
background: var(--primaryColor) !important;
|
||||
color: var(--item-selected) !important;
|
||||
}
|
||||
|
||||
#listingView.gallery .item[aria-selected=true] {
|
||||
border: 1em solid var(--primaryColor) !important;
|
||||
border-radius: 1em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#listingView.gallery .item[aria-selected=true] .text {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#listingView.gallery .item .size,
|
||||
#listingView.gallery .item .modified {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#listingView.list .item div:first-of-type {
|
||||
width: 3em;
|
||||
}
|
||||
@@ -337,13 +346,18 @@ body.rtl #listingView {
|
||||
}
|
||||
|
||||
#listingView .header {
|
||||
display: none;
|
||||
display: flex !important;
|
||||
background: white;
|
||||
border-radius: 1em;
|
||||
border: 1px solid rgba(0, 0, 0, .1);
|
||||
z-index: 999;
|
||||
padding: .85em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#listingView.list .header i {
|
||||
#listingView.list .header i,
|
||||
#listingView.normal .header i,
|
||||
#listingView.gallary .header i {
|
||||
font-size: 1.5em;
|
||||
vertical-align: middle;
|
||||
margin-left: .2em;
|
||||
@@ -351,12 +365,8 @@ body.rtl #listingView {
|
||||
|
||||
#listingView.compact .header,
|
||||
#listingView.list .header {
|
||||
display: flex !important;
|
||||
border-top-left-radius: 1em;
|
||||
border-top-right-radius: 1em;
|
||||
z-index: 999;
|
||||
padding: .85em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#listingView.list .item:first-child {
|
||||
@@ -383,7 +393,9 @@ body.rtl #listingView {
|
||||
display:flex;
|
||||
width: 100%;
|
||||
}
|
||||
#listingView.list .header {
|
||||
#listingView.list .header,
|
||||
#listingView.normal .header,
|
||||
#listingView.gallery .header {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
@@ -391,11 +403,11 @@ body.rtl #listingView {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#listingView.list .name {
|
||||
#listingView .name {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#listingView.list .header span {
|
||||
#listingView .header span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
@@ -130,19 +130,20 @@ router.beforeResolve(async (to, from, next) => {
|
||||
return next(false);
|
||||
}
|
||||
|
||||
if (state != null && state.user != null && !('username' in state.user)) {
|
||||
await validateLogin();
|
||||
}
|
||||
|
||||
// Set the page title using i18n
|
||||
const title = i18n.global.t(titles[to.name as keyof typeof titles]);
|
||||
document.title = title + " - " + name;
|
||||
document.title = name + " - " + title ;
|
||||
|
||||
// Update store with the current route
|
||||
mutations.setRoute(to);
|
||||
|
||||
// Handle auth requirements
|
||||
if (to.matched.some((record) => record.meta.requiresAuth)) {
|
||||
|
||||
if (state != null && state.user != null && !('username' in state.user)) {
|
||||
await validateLogin();
|
||||
}
|
||||
|
||||
if (!getters.isLoggedIn()) {
|
||||
next({
|
||||
path: "/login",
|
||||
|
||||
@@ -53,10 +53,6 @@ export const getters = {
|
||||
state.req.items.forEach((item) => {
|
||||
// Check if the item is a directory
|
||||
if (item.type == "directory") {
|
||||
// If hideDotfiles is enabled and the item is a dotfile, skip it
|
||||
if (state.user.hideDotfiles && item.name.startsWith(".")) {
|
||||
return;
|
||||
}
|
||||
// Otherwise, count this directory
|
||||
dirCount++;
|
||||
}
|
||||
@@ -69,10 +65,6 @@ export const getters = {
|
||||
state.req.items.forEach((item) => {
|
||||
// Check if the item is a directory
|
||||
if (item.type != "directory") {
|
||||
// If hideDotfiles is enabled and the item is a dotfile, skip it
|
||||
if (state.user.hideDotfiles && item.name.startsWith(".")) {
|
||||
return;
|
||||
}
|
||||
// Otherwise, count this directory
|
||||
fileCount++;
|
||||
}
|
||||
@@ -88,9 +80,6 @@ export const getters = {
|
||||
const files = [];
|
||||
|
||||
state.req.items.forEach((item) => {
|
||||
if (state.user.hideDotfiles && item.name.startsWith(".")) {
|
||||
return;
|
||||
}
|
||||
if (item.type == "directory") {
|
||||
dirs.push(item);
|
||||
} else {
|
||||
|
||||
@@ -140,16 +140,21 @@ export const mutations = {
|
||||
emitStateChanged();
|
||||
},
|
||||
addSelected: (value) => {
|
||||
console.log("addSelected", value)
|
||||
state.selected.push(value);
|
||||
emitStateChanged();
|
||||
},
|
||||
removeSelected: (value) => {
|
||||
console.log("removeSelected", value)
|
||||
|
||||
let i = state.selected.indexOf(value);
|
||||
if (i === -1) return;
|
||||
state.selected.splice(i, 1);
|
||||
emitStateChanged();
|
||||
},
|
||||
resetSelected: () => {
|
||||
console.log("resetSelected")
|
||||
|
||||
state.selected = [];
|
||||
mutations.setMultiple(false);
|
||||
emitStateChanged();
|
||||
@@ -196,17 +201,20 @@ export const mutations = {
|
||||
// Emit state change event
|
||||
emitStateChanged();
|
||||
},
|
||||
updateRequest: (value) => {
|
||||
const selectedItems = state.selected.map((i) => state.req.items[i]);
|
||||
state.oldReq = state.req;
|
||||
state.req = value;
|
||||
state.selected = [];
|
||||
if (!state.req?.items) return;
|
||||
state.selected = state.req.items
|
||||
.filter((item) => selectedItems.some((rItem) => rItem.url === item.url))
|
||||
.map((item) => item.index);
|
||||
},
|
||||
replaceRequest: (value) => {
|
||||
state.selected = [];
|
||||
if (!value?.items) {
|
||||
state.req = value;
|
||||
emitStateChanged();
|
||||
return
|
||||
}
|
||||
if (state.user.hideDotfiles) {
|
||||
value.items = value.items.filter((item) => !item.name.startsWith("."));
|
||||
}
|
||||
value.items.map((item, index) => {
|
||||
item.index = index;
|
||||
return item;
|
||||
})
|
||||
state.req = value;
|
||||
emitStateChanged();
|
||||
},
|
||||
@@ -220,7 +228,8 @@ export const mutations = {
|
||||
emitStateChanged();
|
||||
},
|
||||
updateListingItems: () => {
|
||||
state.req.items = sortedItems(state.req.items,state.user.sorting.by)
|
||||
state.req.items = sortedItems(state.req.items, state.user.sorting.by)
|
||||
mutations.replaceRequest(state.req);
|
||||
emitStateChanged();
|
||||
},
|
||||
updateClipboard: (value) => {
|
||||
|
||||
@@ -17,6 +17,7 @@ const loginPage = window.FileBrowser.LoginPage;
|
||||
const enableThumbs = window.FileBrowser.EnableThumbs;
|
||||
const resizePreview = window.FileBrowser.ResizePreview;
|
||||
const enableExec = window.FileBrowser.EnableExec;
|
||||
const externalUrl = window.FileBrowser.ExternalUrl
|
||||
const origin = window.location.origin;
|
||||
|
||||
const settings = [
|
||||
@@ -29,6 +30,7 @@ const settings = [
|
||||
|
||||
export {
|
||||
name,
|
||||
externalUrl,
|
||||
disableExternal,
|
||||
externalLinks,
|
||||
disableUsedPercentage,
|
||||
|
||||
@@ -1,8 +1,32 @@
|
||||
import { state, mutations, getters } from "@/store"
|
||||
import { filesApi } from "@/api";
|
||||
import { notify } from "@/notify"
|
||||
import { removePrefix } from "@/utils/url.js";
|
||||
import { publicApi } from "@/api";
|
||||
|
||||
export default function download() {
|
||||
if (getters.currentView() === "share") {
|
||||
let urlPath = getters.routePath("share");
|
||||
// Step 1: Split the path by '/'
|
||||
let parts = urlPath.split("/");
|
||||
// Step 2: Assign hash to the second part (index 2) and join the rest for subPath
|
||||
const hash = parts[1];
|
||||
const subPath = "/" + parts.slice(2).join("/");
|
||||
let files = [];
|
||||
for (let i of state.selected) {
|
||||
const dlfile = removePrefix(state.req.items[i].url, "share/"+hash);
|
||||
files.push(dlfile);
|
||||
}
|
||||
const share = {
|
||||
path: subPath,
|
||||
hash: hash,
|
||||
token: "",
|
||||
format: files.length ? "zip" : null,
|
||||
};
|
||||
publicApi.download(share, ...files);
|
||||
return
|
||||
}
|
||||
|
||||
if (getters.isSingleFileSelected()) {
|
||||
filesApi.download(null, [getters.selectedDownloadUrl()]);
|
||||
return;
|
||||
|
||||
@@ -3,6 +3,8 @@ import url from "@/utils/url.js";
|
||||
import { filesApi } from "@/api";
|
||||
|
||||
export function checkConflict(files, items) {
|
||||
console.log("testing",files)
|
||||
|
||||
if (typeof items === "undefined" || items === null) {
|
||||
items = [];
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export default {
|
||||
methods: {
|
||||
scrollToHash() {
|
||||
if (window.location.hash === this.lastHash) return;
|
||||
this.lastHash = window.location.hash
|
||||
this.lastHash = window.location.hash;
|
||||
if (window.location.hash) {
|
||||
const id = url.base64Encode(window.location.hash.slice(1));
|
||||
const element = document.getElementById(id);
|
||||
@@ -94,13 +94,12 @@ export default {
|
||||
},
|
||||
async fetchData() {
|
||||
if (state.route.path === this.lastPath) return;
|
||||
this.lastHash = ""
|
||||
this.lastHash = "";
|
||||
// Set loading to true and reset the error.
|
||||
mutations.setLoading("files", true);
|
||||
this.error = null;
|
||||
// Reset view information using mutations
|
||||
mutations.setReload(false);
|
||||
mutations.resetSelected();
|
||||
mutations.setMultiple(false);
|
||||
mutations.closeHovers();
|
||||
|
||||
@@ -125,7 +124,7 @@ export default {
|
||||
} catch (e) {
|
||||
notify.showError(e);
|
||||
this.error = e;
|
||||
mutations.replaceRequest(null);
|
||||
mutations.replaceRequest({});
|
||||
} finally {
|
||||
mutations.replaceRequest(data);
|
||||
mutations.setLoading("files", false);
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
<template v-if="isLoggedIn">
|
||||
<template>
|
||||
<div>
|
||||
<div v-show="showOverlay" @contextmenu.prevent="onOverlayRightClick" @click="resetPrompts" class="overlay"></div>
|
||||
<div
|
||||
v-show="showOverlay"
|
||||
@contextmenu.prevent="onOverlayRightClick"
|
||||
@click="resetPrompts"
|
||||
class="overlay"
|
||||
></div>
|
||||
<div v-if="progress" class="progress">
|
||||
<div v-bind:style="{ width: this.progress + '%' }"></div>
|
||||
</div>
|
||||
<listingBar :class="{ 'dark-mode-header': isDarkMode }" v-if="currentView == 'listingView'"></listingBar>
|
||||
<editorBar :class="{ 'dark-mode-header': isDarkMode }" v-else-if="currentView == 'editor'"></editorBar>
|
||||
<defaultBar :class="{ 'dark-mode-header': isDarkMode }" v-else></defaultBar>
|
||||
<listingBar
|
||||
:class="{ 'dark-mode-header': isDarkMode }"
|
||||
v-if="currentView == 'listingView'"
|
||||
></listingBar>
|
||||
<editorBar
|
||||
:class="{ 'dark-mode-header': isDarkMode }"
|
||||
v-else-if="currentView == 'editor'"
|
||||
></editorBar>
|
||||
<defaultBar v-else :class="{ 'dark-mode-header': isDarkMode }"></defaultBar>
|
||||
<sidebar></sidebar>
|
||||
<search v-if="showSearch"></search>
|
||||
<main :class="{ 'dark-mode': isDarkMode, moveWithSidebar: moveWithSidebar }">
|
||||
@@ -14,6 +25,7 @@
|
||||
</main>
|
||||
<prompts :class="{ 'dark-mode': isDarkMode }"></prompts>
|
||||
</div>
|
||||
|
||||
<div class="card" id="popup-notification">
|
||||
<i v-on:click="closePopUp" class="material-icons">close</i>
|
||||
<div id="popup-notification-content">no info</div>
|
||||
@@ -55,7 +67,7 @@ export default {
|
||||
mounted() {
|
||||
window.addEventListener("resize", this.updateIsMobile);
|
||||
if (state.user.themeColor) {
|
||||
document.documentElement.style.setProperty('--primaryColor', state.user.themeColor);
|
||||
document.documentElement.style.setProperty("--primaryColor", state.user.themeColor);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -114,16 +126,6 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onOverlayRightClick(event) {
|
||||
// Example: Show a custom context menu
|
||||
mutations.showHover({
|
||||
name: "ContextMenu", // Assuming ContextMenu is a component you've already imported
|
||||
props: {
|
||||
posX: event.clientX,
|
||||
posY: event.clientY,
|
||||
},
|
||||
});
|
||||
},
|
||||
updateIsMobile() {
|
||||
mutations.setMobile();
|
||||
},
|
||||
|
||||
@@ -67,13 +67,13 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
signup: () => signup,
|
||||
name: () => name,
|
||||
name: () => name || "FileBrowser Quantum",
|
||||
logoURL: () => logoURL,
|
||||
isDarkMode() {
|
||||
return darkMode === true;
|
||||
},
|
||||
loginName() {
|
||||
return name || "FileBrowser Quantum"
|
||||
return name
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
|
||||
@@ -279,7 +279,7 @@ export default {
|
||||
let file = await publicApi.fetchPub(this.subPath, this.hash, this.password);
|
||||
file.hash = this.hash;
|
||||
this.token = file.token;
|
||||
mutations.updateRequest(file);
|
||||
mutations.replaceRequest(file);
|
||||
document.title = `${file.name} - ${document.title}`;
|
||||
} catch (error) {
|
||||
this.error = error;
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
<template>
|
||||
<header>
|
||||
<header :class="{ 'dark-mode-header': isDarkMode }">
|
||||
<action v-if="notShare" icon="close" :label="$t('buttons.close')" @action="close()" />
|
||||
<title v-if="isSettings" class="topTitle">Settings</title>
|
||||
<title v-else class="topTitle">{{ req.name }}</title>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.flexbar {
|
||||
display: flex;
|
||||
flex-direction: block;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { url } from "@/utils";
|
||||
import router from "@/router";
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import { filesApi } from "@/api";
|
||||
import { getters, state, mutations } from "@/store";
|
||||
import Action from "@/components/Action.vue";
|
||||
|
||||
export default {
|
||||
@@ -26,182 +17,18 @@ export default {
|
||||
components: {
|
||||
Action,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
columnWidth: 350,
|
||||
width: window.innerWidth,
|
||||
itemWeight: 0,
|
||||
viewModes: ["list", "compact", "normal", "gallery"],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
notShare() {
|
||||
return getters.currentView() != "share";
|
||||
},
|
||||
isSettings() {
|
||||
return getters.isSettings();
|
||||
},
|
||||
// Map state and getters
|
||||
req() {
|
||||
return state.req;
|
||||
},
|
||||
user() {
|
||||
return state.user;
|
||||
},
|
||||
selected() {
|
||||
return state.selected;
|
||||
},
|
||||
nameSorted() {
|
||||
return state.user.sorting.by === "name";
|
||||
},
|
||||
sizeSorted() {
|
||||
return state.user.sorting.by === "size";
|
||||
},
|
||||
modifiedSorted() {
|
||||
return state.user.sorting.by === "modified";
|
||||
},
|
||||
ascOrdered() {
|
||||
return state.req.sorting.asc;
|
||||
},
|
||||
items() {
|
||||
const dirs = [];
|
||||
const files = [];
|
||||
|
||||
state.req.items.forEach((item) => {
|
||||
if (item.type == "directory") {
|
||||
dirs.push(item);
|
||||
} else {
|
||||
files.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return { dirs, files };
|
||||
},
|
||||
dirs() {
|
||||
return this.items.dirs.slice(0, this.showLimit);
|
||||
},
|
||||
files() {
|
||||
let showLimit = this.showLimit - this.items.dirs.length;
|
||||
|
||||
if (showLimit < 0) showLimit = 0;
|
||||
|
||||
return this.items.files.slice(0, showLimit);
|
||||
},
|
||||
nameIcon() {
|
||||
if (this.nameSorted && !this.ascOrdered) {
|
||||
return "arrow_upward";
|
||||
}
|
||||
|
||||
return "arrow_downward";
|
||||
},
|
||||
sizeIcon() {
|
||||
if (this.sizeSorted && this.ascOrdered) {
|
||||
return "arrow_downward";
|
||||
}
|
||||
|
||||
return "arrow_upward";
|
||||
},
|
||||
modifiedIcon() {
|
||||
if (this.modifiedSorted && this.ascOrdered) {
|
||||
return "arrow_downward";
|
||||
}
|
||||
|
||||
return "arrow_upward";
|
||||
},
|
||||
viewIcon() {
|
||||
const icons = {
|
||||
list: "view_module",
|
||||
compact: "view_module",
|
||||
normal: "grid_view",
|
||||
gallery: "view_list",
|
||||
};
|
||||
return icons[state.user.viewMode];
|
||||
},
|
||||
headerButtons() {
|
||||
return {
|
||||
select: getters.selectedCount() > 0,
|
||||
upload: state.user.perm?.create && getters.selectedCount() > 0,
|
||||
download: state.user.perm?.download && getters.selectedCount() > 0,
|
||||
delete: getters.selectedCount() > 0 && state.user.perm.delete,
|
||||
rename: getters.selectedCount() === 1 && state.user.perm.rename,
|
||||
share: getters.selectedCount() === 1 && state.user.perm.share,
|
||||
move: getters.selectedCount() > 0 && state.user.perm.rename,
|
||||
copy: getters.selectedCount() > 0 && state.user.perm?.create,
|
||||
};
|
||||
isDarkMode() {
|
||||
return getters.isDarkMode();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
||||
// How much every listing item affects the window height
|
||||
this.setItemWeight();
|
||||
|
||||
// Fill and fit the window with listing items
|
||||
this.fillWindow(true);
|
||||
|
||||
// Add the needed event listeners to the window and document.
|
||||
window.addEventListener("keydown", this.keyEvent);
|
||||
window.addEventListener("scroll", this.scrollEvent);
|
||||
window.addEventListener("resize", this.windowsResize);
|
||||
if (state.route.path.startsWith("/share")) {
|
||||
return;
|
||||
}
|
||||
if (!state.user.perm?.create) return;
|
||||
document.addEventListener("dragover", this.preventDefault);
|
||||
document.addEventListener("dragenter", this.dragEnter);
|
||||
document.addEventListener("dragleave", this.dragLeave);
|
||||
document.addEventListener("drop", this.drop);
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
// Remove event listeners before destroying this page.
|
||||
window.removeEventListener("keydown", this.keyEvent);
|
||||
window.removeEventListener("scroll", this.scrollEvent);
|
||||
window.removeEventListener("resize", this.windowsResize);
|
||||
|
||||
if (state.user && !state.user.perm?.create) return;
|
||||
document.removeEventListener("dragover", this.preventDefault);
|
||||
document.removeEventListener("dragenter", this.dragEnter);
|
||||
document.removeEventListener("dragleave", this.dragLeave);
|
||||
document.removeEventListener("drop", this.drop);
|
||||
},
|
||||
|
||||
methods: {
|
||||
fillWindow(fit = false) {
|
||||
const totalItems = state.req.numDirs + state.req.numFiles;
|
||||
|
||||
// More items are displayed than the total
|
||||
if (this.showLimit >= totalItems && !fit) return;
|
||||
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
// Quantity of items needed to fill 2x of the window height
|
||||
const showQuantity = Math.ceil((windowHeight + windowHeight * 2) / this.itemWeight);
|
||||
|
||||
// Less items to display than current
|
||||
if (this.showLimit > showQuantity && !fit) return;
|
||||
|
||||
// Set the number of displayed items
|
||||
this.showLimit = showQuantity > totalItems ? totalItems : showQuantity;
|
||||
},
|
||||
setItemWeight() {
|
||||
// Listing element is not displayed
|
||||
if (this.$refs.listingView == null) return;
|
||||
|
||||
let itemQuantity = state.req.numDirs + state.req.numFiles;
|
||||
if (itemQuantity > this.showLimit) itemQuantity = this.showLimit;
|
||||
|
||||
// How much every listing item affects the window height
|
||||
this.itemWeight = this.$refs.listingView.offsetHeight / itemQuantity;
|
||||
},
|
||||
action() {
|
||||
if (this.show) {
|
||||
mutations.showHover(this.show);
|
||||
}
|
||||
|
||||
this.$emit("action");
|
||||
},
|
||||
close() {
|
||||
if (getters.isSettings()) {
|
||||
// Use this.isSettings to access the computed property
|
||||
@@ -214,146 +41,6 @@ export default {
|
||||
router.push({ path: uri });
|
||||
mutations.closeHovers();
|
||||
},
|
||||
base64(name) {
|
||||
return url.base64Encode(name);
|
||||
},
|
||||
keyEvent(event) {
|
||||
// No prompts are shown
|
||||
if (this.show !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Esc!
|
||||
if (event.keyCode === 27) {
|
||||
// Reset files selection.
|
||||
mutations.resetSelected();
|
||||
}
|
||||
|
||||
// Del!
|
||||
if (event.keyCode === 46) {
|
||||
if (!state.user.perm.delete || getters.selectedCount() == 0) return;
|
||||
|
||||
// Show delete prompt.
|
||||
mutations.showHover("delete");
|
||||
}
|
||||
|
||||
// F2!
|
||||
if (event.keyCode === 113) {
|
||||
if (!state.user.perm.rename || getters.selectedCount() !== 1) return;
|
||||
|
||||
// Show rename prompt.
|
||||
mutations.showHover("rename");
|
||||
}
|
||||
|
||||
// Ctrl is pressed
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
let key = String.fromCharCode(event.which).toLowerCase();
|
||||
|
||||
switch (key) {
|
||||
case "f":
|
||||
event.preventDefault();
|
||||
mutations.showHover("search");
|
||||
break;
|
||||
case "c":
|
||||
case "x":
|
||||
this.copyCut(event, key);
|
||||
break;
|
||||
case "v":
|
||||
this.paste(event);
|
||||
break;
|
||||
case "a":
|
||||
event.preventDefault();
|
||||
for (let file of this.items.files) {
|
||||
if (state.selected.indexOf(file.index) === -1) {
|
||||
this.addSelected(file.index);
|
||||
}
|
||||
}
|
||||
for (let dir of this.items.dirs) {
|
||||
if (state.selected.indexOf(dir.index) === -1) {
|
||||
this.addSelected(dir.index);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "s":
|
||||
event.preventDefault();
|
||||
document.getElementById("download-button").click();
|
||||
break;
|
||||
}
|
||||
},
|
||||
switchView: async function () {
|
||||
mutations.closeHovers();
|
||||
const currentIndex = this.viewModes.indexOf(state.user.viewMode);
|
||||
const nextIndex = (currentIndex + 1) % this.viewModes.length;
|
||||
const newView = this.viewModes[nextIndex];
|
||||
mutations.updateCurrentUser({ viewMode: newView });
|
||||
},
|
||||
preventDefault(event) {
|
||||
// Wrapper around prevent default.
|
||||
event.preventDefault();
|
||||
},
|
||||
copyCut(event, key) {
|
||||
if (event.target.tagName.toLowerCase() === "input") {
|
||||
return;
|
||||
}
|
||||
|
||||
let items = [];
|
||||
|
||||
for (let i of state.selected) {
|
||||
items.push({
|
||||
from: state.req.items[i].url,
|
||||
name: state.req.items[i].name,
|
||||
});
|
||||
}
|
||||
|
||||
if (items.length == 0) {
|
||||
return;
|
||||
}
|
||||
mutations.updateClipboard({
|
||||
key: key,
|
||||
items: items,
|
||||
path: state.route.path,
|
||||
});
|
||||
},
|
||||
async paste(event) {
|
||||
if (event.target.tagName.toLowerCase() === "input") {
|
||||
return;
|
||||
}
|
||||
|
||||
let items = [];
|
||||
|
||||
for (let item of state.clipboard.items) {
|
||||
const from = item.from.endsWith("/") ? item.from.slice(0, -1) : item.from;
|
||||
const to = state.route.path + encodeURIComponent(item.name);
|
||||
items.push({ from, to, name: item.name });
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let action = async (overwrite, rename) => {
|
||||
await filesApi.moveCopy(items, "copy", overwrite, rename);
|
||||
notify.showSuccess("Items pasted successfully.");
|
||||
mutations.setReload(true);
|
||||
};
|
||||
|
||||
this.$confirm(
|
||||
"Are you sure you want to copy these items?",
|
||||
"Copy",
|
||||
() => {
|
||||
action(false, false);
|
||||
},
|
||||
() => {
|
||||
action(true, false);
|
||||
},
|
||||
() => {
|
||||
action(true, true);
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -16,26 +16,64 @@
|
||||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>{{ $t("files.lonely") }}</span>
|
||||
</h2>
|
||||
<input style="display: none" type="file" id="upload-input" @change="uploadInput($event)" multiple />
|
||||
<input style="display: none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory
|
||||
multiple />
|
||||
<input
|
||||
style="display: none"
|
||||
type="file"
|
||||
id="upload-input"
|
||||
@change="uploadInput($event)"
|
||||
multiple
|
||||
/>
|
||||
<input
|
||||
style="display: none"
|
||||
type="file"
|
||||
id="upload-folder-input"
|
||||
@change="uploadInput($event)"
|
||||
webkitdirectory
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
<div v-else id="listingView" ref="listingView" :class="listingViewMode + ' file-icons'">
|
||||
<div
|
||||
v-else
|
||||
id="listingView"
|
||||
ref="listingView"
|
||||
:class="listingViewMode + ' file-icons'"
|
||||
>
|
||||
<div>
|
||||
<div class="header" :class="{ 'dark-mode-item-header': isDarkMode }">
|
||||
<p :class="{ active: nameSorted }" class="name" role="button" tabindex="0" @click="sort('name')"
|
||||
:title="$t('files.sortByName')" :aria-label="$t('files.sortByName')">
|
||||
<p
|
||||
:class="{ active: nameSorted }"
|
||||
class="name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('name')"
|
||||
:title="$t('files.sortByName')"
|
||||
:aria-label="$t('files.sortByName')"
|
||||
>
|
||||
<span>{{ $t("files.name") }}</span>
|
||||
<i class="material-icons">{{ nameIcon }}</i>
|
||||
</p>
|
||||
|
||||
<p :class="{ active: sizeSorted }" class="size" role="button" tabindex="0" @click="sort('size')"
|
||||
:title="$t('files.sortBySize')" :aria-label="$t('files.sortBySize')">
|
||||
<p
|
||||
:class="{ active: sizeSorted }"
|
||||
class="size"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('size')"
|
||||
:title="$t('files.sortBySize')"
|
||||
:aria-label="$t('files.sortBySize')"
|
||||
>
|
||||
<span>{{ $t("files.size") }}</span>
|
||||
<i class="material-icons">{{ sizeIcon }}</i>
|
||||
</p>
|
||||
<p :class="{ active: modifiedSorted }" class="modified" role="button" tabindex="0" @click="sort('modified')"
|
||||
:title="$t('files.sortByLastModified')" :aria-label="$t('files.sortByLastModified')">
|
||||
<p
|
||||
:class="{ active: modifiedSorted }"
|
||||
class="modified"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('modified')"
|
||||
:title="$t('files.sortByLastModified')"
|
||||
:aria-label="$t('files.sortByLastModified')"
|
||||
>
|
||||
<span>{{ $t("files.lastModified") }}</span>
|
||||
<i class="material-icons">{{ modifiedIcon }}</i>
|
||||
</p>
|
||||
@@ -46,10 +84,23 @@
|
||||
<h2>{{ $t("files.folders") }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="numDirs > 0" class="folder-items" :class="{ lastGroup: numFiles === 0 }">
|
||||
<item v-for="item in dirs" :key="base64(item.name)" v-bind:index="item.index" v-bind:name="item.name"
|
||||
v-bind:isDir="item.type == 'directory'" v-bind:url="item.url" v-bind:modified="item.modified"
|
||||
v-bind:type="item.type" v-bind:size="item.size" v-bind:path="item.path" />
|
||||
<div
|
||||
v-if="numDirs > 0"
|
||||
class="folder-items"
|
||||
:class="{ lastGroup: numFiles === 0 }"
|
||||
>
|
||||
<item
|
||||
v-for="item in dirs"
|
||||
:key="base64(item.name)"
|
||||
v-bind:index="item.index"
|
||||
v-bind:name="item.name"
|
||||
v-bind:isDir="item.type == 'directory'"
|
||||
v-bind:url="item.url"
|
||||
v-bind:modified="item.modified"
|
||||
v-bind:type="item.type"
|
||||
v-bind:size="item.size"
|
||||
v-bind:path="item.path"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="numFiles > 0">
|
||||
<div class="header-items">
|
||||
@@ -57,14 +108,35 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="numFiles > 0" class="file-items" :class="{ lastGroup: numFiles > 0 }">
|
||||
<item v-for="item in files" :key="base64(item.name)" v-bind:index="item.index" v-bind:name="item.name"
|
||||
v-bind:isDir="item.type == 'directory'" v-bind:url="item.url" v-bind:modified="item.modified"
|
||||
v-bind:type="item.type" v-bind:size="item.size" v-bind:path="item.path" />
|
||||
<item
|
||||
v-for="item in files"
|
||||
:key="base64(item.name)"
|
||||
v-bind:index="item.index"
|
||||
v-bind:name="item.name"
|
||||
v-bind:isDir="item.type == 'directory'"
|
||||
v-bind:url="item.url"
|
||||
v-bind:modified="item.modified"
|
||||
v-bind:type="item.type"
|
||||
v-bind:size="item.size"
|
||||
v-bind:path="item.path"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input style="display: none" type="file" id="upload-input" @change="uploadInput($event)" multiple />
|
||||
<input style="display: none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory
|
||||
multiple />
|
||||
<input
|
||||
style="display: none"
|
||||
type="file"
|
||||
id="upload-input"
|
||||
@change="uploadInput($event)"
|
||||
multiple
|
||||
/>
|
||||
<input
|
||||
style="display: none"
|
||||
type="file"
|
||||
id="upload-folder-input"
|
||||
@change="uploadInput($event)"
|
||||
webkitdirectory
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,6 +165,7 @@ export default {
|
||||
width: window.innerWidth,
|
||||
lastSelected: {}, // Add this to track the currently focused item
|
||||
contextTimeout: null, // added for safari context menu
|
||||
ctrKeyPressed: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -196,7 +269,6 @@ export default {
|
||||
this.colunmsResize();
|
||||
return state.user.viewMode;
|
||||
},
|
||||
|
||||
selectedCount() {
|
||||
return state.selected.length;
|
||||
},
|
||||
@@ -215,7 +287,8 @@ export default {
|
||||
window.addEventListener("keydown", this.keyEvent);
|
||||
window.addEventListener("scroll", this.scrollEvent);
|
||||
window.addEventListener("resize", this.windowsResize);
|
||||
this.$el.addEventListener("click", this.clickClear);
|
||||
window.addEventListener("click", this.clickClear);
|
||||
window.addEventListener("keyup", this.clearCtrKey);
|
||||
|
||||
// Adjust contextmenu listener based on browser
|
||||
if (state.isSafari) {
|
||||
@@ -250,7 +323,6 @@ export default {
|
||||
this.$el.removeEventListener("touchend", this.cancelContext);
|
||||
this.$el.removeEventListener("mouseup", this.cancelContext);
|
||||
this.$el.removeEventListener("touchmove", this.handleTouchMove);
|
||||
|
||||
} else {
|
||||
window.removeEventListener("contextmenu", this.openContext);
|
||||
}
|
||||
@@ -425,17 +497,22 @@ export default {
|
||||
}, 50);
|
||||
}
|
||||
},
|
||||
clearCtrKey(event) {
|
||||
const { ctrlKey } = event;
|
||||
if (!ctrlKey) {
|
||||
this.ctrKeyPressed = false;
|
||||
}
|
||||
},
|
||||
keyEvent(event) {
|
||||
const { key, ctrlKey, metaKey, which } = event;
|
||||
// Check if the key is alphanumeric
|
||||
const isAlphanumeric = /^[a-z0-9]$/i.test(key);
|
||||
const noModifierKeys = !ctrlKey && !metaKey;
|
||||
|
||||
if (isAlphanumeric && noModifierKeys && getters.currentPromptName() == null) {
|
||||
const modifierKeys = ctrlKey || metaKey;
|
||||
if (isAlphanumeric && !modifierKeys && getters.currentPromptName() == null) {
|
||||
this.alphanumericKeyPress(key); // Call the alphanumeric key press function
|
||||
return;
|
||||
}
|
||||
if (noModifierKeys && getters.currentPromptName() != null) {
|
||||
if (!modifierKeys && getters.currentPromptName() != null) {
|
||||
return;
|
||||
}
|
||||
// Handle the space bar key
|
||||
@@ -452,6 +529,13 @@ export default {
|
||||
}
|
||||
let currentPath = state.route.path.replace(/\/+$/, ""); // Remove trailing slashes
|
||||
let newPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
|
||||
|
||||
if (modifierKeys) {
|
||||
if (!ctrlKey) {
|
||||
this.ctrKeyPressed = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Handle key events using a switch statement
|
||||
switch (key) {
|
||||
case "Enter":
|
||||
@@ -486,11 +570,6 @@ export default {
|
||||
event.preventDefault();
|
||||
this.navigateKeboardArrows(key);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Handle keys with ctrl or meta keys
|
||||
if (!ctrlKey && !metaKey) return;
|
||||
break;
|
||||
}
|
||||
|
||||
const charKey = String.fromCharCode(which).toLowerCase();
|
||||
@@ -513,8 +592,6 @@ export default {
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// Helper method to select all files and directories
|
||||
selectAll() {
|
||||
for (let file of this.items.files) {
|
||||
if (state.selected.indexOf(file.index) === -1) {
|
||||
@@ -650,12 +727,18 @@ export default {
|
||||
action(false, false);
|
||||
},
|
||||
colunmsResize() {
|
||||
document.documentElement.style.setProperty('--item-width', `calc(${100 / this.numColumns}% - 1em)`);
|
||||
document.documentElement.style.setProperty(
|
||||
"--item-width",
|
||||
`calc(${100 / this.numColumns}% - 1em)`
|
||||
);
|
||||
|
||||
if (state.user.viewMode == "gallery") {
|
||||
document.documentElement.style.setProperty('--item-height', `calc(${this.columnWidth / 25}em)`);
|
||||
document.documentElement.style.setProperty(
|
||||
"--item-height",
|
||||
`calc(${this.columnWidth / 25}em)`
|
||||
);
|
||||
} else {
|
||||
document.documentElement.style.setProperty('--item-height', `auto`);
|
||||
document.documentElement.style.setProperty("--item-height", `auto`);
|
||||
}
|
||||
},
|
||||
dragEnter() {
|
||||
@@ -814,9 +897,11 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
clickClear() {
|
||||
clickClear(event) {
|
||||
// if control or shift is pressed, do not clear the selection
|
||||
if (this.ctrKeyPressed || event.shiftKey) return;
|
||||
const sameAsBefore = state.selected == this.lastSelected;
|
||||
if (sameAsBefore && !state.multiple) {
|
||||
if (sameAsBefore && !state.multiple && getters.currentPromptName == null) {
|
||||
mutations.resetSelected();
|
||||
}
|
||||
this.lastSelected = state.selected;
|
||||
|
||||
@@ -110,7 +110,6 @@ export default {
|
||||
event.preventDefault();
|
||||
try {
|
||||
if (this.isNew) {
|
||||
const loc = await usersApi.create(this.userPayload); // Use the computed property
|
||||
this.$router.push({ path: "/settings", hash: "#users-main" });
|
||||
notify.showSuccess(this.$t("settings.userCreated"));
|
||||
} else {
|
||||
|
||||
+14
-20
@@ -1,6 +1,8 @@
|
||||
import { test, expect } from "./fixtures/auth";
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("redirect to login", async ({ page, context }) => {
|
||||
await context.clearCookies();
|
||||
|
||||
test("redirect to login", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
|
||||
@@ -8,24 +10,16 @@ test("redirect to login", async ({ page }) => {
|
||||
await expect(page).toHaveURL(/\/login\?redirect=\/files\//);
|
||||
});
|
||||
|
||||
test("login", async ({ authPage, page, context }) => {
|
||||
await authPage.goto();
|
||||
await expect(page).toHaveTitle(/Login - FileBrowser Quantum$/);
|
||||
|
||||
await authPage.loginAs("fake", "fake");
|
||||
await expect(authPage.wrongCredentials).toBeVisible();
|
||||
|
||||
await authPage.loginAs();
|
||||
await expect(authPage.wrongCredentials).toBeHidden();
|
||||
// await page.waitForURL("**/files/", { timeout: 5000 });
|
||||
await expect(page).toHaveTitle(/.*Files - FileBrowser Quantum$/);
|
||||
|
||||
test("logout", async ({ page, context }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator("div.wrong")).toBeHidden();
|
||||
await page.waitForURL("**/files/", { timeout: 100 });
|
||||
await expect(page).toHaveTitle('playwright-files - FileBrowser Quantum - Files');
|
||||
let cookies = await context.cookies();
|
||||
expect(cookies.find((c) => c.name == "auth")?.value).toBeDefined();
|
||||
|
||||
// await authPage.logout();
|
||||
// await page.waitForURL("**/login", { timeout: 5000 });
|
||||
// await expect(page).toHaveTitle(/Login - FileBrowser Quantum$/);
|
||||
// cookies = await context.cookies();
|
||||
// expect(cookies.find((c) => c.name == "auth")?.value).toBeUndefined();
|
||||
await page.locator('div.inner-card.logout-button').click();
|
||||
await page.waitForURL("**/login", { timeout: 100 });
|
||||
await expect(page).toHaveTitle('FileBrowser Quantum - Login');
|
||||
cookies = await context.cookies();
|
||||
expect(cookies.find((c) => c.name == "auth")?.value).toBeUndefined();
|
||||
});
|
||||
Vendored
-40
@@ -1,40 +0,0 @@
|
||||
import {
|
||||
type Page,
|
||||
type Locator,
|
||||
test as base,
|
||||
expect,
|
||||
} from "@playwright/test";
|
||||
|
||||
export class AuthPage {
|
||||
public readonly wrongCredentials: Locator;
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.wrongCredentials = this.page.locator("div.wrong");
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto("/login");
|
||||
}
|
||||
|
||||
async loginAs(username = "admin", password = "admin") {
|
||||
await this.page.getByPlaceholder("Username").fill(username);
|
||||
await this.page.getByPlaceholder("Password").fill(password);
|
||||
await this.page.getByRole("button", { name: "Login" }).click();
|
||||
}
|
||||
|
||||
//async logout() {
|
||||
// await this.page.getByRole("button", { name: "Logout" }).click();
|
||||
//}
|
||||
}
|
||||
|
||||
const test = base.extend<{ authPage: AuthPage }>({
|
||||
authPage: async ({ page }, use) => {
|
||||
const authPage = new AuthPage(page);
|
||||
await authPage.goto();
|
||||
await authPage.loginAs();
|
||||
await use(authPage);
|
||||
// await authPage.logout();
|
||||
},
|
||||
});
|
||||
|
||||
export { test, expect };
|
||||
Whitespace-only changes.
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("file preview", async ({ page, context }) => {
|
||||
await page.goto("/files/");
|
||||
await expect(page).toHaveTitle('playwright-files - FileBrowser Quantum - Files');
|
||||
await page.locator('a[aria-label="file.tar.gz"]').waitFor({ state: 'visible' });
|
||||
await page.locator('a[aria-label="file.tar.gz"]').dblclick();
|
||||
await expect(page).toHaveTitle('file.tar.gz - FileBrowser Quantum - Files');
|
||||
await page.locator('button[title="Close"]').click();
|
||||
await expect(page).toHaveTitle('playwright-files - FileBrowser Quantum - Files');
|
||||
});
|
||||
@@ -20,6 +20,6 @@
|
||||
},
|
||||
"checkJs": false, // check later
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.vue"],
|
||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.vue", "tests/auth.spec.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// vite.config.ts
|
||||
import path from "node:path";
|
||||
import { defineConfig } from "file:///Users/steffag/git/personal/filebrowser/frontend/node_modules/vite/dist/node/index.js";
|
||||
import vue from "file:///Users/steffag/git/personal/filebrowser/frontend/node_modules/@vitejs/plugin-vue/dist/index.mjs";
|
||||
import VueI18nPlugin from "file:///Users/steffag/git/personal/filebrowser/frontend/node_modules/@intlify/unplugin-vue-i18n/lib/vite.mjs";
|
||||
import { compression } from "file:///Users/steffag/git/personal/filebrowser/frontend/node_modules/vite-plugin-compression2/dist/index.mjs";
|
||||
var __vite_injected_original_dirname = "/Users/steffag/git/personal/filebrowser/frontend";
|
||||
var plugins = [
|
||||
vue(),
|
||||
VueI18nPlugin({
|
||||
include: [path.resolve(__vite_injected_original_dirname, "./src/i18n/**/*.json")]
|
||||
}),
|
||||
compression({
|
||||
include: /\.(js|woff2|woff)(\?.*)?$/i,
|
||||
deleteOriginalAssets: true
|
||||
})
|
||||
];
|
||||
var resolve = {
|
||||
alias: {
|
||||
"@": path.resolve(__vite_injected_original_dirname, "src")
|
||||
}
|
||||
};
|
||||
var vite_config_default = defineConfig(({ command }) => {
|
||||
if (command === "serve") {
|
||||
return {
|
||||
plugins,
|
||||
resolve,
|
||||
server: {
|
||||
proxy: {
|
||||
"/api/command": {
|
||||
target: "ws://127.0.0.1:8080",
|
||||
ws: true
|
||||
},
|
||||
"/api": "http://127.0.0.1:8080"
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
plugins,
|
||||
resolve,
|
||||
base: "",
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: path.resolve(__vite_injected_original_dirname, "./public/index.html")
|
||||
},
|
||||
output: {
|
||||
manualChunks: (id) => {
|
||||
if (id.includes("i18n/")) {
|
||||
return "i18n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
experimental: {
|
||||
renderBuiltUrl(filename, { hostType }) {
|
||||
if (hostType === "js") {
|
||||
return { runtime: `window.__prependStaticUrl("${filename}")` };
|
||||
} else if (hostType === "html") {
|
||||
return `{{ .StaticURL }}/${filename}`;
|
||||
} else {
|
||||
return { relative: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
export {
|
||||
vite_config_default as default
|
||||
};
|
||||
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc3RlZmZhZy9naXQvcGVyc29uYWwvZmlsZWJyb3dzZXIvZnJvbnRlbmRcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9zdGVmZmFnL2dpdC9wZXJzb25hbC9maWxlYnJvd3Nlci9mcm9udGVuZC92aXRlLmNvbmZpZy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc3RlZmZhZy9naXQvcGVyc29uYWwvZmlsZWJyb3dzZXIvZnJvbnRlbmQvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgcGF0aCBmcm9tIFwibm9kZTpwYXRoXCI7XG5pbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tIFwidml0ZVwiO1xuaW1wb3J0IHZ1ZSBmcm9tIFwiQHZpdGVqcy9wbHVnaW4tdnVlXCI7XG5pbXBvcnQgVnVlSTE4blBsdWdpbiBmcm9tIFwiQGludGxpZnkvdW5wbHVnaW4tdnVlLWkxOG4vdml0ZVwiO1xuaW1wb3J0IHsgY29tcHJlc3Npb24gfSBmcm9tIFwidml0ZS1wbHVnaW4tY29tcHJlc3Npb24yXCI7XG5cbmNvbnN0IHBsdWdpbnMgPSBbXG4gIHZ1ZSgpLFxuICBWdWVJMThuUGx1Z2luKHtcbiAgICBpbmNsdWRlOiBbcGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgXCIuL3NyYy9pMThuLyoqLyouanNvblwiKV0sXG4gIH0pLFxuICBjb21wcmVzc2lvbih7XG4gICAgaW5jbHVkZTogL1xcLihqc3x3b2ZmMnx3b2ZmKShcXD8uKik/JC9pLFxuICAgIGRlbGV0ZU9yaWdpbmFsQXNzZXRzOiB0cnVlLFxuICB9KSxcbl07XG5cbmNvbnN0IHJlc29sdmUgPSB7XG4gIGFsaWFzOiB7XG4gICAgXCJAXCI6IHBhdGgucmVzb2x2ZShfX2Rpcm5hbWUsIFwic3JjXCIpLFxuICB9LFxufTtcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZygoeyBjb21tYW5kIH0pID0+IHtcbiAgaWYgKGNvbW1hbmQgPT09IFwic2VydmVcIikge1xuICAgIHJldHVybiB7XG4gICAgICBwbHVnaW5zLFxuICAgICAgcmVzb2x2ZSxcbiAgICAgIHNlcnZlcjoge1xuICAgICAgICBwcm94eToge1xuICAgICAgICAgIFwiL2FwaS9jb21tYW5kXCI6IHtcbiAgICAgICAgICAgIHRhcmdldDogXCJ3czovLzEyNy4wLjAuMTo4MDgwXCIsXG4gICAgICAgICAgICB3czogdHJ1ZSxcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwiL2FwaVwiOiBcImh0dHA6Ly8xMjcuMC4wLjE6ODA4MFwiLFxuICAgICAgICB9LFxuICAgICAgfSxcbiAgICB9O1xuICB9IGVsc2Uge1xuICAgIC8vIGNvbW1hbmQgPT09ICdidWlsZCdcbiAgICByZXR1cm4ge1xuICAgICAgcGx1Z2lucyxcbiAgICAgIHJlc29sdmUsXG4gICAgICBiYXNlOiBcIlwiLFxuICAgICAgYnVpbGQ6IHtcbiAgICAgICAgcm9sbHVwT3B0aW9uczoge1xuICAgICAgICAgIGlucHV0OiB7XG4gICAgICAgICAgICBpbmRleDogcGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgXCIuL3B1YmxpYy9pbmRleC5odG1sXCIpLFxuICAgICAgICAgIH0sXG4gICAgICAgICAgb3V0cHV0OiB7XG4gICAgICAgICAgICBtYW51YWxDaHVua3M6IChpZCkgPT4ge1xuICAgICAgICAgICAgICBpZiAoaWQuaW5jbHVkZXMoXCJpMThuL1wiKSkge1xuICAgICAgICAgICAgICAgIHJldHVybiBcImkxOG5cIjtcbiAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgfSxcbiAgICAgICAgICB9LFxuICAgICAgICB9LFxuICAgICAgfSxcbiAgICAgIGV4cGVyaW1lbnRhbDoge1xuICAgICAgICByZW5kZXJCdWlsdFVybChmaWxlbmFtZSwgeyBob3N0VHlwZSB9KSB7XG4gICAgICAgICAgaWYgKGhvc3RUeXBlID09PSBcImpzXCIpIHtcbiAgICAgICAgICAgIHJldHVybiB7IHJ1bnRpbWU6IGB3aW5kb3cuX19wcmVwZW5kU3RhdGljVXJsKFwiJHtmaWxlbmFtZX1cIilgIH07XG4gICAgICAgICAgfSBlbHNlIGlmIChob3N0VHlwZSA9PT0gXCJodG1sXCIpIHtcbiAgICAgICAgICAgIHJldHVybiBge3sgLlN0YXRpY1VSTCB9fS8ke2ZpbGVuYW1lfWA7XG4gICAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIHJldHVybiB7IHJlbGF0aXZlOiB0cnVlIH07XG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgfSxcbiAgICB9O1xuICB9XG59KTtcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBa1UsT0FBTyxVQUFVO0FBQ25WLFNBQVMsb0JBQW9CO0FBQzdCLE9BQU8sU0FBUztBQUNoQixPQUFPLG1CQUFtQjtBQUMxQixTQUFTLG1CQUFtQjtBQUo1QixJQUFNLG1DQUFtQztBQU16QyxJQUFNLFVBQVU7QUFBQSxFQUNkLElBQUk7QUFBQSxFQUNKLGNBQWM7QUFBQSxJQUNaLFNBQVMsQ0FBQyxLQUFLLFFBQVEsa0NBQVcsc0JBQXNCLENBQUM7QUFBQSxFQUMzRCxDQUFDO0FBQUEsRUFDRCxZQUFZO0FBQUEsSUFDVixTQUFTO0FBQUEsSUFDVCxzQkFBc0I7QUFBQSxFQUN4QixDQUFDO0FBQ0g7QUFFQSxJQUFNLFVBQVU7QUFBQSxFQUNkLE9BQU87QUFBQSxJQUNMLEtBQUssS0FBSyxRQUFRLGtDQUFXLEtBQUs7QUFBQSxFQUNwQztBQUNGO0FBR0EsSUFBTyxzQkFBUSxhQUFhLENBQUMsRUFBRSxRQUFRLE1BQU07QUFDM0MsTUFBSSxZQUFZLFNBQVM7QUFDdkIsV0FBTztBQUFBLE1BQ0w7QUFBQSxNQUNBO0FBQUEsTUFDQSxRQUFRO0FBQUEsUUFDTixPQUFPO0FBQUEsVUFDTCxnQkFBZ0I7QUFBQSxZQUNkLFFBQVE7QUFBQSxZQUNSLElBQUk7QUFBQSxVQUNOO0FBQUEsVUFDQSxRQUFRO0FBQUEsUUFDVjtBQUFBLE1BQ0Y7QUFBQSxJQUNGO0FBQUEsRUFDRixPQUFPO0FBRUwsV0FBTztBQUFBLE1BQ0w7QUFBQSxNQUNBO0FBQUEsTUFDQSxNQUFNO0FBQUEsTUFDTixPQUFPO0FBQUEsUUFDTCxlQUFlO0FBQUEsVUFDYixPQUFPO0FBQUEsWUFDTCxPQUFPLEtBQUssUUFBUSxrQ0FBVyxxQkFBcUI7QUFBQSxVQUN0RDtBQUFBLFVBQ0EsUUFBUTtBQUFBLFlBQ04sY0FBYyxDQUFDLE9BQU87QUFDcEIsa0JBQUksR0FBRyxTQUFTLE9BQU8sR0FBRztBQUN4Qix1QkFBTztBQUFBLGNBQ1Q7QUFBQSxZQUNGO0FBQUEsVUFDRjtBQUFBLFFBQ0Y7QUFBQSxNQUNGO0FBQUEsTUFDQSxjQUFjO0FBQUEsUUFDWixlQUFlLFVBQVUsRUFBRSxTQUFTLEdBQUc7QUFDckMsY0FBSSxhQUFhLE1BQU07QUFDckIsbUJBQU8sRUFBRSxTQUFTLDhCQUE4QixRQUFRLEtBQUs7QUFBQSxVQUMvRCxXQUFXLGFBQWEsUUFBUTtBQUM5QixtQkFBTyxvQkFBb0IsUUFBUTtBQUFBLFVBQ3JDLE9BQU87QUFDTCxtQkFBTyxFQUFFLFVBQVUsS0FBSztBQUFBLFVBQzFCO0FBQUEsUUFDRjtBQUFBLE1BQ0Y7QUFBQSxJQUNGO0FBQUEsRUFDRjtBQUNGLENBQUM7IiwKICAibmFtZXMiOiBbXQp9Cg==
|
||||
@@ -16,6 +16,9 @@ update:
|
||||
build:
|
||||
docker build --build-arg="VERSION=testing" --build-arg="REVISION=n/a" -t gtstef/filebrowser .
|
||||
|
||||
build-backend:
|
||||
cd backend && go build -o filebrowser --ldflags="-w -s -X 'github.com/gtsteffaniak/filebrowser/backend/version.CommitSHA=testingCommit' -X 'github.com/gtsteffaniak/filebrowser/backend/version.Version=testing'"
|
||||
|
||||
run: run-frontend
|
||||
cd backend && swag init --output swagger/docs && \
|
||||
if [ "$(shell uname)" = "Darwin" ]; then \
|
||||
@@ -48,9 +51,9 @@ test-backend:
|
||||
test-frontend:
|
||||
cd frontend && npm run test
|
||||
|
||||
test-frontend-playwright:
|
||||
npx playwright install
|
||||
docker build -t gtstef/filebrowser-tests -f Dockerfile.playwright .
|
||||
test-playwright: run-frontend build-backend
|
||||
docker build -t filebrowser-playwright-tests -f Dockerfile.playwright .
|
||||
docker run --rm --name filebrowser-playwright-tests filebrowser-playwright-tests
|
||||
|
||||
# Run on a windows machine!
|
||||
release-windows:
|
||||
|
||||
Reference in new issue
Block a user