Beta/v0.7.6 (#697)
This commit is contained in:
1 parent
40374752a3
commit
0a9af58b07
41 files changed
+626
-288
No files matched your search
@@ -104,7 +104,7 @@ jobs:
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./_docker/Dockerfile
|
||||
file: ./_docker/Dockerfile.slim
|
||||
push: false # Do not push the image for now
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -55,8 +55,8 @@ jobs:
|
||||
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
|
||||
file: ./_docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: ./_docker/Dockerfile.slim
|
||||
push: true
|
||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -1,125 +0,0 @@
|
||||
name: Manual Docker Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "Tag or Branch (format: v0.0.0-beta)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
push_release_to_registry:
|
||||
name: Push release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }} # Uses the Personal Access Token
|
||||
ref: "${{ github.event.inputs.branch }}"
|
||||
- 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: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{github.actor}}
|
||||
password: ${{ secrets.PAT }}
|
||||
- name: Extract metadata (tags, labels) for Docker and GHCR
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
gtstef/filebrowser
|
||||
ghcr.io/gtsteffaniak/filebrowser
|
||||
tags: |
|
||||
type=raw,value=${{ github.event.inputs.branch }}
|
||||
- name: Modify tags (strip 'v' and add 'beta'/'stable' if needed)
|
||||
id: modify-json
|
||||
run: |
|
||||
JSON='${{ steps.meta.outputs.json }}'
|
||||
MODIFIED_JSON=$(echo "$JSON" | jq '
|
||||
.tags |= map(sub(":v"; ":")) |
|
||||
if any(.tags[]; test("beta")) then
|
||||
.tags += [
|
||||
"gtstef/filebrowser:beta",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:beta",
|
||||
"gtstef/filebrowser:latest",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:latest"
|
||||
]
|
||||
else . end |
|
||||
if any(.tags[]; test("stable")) then
|
||||
.tags += [
|
||||
"gtstef/filebrowser:stable",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:stable",
|
||||
"gtstef/filebrowser:latest",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:latest"
|
||||
]
|
||||
else . end
|
||||
')
|
||||
TAGS_CSV=$(echo "$MODIFIED_JSON" | jq -r '.tags | join(",")')
|
||||
echo "$TAGS_CSV" > modified_tags.txt # Optional debug output
|
||||
echo "cleaned_tag=$TAGS_CSV" >> $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: ./_docker/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
- name: Modify version names
|
||||
id: modify-json-media
|
||||
run: |
|
||||
JSON='${{ steps.meta.outputs.json }}'
|
||||
MODIFIED_JSON=$(echo "$JSON" | jq '
|
||||
.tags |= map(
|
||||
sub(":v"; ":") + "-media"
|
||||
) |
|
||||
if any(.tags[]; test("beta-media")) then
|
||||
.tags += [
|
||||
"gtstef/filebrowser:beta-media",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:beta-media",
|
||||
"gtstef/filebrowser:media",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:media"
|
||||
]
|
||||
else . end |
|
||||
if any(.tags[]; test("stable-media")) then
|
||||
.tags += [
|
||||
"gtstef/filebrowser:stable-media",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:stable-media",
|
||||
"gtstef/filebrowser:media",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:media"
|
||||
]
|
||||
else . end
|
||||
')
|
||||
TAGS_CSV=$(echo "$MODIFIED_JSON" | jq -r '.tags | join(",")')
|
||||
echo "$TAGS_CSV" > modified_tags.txt # Optional debug output
|
||||
echo "cleaned_tag=$TAGS_CSV" >> $GITHUB_OUTPUT
|
||||
- name: Build and push media image
|
||||
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'] }}
|
||||
file: ./_docker/Dockerfile.media
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.modify-json-media.outputs.cleaned_tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -91,8 +91,75 @@ jobs:
|
||||
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
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: ./_docker/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
push_slim_release_to_registry:
|
||||
name: Push slim 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: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{github.actor}}
|
||||
password: ${{ secrets.PAT }}
|
||||
- name: Extract metadata (tags, labels) for Docker and GHCR
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
gtstef/filebrowser
|
||||
ghcr.io/gtsteffaniak/filebrowser
|
||||
- name: Modify tags for slim image
|
||||
id: modify-tags-slim
|
||||
run: |
|
||||
TAG_OR_BRANCH="${{ github.ref_name }}"
|
||||
CLEAN_TAG=$(echo "$TAG_OR_BRANCH" | sed 's/^v//')
|
||||
BASE_TAG="gtstef/filebrowser:${CLEAN_TAG}-slim"
|
||||
TAGS=("$BASE_TAG")
|
||||
if echo "$CLEAN_TAG" | grep -qi "beta"; then
|
||||
TAGS+=(
|
||||
"gtstef/filebrowser:beta-slim"
|
||||
"ghcr.io/gtsteffaniak/filebrowser:beta-slim"
|
||||
"gtstef/filebrowser:slim"
|
||||
"ghcr.io/gtsteffaniak/filebrowser:slim"
|
||||
)
|
||||
elif echo "$CLEAN_TAG" | grep -qi "stable"; then
|
||||
TAGS+=(
|
||||
"gtstef/filebrowser:stable-slim"
|
||||
"ghcr.io/gtsteffaniak/filebrowser:stable-slim"
|
||||
"gtstef/filebrowser:slim"
|
||||
"ghcr.io/gtsteffaniak/filebrowser:slim"
|
||||
)
|
||||
fi
|
||||
TAGS_CSV=$(IFS=, ; echo "${TAGS[*]}")
|
||||
echo "$TAGS_CSV" > modified_tags.txt # Optional debug output
|
||||
echo "cleaned_tag=$TAGS_CSV" >> $GITHUB_OUTPUT
|
||||
- name: Build and push slim image
|
||||
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: ./_docker/Dockerfile.slim
|
||||
latest: false
|
||||
push: true
|
||||
tags: ${{ steps.modify-json-slim.outputs.cleaned_tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -2,6 +2,39 @@
|
||||
|
||||
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.7.6-beta
|
||||
|
||||
NOTE: if using docker arm32 image, you will need to switch to the slim images. The regular docker images are much larger now and support generating office previews out of the box without any only office running. However, they don't support arm32. Also, be aware the docker images are much larger now (600MB I believe) because of the office document preview support -- if thats not something you care about you can switch to the slim images.
|
||||
|
||||
**New Features**:
|
||||
- native document preview generation enabled for certain document types on the regular docker image (no office integration needed)
|
||||
- supported native document preview types:
|
||||
- ".pdf", // PDF
|
||||
- ".xps", // XPS
|
||||
- ".epub", // EPUB
|
||||
- ".mobi", // MOBI
|
||||
- ".fb2", // FB2
|
||||
- ".cbz", // CBZ
|
||||
- ".svg", // SVG
|
||||
- ".txt", // TXT
|
||||
- ".doc", // DOC
|
||||
- ".docx", // DOCX
|
||||
- ".ppt", // PPT
|
||||
- ".pptx", // PPTX
|
||||
- ".xls", // excel xls
|
||||
- ".xlsx", // exel XLSX
|
||||
- ".hwp", // HWP
|
||||
- ".hwp", // HWPX
|
||||
- proxy logout redirectUrl support via `auth.methods.proxy.logoutRedirectUrl` https://github.com/gtsteffaniak/filebrowser/issues/684
|
||||
|
||||
**Notes**:
|
||||
- image loading placeholders added and remain if image can't be loaded.
|
||||
- no more arm32 support on main image -- use a `slim` tagged image.
|
||||
|
||||
**BugFixes**:
|
||||
- onlyoffice and other cache issues https://github.com/gtsteffaniak/filebrowser/issues/686
|
||||
- gallery size indicator centering https://github.com/gtsteffaniak/filebrowser/issues/652
|
||||
|
||||
## v0.7.5-beta
|
||||
|
||||
**New Features**
|
||||
|
||||
+7
-19
@@ -1,3 +1,4 @@
|
||||
FROM gtstef/ffmpeg:7.1.1 AS ffmpeg
|
||||
FROM golang:1.24-alpine AS base
|
||||
ARG VERSION
|
||||
ARG REVISION
|
||||
@@ -5,12 +6,12 @@ WORKDIR /app
|
||||
COPY ./backend ./
|
||||
#RUN swag init --output swagger/docs
|
||||
RUN ln -s swagger /usr/local/go/src/
|
||||
RUN go build -ldflags="-w -s \
|
||||
RUN go build -tags pdf -ldflags="-w -s \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.Version=${VERSION}' \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.CommitSHA=${REVISION}'" \
|
||||
-o filebrowser .
|
||||
|
||||
FROM node:slim AS nbuild
|
||||
FROM node:lts-slim AS nbuild
|
||||
WORKDIR /app
|
||||
COPY ./frontend/package.json ./
|
||||
RUN npm i --maxsockets 1
|
||||
@@ -18,23 +19,10 @@ COPY ./frontend/ ./
|
||||
RUN npm run build-docker
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache curl xz && \
|
||||
ARCH=$(uname -m) && \
|
||||
case "$ARCH" in \
|
||||
x86_64) URL="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz" ;; \
|
||||
aarch64) URL="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz" ;; \
|
||||
armv7l) URL="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-armhf-static.tar.xz" ;; \
|
||||
*) echo "Unsupported architecture: $ARCH" && exit 1 ;; \
|
||||
esac && \
|
||||
mkdir -p /tmp/ffmpeg && \
|
||||
curl -L "$URL" | tar -xJ -C /tmp/ffmpeg && \
|
||||
mv /tmp/ffmpeg/ffmpeg-*/ffmpeg /usr/local/bin/ && \
|
||||
mv /tmp/ffmpeg/ffmpeg-*/ffprobe /usr/local/bin/ && \
|
||||
chmod +x /usr/local/bin/ffmpeg /usr/local/bin/ffprobe && \
|
||||
rm -rf /tmp/ffmpeg && \
|
||||
apk del curl xz && \
|
||||
rm -rf /var/cache/apk/*
|
||||
COPY --from=ffmpeg [ "/ffmpeg", "/ffprobe", "/usr/local/bin/" ]
|
||||
# requirements to enable pdf support
|
||||
RUN apk add --no-cache libffi mupdf && \
|
||||
find / -name "libmupdf.so*" -type f -exec mv {} /usr/lib/libmupdf.so \;
|
||||
|
||||
ENV FILEBROWSER_FFMPEG_PATH="/usr/local/bin/"
|
||||
ENV FILEBROWSER_NO_EMBEDED="true"
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
FROM golang:1.24-alpine AS base
|
||||
ARG VERSION
|
||||
ARG REVISION
|
||||
WORKDIR /app
|
||||
COPY ./backend ./
|
||||
#RUN swag init --output swagger/docs
|
||||
RUN ln -s swagger /usr/local/go/src/
|
||||
RUN go build -ldflags="-w -s \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.Version=${VERSION}' \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.CommitSHA=${REVISION}'" \
|
||||
-o filebrowser .
|
||||
|
||||
FROM node:lts-slim AS nbuild
|
||||
WORKDIR /app
|
||||
COPY ./frontend/package.json ./
|
||||
RUN npm i --maxsockets 1
|
||||
COPY ./frontend/ ./
|
||||
RUN npm run build-docker
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache curl xz && \
|
||||
ARCH=$(uname -m) && \
|
||||
case "$ARCH" in \
|
||||
x86_64) URL="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz" ;; \
|
||||
aarch64) URL="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz" ;; \
|
||||
armv7l) URL="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-armhf-static.tar.xz" ;; \
|
||||
*) echo "Unsupported architecture: $ARCH" && exit 1 ;; \
|
||||
esac && \
|
||||
mkdir -p /tmp/ffmpeg && \
|
||||
curl -L "$URL" | tar -xJ -C /tmp/ffmpeg && \
|
||||
mv /tmp/ffmpeg/ffmpeg-*/ffmpeg /usr/local/bin/ && \
|
||||
mv /tmp/ffmpeg/ffmpeg-*/ffprobe /usr/local/bin/ && \
|
||||
chmod +x /usr/local/bin/ffmpeg /usr/local/bin/ffprobe && \
|
||||
rm -rf /tmp/ffmpeg && \
|
||||
apk del curl xz && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
ENV FILEBROWSER_FFMPEG_PATH="/usr/local/bin/"
|
||||
ENV FILEBROWSER_NO_EMBEDED="true"
|
||||
ENV PATH="$PATH:/home/filebrowser"
|
||||
RUN apk --no-cache add ca-certificates mailcap tzdata
|
||||
RUN adduser -D -s /bin/true -u 1000 filebrowser
|
||||
USER filebrowser
|
||||
WORKDIR /home/filebrowser
|
||||
COPY --from=base --chown=filebrowser:1000 /app/filebrowser* ./
|
||||
COPY --from=base --chown=filebrowser:1000 /app/config.media.yaml ./config.yaml
|
||||
COPY --from=nbuild --chown=filebrowser:1000 /app/dist/ ./http/dist/
|
||||
|
||||
## sanity checks
|
||||
RUN [ "filebrowser", "version" ]
|
||||
RUN [ "ffmpeg", "-version" ]
|
||||
RUN [ "ffprobe", "-version" ]
|
||||
|
||||
USER root
|
||||
# exposing default port for auto discovery.
|
||||
EXPOSE 80
|
||||
ENTRYPOINT [ "./filebrowser" ]
|
||||
@@ -23,6 +23,7 @@ auth:
|
||||
enabled: true
|
||||
header: "X-Username"
|
||||
createUser: true
|
||||
logoutRedirectUrl: "https://localhost/api/auth/logout2"
|
||||
|
||||
userDefaults:
|
||||
darkMode: true
|
||||
|
||||
@@ -9,13 +9,14 @@ import (
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/fileutils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
@@ -23,12 +24,9 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
|
||||
"github.com/gtsteffaniak/go-cache/cache"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
)
|
||||
|
||||
var OnlyOfficeCache = cache.NewCache(48 * time.Hour)
|
||||
|
||||
func FileInfoFaster(opts iteminfo.FileOptions) (iteminfo.ExtendedFileInfo, error) {
|
||||
response := iteminfo.ExtendedFileInfo{}
|
||||
if opts.Source == "" {
|
||||
@@ -70,7 +68,7 @@ func FileInfoFaster(opts iteminfo.FileOptions) (iteminfo.ExtendedFileInfo, error
|
||||
if !exists {
|
||||
return response, fmt.Errorf("could not get metadata for path: %v", opts.Path)
|
||||
}
|
||||
if opts.Content && strings.HasPrefix(info.Type, "text") {
|
||||
if opts.Content {
|
||||
if info.Size < 20*1024*1024 { // 20 megabytes in bytes
|
||||
content, err := getContent(realPath)
|
||||
if err != nil {
|
||||
@@ -98,11 +96,11 @@ func FileInfoFaster(opts iteminfo.FileOptions) (iteminfo.ExtendedFileInfo, error
|
||||
}
|
||||
|
||||
func generateOfficeId(realPath string) string {
|
||||
key, ok := OnlyOfficeCache.Get(realPath).(string)
|
||||
key, ok := utils.OnlyOfficeCache.Get(realPath).(string)
|
||||
if !ok {
|
||||
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
documentKey := utils.HashSHA256(realPath + timestamp)
|
||||
OnlyOfficeCache.Set(realPath, documentKey)
|
||||
utils.OnlyOfficeCache.Set(realPath, documentKey)
|
||||
return documentKey
|
||||
}
|
||||
return key
|
||||
@@ -271,28 +269,129 @@ func WriteFile(opts iteminfo.FileOptions, in io.Reader) error {
|
||||
return idx.RefreshFileInfo(opts)
|
||||
}
|
||||
|
||||
// getContent reads and returns the file content if it's UTF-8 readable.
|
||||
// getContent reads and returns the file content if it's considered an editable text file.
|
||||
func getContent(realPath string) (string, error) {
|
||||
// Read the entire file in one go. This is more efficient.
|
||||
const headerSize = 4096
|
||||
// Thresholds for detecting binary-like content (these can be tuned)
|
||||
const maxNullBytesInHeaderAbs = 10 // Max absolute null bytes in header
|
||||
const maxNullByteRatioInHeader = 0.1 // Max 10% null bytes in header
|
||||
const maxNullByteRatioInFile = 0.05 // Max 5% null bytes in the entire file
|
||||
const maxNonPrintableRuneRatio = 0.05 // Max 5% non-printable runes in the entire file
|
||||
|
||||
// Open file
|
||||
f, err := os.Open(realPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Read header
|
||||
headerBytes := make([]byte, headerSize)
|
||||
n, err := f.Read(headerBytes)
|
||||
if err != nil && err != io.EOF {
|
||||
return "", err
|
||||
}
|
||||
actualHeader := headerBytes[:n]
|
||||
|
||||
// --- Start of new heuristic checks ---
|
||||
|
||||
if n > 0 {
|
||||
// 1. Basic Check: Is the header valid UTF-8?
|
||||
// If not, it's unlikely an editable UTF-8 text file.
|
||||
if !utf8.Valid(actualHeader) {
|
||||
return "", nil // Not an error, just not the text file we want
|
||||
}
|
||||
|
||||
// 2. Check for excessive null bytes in the header
|
||||
nullCountInHeader := 0
|
||||
for _, b := range actualHeader {
|
||||
if b == 0x00 {
|
||||
nullCountInHeader++
|
||||
}
|
||||
}
|
||||
// Reject if too many nulls absolutely or relatively in the header
|
||||
if nullCountInHeader > 0 { // Only perform check if there are any nulls
|
||||
if nullCountInHeader > maxNullBytesInHeaderAbs ||
|
||||
(float64(nullCountInHeader)/float64(n) > maxNullByteRatioInHeader) {
|
||||
return "", nil // Too many nulls in header
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check for other non-text ASCII control characters in the header
|
||||
// (C0 controls excluding \t, \n, \r)
|
||||
for _, b := range actualHeader {
|
||||
if b < 0x20 && b != '\t' && b != '\n' && b != '\r' {
|
||||
return "", nil // Found problematic control character
|
||||
}
|
||||
// C1 control characters (0x80-0x9F) would be caught by utf8.Valid if part of invalid sequences,
|
||||
// or by the non-printable rune check later if they form valid (but undesirable) codepoints.
|
||||
}
|
||||
|
||||
// Optional: Use http.DetectContentType for an additional check on the header
|
||||
// contentType := http.DetectContentType(actualHeader)
|
||||
// if !strings.HasPrefix(contentType, "text/") && contentType != "application/octet-stream" {
|
||||
// // If it's clearly a non-text MIME type (e.g., "image/jpeg"), reject it.
|
||||
// // "application/octet-stream" is ambiguous, so we rely on other heuristics.
|
||||
// return "", nil
|
||||
// }
|
||||
}
|
||||
// --- End of new heuristic checks for header ---
|
||||
|
||||
// Now read the full file (original logic)
|
||||
content, err := os.ReadFile(realPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Check if the file content is valid UTF-8.
|
||||
// This allows for all characters, including ASCII and emojis.
|
||||
if !utf8.Valid(content) {
|
||||
// File is not valid text (e.g., a binary image), so we reject it.
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Handle the special case for an empty file, as in your original code.
|
||||
// Handle empty file (original logic - returns specific string)
|
||||
if len(content) == 0 {
|
||||
return "empty-file-x6OlSil", nil
|
||||
}
|
||||
|
||||
// The file is valid, so return its string content.
|
||||
return string(content), nil
|
||||
stringContent := string(content)
|
||||
|
||||
// 4. Final UTF-8 validation for the entire file
|
||||
// (This is crucial as the header might be fine, but the rest of the file isn't)
|
||||
if !utf8.ValidString(stringContent) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// 5. Check for excessive null bytes in the entire file content
|
||||
if len(content) > 0 { // Check only for non-empty files
|
||||
totalNullCount := 0
|
||||
for _, b := range content {
|
||||
if b == 0x00 {
|
||||
totalNullCount++
|
||||
}
|
||||
}
|
||||
if float64(totalNullCount)/float64(len(content)) > maxNullByteRatioInFile {
|
||||
return "", nil // Too many nulls in the entire file
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Check for excessive non-printable runes in the entire file content
|
||||
// (Excluding tab, newline, carriage return, which are common in text files)
|
||||
if len(stringContent) > 0 { // Check only for non-empty strings
|
||||
nonPrintableRuneCount := 0
|
||||
totalRuneCount := 0
|
||||
for _, r := range stringContent {
|
||||
totalRuneCount++
|
||||
// unicode.IsPrint includes letters, numbers, punctuation, symbols, and spaces.
|
||||
// It excludes control characters. We explicitly allow \t, \n, \r.
|
||||
if !unicode.IsPrint(r) && r != '\t' && r != '\n' && r != '\r' {
|
||||
nonPrintableRuneCount++
|
||||
}
|
||||
}
|
||||
|
||||
if totalRuneCount > 0 { // Avoid division by zero
|
||||
if float64(nonPrintableRuneCount)/float64(totalRuneCount) > maxNonPrintableRuneRatio {
|
||||
return "", nil // Too many non-printable runes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The file has passed all checks and is considered editable text.
|
||||
return stringContent, nil
|
||||
}
|
||||
|
||||
func IsNamedPipe(mode os.FileMode) bool {
|
||||
|
||||
@@ -38,9 +38,10 @@ type PasswordAuthConfig struct {
|
||||
}
|
||||
|
||||
type ProxyAuthConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
CreateUser bool `json:"createUser"` // create user if not exists
|
||||
Header string `json:"header"` // required header to use for authentication. Security Warning: FileBrowser blindly accepts the header value as username.
|
||||
Enabled bool `json:"enabled"`
|
||||
CreateUser bool `json:"createUser"` // create user if not exists
|
||||
Header string `json:"header"` // required header to use for authentication. Security Warning: FileBrowser blindly accepts the header value as username.
|
||||
LogoutRedirectUrl string `json:"logoutRedirectUrl"` // if provider logout url is provided, filebrowser will also redirect to logout url. Custom logout query params are respected.
|
||||
}
|
||||
|
||||
type Recaptcha struct {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -279,10 +280,15 @@ func loadEnvConfig() {
|
||||
}
|
||||
|
||||
func setDefaults() Settings {
|
||||
// get number of CPUs available
|
||||
numCpus := 4 // default to 4 CPUs if runtime.NumCPU() fails or is not available
|
||||
if cpus := runtime.NumCPU(); cpus > 0 {
|
||||
numCpus = cpus
|
||||
}
|
||||
return Settings{
|
||||
Server: Server{
|
||||
Port: 80,
|
||||
NumImageProcessors: 4,
|
||||
NumImageProcessors: numCpus,
|
||||
BaseURL: "",
|
||||
Database: "database.db",
|
||||
SourceMap: map[string]Source{},
|
||||
|
||||
@@ -21,7 +21,7 @@ type Settings struct {
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
NumImageProcessors int `json:"numImageProcessors"` // number of concurrent image processing jobs
|
||||
NumImageProcessors int `json:"numImageProcessors"` // number of concurrent image processing jobs used to create previews, default is number of cpu cores available.
|
||||
Socket string `json:"socket"` // socket to listen on
|
||||
TLSKey string `json:"tlsKey"` // path to TLS key
|
||||
TLSCert string `json:"tlsCert"` // path to TLS cert
|
||||
@@ -42,6 +42,7 @@ type Server struct {
|
||||
SourceMap map[string]Source `json:"-" validate:"omitempty"` // uses realpath as key
|
||||
NameToSource map[string]Source `json:"-" validate:"omitempty"` // uses name as key
|
||||
DefaultSource Source `json:"-" validate:"omitempty"`
|
||||
PdfAvailable bool `json:"-"` // used internally if compiled with pdf support
|
||||
}
|
||||
|
||||
type Integrations struct {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/go-cache/cache"
|
||||
)
|
||||
|
||||
var (
|
||||
DiskUsageCache = cache.NewCache(30*time.Second, 24*time.Hour)
|
||||
RealPathCache = cache.NewCache(48*time.Hour, 72*time.Hour)
|
||||
SearchResultsCache = cache.NewCache(15*time.Second, 1*time.Hour)
|
||||
OnlyOfficeCache = cache.NewCache(48*time.Hour, 1*time.Hour)
|
||||
JwtCache = cache.NewCache(1*time.Hour, 72*time.Hour)
|
||||
)
|
||||
+4
-1
@@ -7,8 +7,9 @@ require (
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dsoprea/go-exif/v3 v3.0.1
|
||||
github.com/gen2brain/go-fitz v1.24.14
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/goccy/go-yaml v1.17.1
|
||||
github.com/goccy/go-yaml v1.18.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/gtsteffaniak/go-cache v0.0.0-20250521142451-edc77dfcb063
|
||||
@@ -70,6 +71,7 @@ require (
|
||||
github.com/denis-tingaikin/go-header v0.5.0 // indirect
|
||||
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/ettle/strcase v0.2.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
@@ -125,6 +127,7 @@ require (
|
||||
github.com/jjti/go-spancheck v0.6.4 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/julz/importas v0.2.0 // indirect
|
||||
github.com/jupiterrider/ffi v0.4.1 // indirect
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect
|
||||
github.com/kisielk/errcheck v1.9.0 // indirect
|
||||
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
|
||||
|
||||
+8
-2
@@ -168,6 +168,8 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003142440-7a1927d49d9d/go.mod h1:LV
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU=
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw=
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@@ -188,6 +190,8 @@ github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
|
||||
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gen2brain/go-fitz v1.24.14 h1:09weRkjVtLYNGo7l0J7DyOwBExbwi8SJ9h8YPhw9WEo=
|
||||
github.com/gen2brain/go-fitz v1.24.14/go.mod h1:0KaZeQgASc20Yp5R/pFzyy7SmP01XcoHKNF842U2/S4=
|
||||
github.com/ghostiam/protogetter v0.3.9 h1:j+zlLLWzqLay22Cz/aYwTHKQ88GE2DQ6GkWSYFOI4lQ=
|
||||
github.com/ghostiam/protogetter v0.3.9/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA=
|
||||
github.com/go-critic/go-critic v0.12.0 h1:iLosHZuye812wnkEz1Xu3aBwn5ocCPfc9yqmFG9pa6w=
|
||||
@@ -260,8 +264,8 @@ github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUW
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
|
||||
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
@@ -408,6 +412,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ=
|
||||
github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY=
|
||||
github.com/jupiterrider/ffi v0.4.1 h1:Obg+xpuA+rRkRTEN7xu3IagjNDMO8fRUhOPpLEztPYI=
|
||||
github.com/jupiterrider/ffi v0.4.1/go.mod h1:Ba3hjU7Sz2+TO8HupjZwGssVnzo7RG3xF77byUBueB4=
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.1 h1:wmZaZYIjnJ0b5UoKDjUHrikcV0zuPyyxI4SVplLd2CI=
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.1/go.mod h1:nFmMlFNlClC2BPvNaHMdkirmTJxVCY0lhxBtlfOypMM=
|
||||
github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M=
|
||||
|
||||
+19
-7
@@ -124,7 +124,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (in
|
||||
return printToken(w, r, d.user) // Pass the data object
|
||||
}
|
||||
|
||||
// logoutHandler handles user logout, specifically used for OIDC.
|
||||
// logoutHandler handles user logout, specifically used for OIDC or proxy users.
|
||||
// @Summary User Logout
|
||||
// @Description logs a user out of the application.
|
||||
// @Tags Auth
|
||||
@@ -136,14 +136,26 @@ func logoutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (i
|
||||
origin = fmt.Sprintf("%s://%s", getScheme(r), r.Host)
|
||||
}
|
||||
logoutUrl := fmt.Sprintf("%s/login", origin)
|
||||
proxyRedirectUrl := config.Auth.Methods.ProxyAuth.LogoutRedirectUrl
|
||||
oidcRedirectUrl := settings.Config.Auth.Methods.OidcAuth.LogoutRedirectUrl
|
||||
proxyUser := r.Header.Get(config.Auth.Methods.ProxyAuth.Header)
|
||||
|
||||
oidcCfg := settings.Config.Auth.Methods.OidcAuth
|
||||
if oidcCfg.Enabled && oidcCfg.LogoutRedirectUrl != "" {
|
||||
logoutUrl = oidcCfg.LogoutRedirectUrl
|
||||
http.Redirect(w, r, logoutUrl, http.StatusFound)
|
||||
return 302, nil
|
||||
if config.Auth.Methods.ProxyAuth.Enabled && proxyUser != "" {
|
||||
if proxyRedirectUrl != "" {
|
||||
logoutUrl = proxyRedirectUrl
|
||||
}
|
||||
} else {
|
||||
if oidcRedirectUrl != "" {
|
||||
logoutUrl = oidcRedirectUrl
|
||||
}
|
||||
}
|
||||
|
||||
// clear cookie
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "auth",
|
||||
Value: "",
|
||||
MaxAge: -1,
|
||||
Path: "/",
|
||||
})
|
||||
http.Redirect(w, r, logoutUrl, http.StatusFound)
|
||||
return 302, nil
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jwt "github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/files"
|
||||
@@ -17,12 +16,9 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
|
||||
"github.com/gtsteffaniak/go-cache/cache"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
)
|
||||
|
||||
var OnlyOfficeCache = cache.NewCache(48 * time.Hour)
|
||||
|
||||
const (
|
||||
onlyOfficeStatusDocumentClosedWithChanges = 2
|
||||
onlyOfficeStatusDocumentClosedWithNoChanges = 4
|
||||
@@ -199,7 +195,7 @@ func getOnlyOfficeId(source, path string) (string, error) {
|
||||
realpath, _, _ := idx.GetRealPath(path)
|
||||
// error is intentionally ignored in order treat errors
|
||||
// the same as a cache-miss
|
||||
cachedDocumentKey, ok := OnlyOfficeCache.Get(realpath).(string)
|
||||
cachedDocumentKey, ok := utils.OnlyOfficeCache.Get(realpath).(string)
|
||||
if ok {
|
||||
return cachedDocumentKey, nil
|
||||
}
|
||||
@@ -213,7 +209,7 @@ func deleteOfficeId(source, path string) {
|
||||
return
|
||||
}
|
||||
realpath, _, _ := idx.GetRealPath(path)
|
||||
OnlyOfficeCache.Delete(realpath)
|
||||
utils.OnlyOfficeCache.Delete(realpath)
|
||||
}
|
||||
|
||||
func onlyofficeGetTokenHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
|
||||
@@ -58,6 +58,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
|
||||
"OidcAvailable": config.Auth.Methods.OidcAuth.Enabled,
|
||||
"PasswordAvailable": config.Auth.Methods.PasswordAuth.Enabled,
|
||||
"MediaAvailable": config.Integrations.Media.FfmpegPath != "",
|
||||
"PdfAvailable": config.Server.PdfAvailable,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(data)
|
||||
|
||||
@@ -20,6 +20,25 @@ var AllFiletypeOptions = []string{
|
||||
"text",
|
||||
}
|
||||
|
||||
var PdfConvertable = []string{
|
||||
".pdf", // PDF
|
||||
".xps", // XPS
|
||||
".epub", // EPUB
|
||||
".mobi", // MOBI
|
||||
".fb2", // FB2
|
||||
".cbz", // CBZ
|
||||
".svg", // SVG
|
||||
".txt", // TXT
|
||||
".doc", // DOC
|
||||
".docx", // DOCX
|
||||
".ppt", // PPT
|
||||
".pptx", // PPTX
|
||||
".xls", // XLS
|
||||
".xlsx", // XLSX
|
||||
".hwp", // HWP
|
||||
".hwp", // HWPX
|
||||
}
|
||||
|
||||
// Known bundle-style extensions that are technically directories but treated as files
|
||||
var BundleExtensions = []string{
|
||||
".app", // macOS application bundle
|
||||
@@ -355,3 +374,16 @@ func hasBundleExtension(name string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func HasDocConvertableExtension(name string) bool {
|
||||
if !settings.Config.Server.PdfAvailable {
|
||||
return false
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
for _, e := range PdfConvertable {
|
||||
if ext == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -3,18 +3,15 @@ package indexing
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
|
||||
"github.com/gtsteffaniak/go-cache/cache"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
)
|
||||
|
||||
var DiskUsageCache = cache.NewCache(30 * time.Second)
|
||||
|
||||
// UpdateFileMetadata updates the FileInfo for the specified directory in the index.
|
||||
func (idx *Index) UpdateMetadata(info *iteminfo.FileInfo) bool {
|
||||
idx.mu.Lock()
|
||||
@@ -108,7 +105,7 @@ func GetIndexInfo(sourceName string) (ReducedIndex, error) {
|
||||
}
|
||||
sourcePath := idx.Path
|
||||
cacheKey := "usageCache-" + sourceName
|
||||
_, ok = DiskUsageCache.Get(cacheKey).(bool)
|
||||
_, ok = utils.DiskUsageCache.Get(cacheKey).(bool)
|
||||
if !ok {
|
||||
usage, err := disk.Usage(sourcePath)
|
||||
if err != nil {
|
||||
@@ -121,7 +118,7 @@ func GetIndexInfo(sourceName string) (ReducedIndex, error) {
|
||||
Used: usage.Used,
|
||||
}
|
||||
idx.SetUsage(latestUsage)
|
||||
DiskUsageCache.Set(cacheKey, true)
|
||||
utils.DiskUsageCache.Set(cacheKey, true)
|
||||
}
|
||||
return idx.ReducedIndex, nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//go:build pdf
|
||||
// +build pdf
|
||||
|
||||
package preview
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image/jpeg"
|
||||
|
||||
"github.com/gen2brain/go-fitz"
|
||||
)
|
||||
|
||||
func docEnabled() bool {
|
||||
// This function checks if the PDF support is enabled.
|
||||
// In a real implementation, this might check a build tag or configuration.
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Service) GenerateImageFromDoc(docPath string, pageNumber int) ([]byte, error) {
|
||||
if err := s.acquire(context.Background()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.release()
|
||||
doc, err := fitz.New(docPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open PDF: %w", err)
|
||||
}
|
||||
defer doc.Close()
|
||||
|
||||
// Get the image from the doc page
|
||||
img, err := doc.Image(pageNumber) // Assuming page numbers are 0-indexed as per go-fitz common usage
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get image from page %d: %w", pageNumber, err)
|
||||
}
|
||||
|
||||
// Create a new buffer to hold the image bytes
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Encode the image directly into the buffer
|
||||
err = jpeg.Encode(&buf, img, &jpeg.Options{Quality: jpeg.DefaultQuality})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode image to jpeg: %w", err)
|
||||
}
|
||||
|
||||
// Return the byte slice from the buffer
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//go:build !pdf
|
||||
// +build !pdf
|
||||
|
||||
package preview
|
||||
|
||||
func docEnabled() bool {
|
||||
// This function checks if the PDF support is enabled.
|
||||
// In a real implementation, this might check a build tag or configuration.
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Service) GenerateImageFromDoc(pdfPath string, pageNumber int) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -119,8 +119,8 @@ func WithQuality(quality Quality) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Resize(ctx context.Context, in io.Reader, width, height int, out io.Writer, options ...Option) error {
|
||||
if err := s.acquire(ctx); err != nil {
|
||||
func (s *Service) Resize(in io.Reader, width, height int, out io.Writer, options ...Option) error {
|
||||
if err := s.acquire(context.Background()); err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.release()
|
||||
|
||||
@@ -2,7 +2,6 @@ package preview
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"image"
|
||||
"image/gif"
|
||||
@@ -282,7 +281,7 @@ func TestService_Resize(t *testing.T) {
|
||||
defer source.Close()
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
err := svc.Resize(context.Background(), source, test.width, test.height, buf, test.options...)
|
||||
err := svc.Resize(source, test.width, test.height, buf, test.options...)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Fatalf("GetMarketSpecs() error = %v, wantErr %v", err, test.wantErr)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package preview
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -21,6 +22,10 @@ type officePreviewResponse struct {
|
||||
|
||||
// GenerateOfficePreview generates a preview for an office document using OnlyOffice.
|
||||
func (s *Service) GenerateOfficePreview(filetype, key, title, url string) ([]byte, error) {
|
||||
if err := s.acquire(context.Background()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.release()
|
||||
data := []byte{}
|
||||
// Create the request payload
|
||||
requestPayload := map[string]interface{}{
|
||||
|
||||
+12
-18
@@ -63,6 +63,7 @@ func NewPreviewGenerator(concurrencyLimit int, ffmpegPath string, cacheDir strin
|
||||
logger.Infof("Media Enabled : %v", errprobe == nil)
|
||||
settings.Config.Integrations.Media.FfmpegPath = filepath.Base(ffmpegMainPath)
|
||||
}
|
||||
settings.Config.Server.PdfAvailable = docEnabled()
|
||||
return &Service{
|
||||
sem: make(chan struct{}, concurrencyLimit),
|
||||
ffmpegPath: ffmpegMainPath,
|
||||
@@ -99,7 +100,12 @@ func GeneratePreview(file iteminfo.ExtendedFileInfo, previewSize, officeUrl stri
|
||||
)
|
||||
|
||||
// Generate an image from office document
|
||||
if file.OnlyOfficeId != "" {
|
||||
if iteminfo.HasDocConvertableExtension(file.Name) {
|
||||
imageBytes, err = service.GenerateImageFromDoc(file.RealPath, 0) // 0 for the first page
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create image for PDF file: %w", err)
|
||||
}
|
||||
} else if file.OnlyOfficeId != "" {
|
||||
imageBytes, err = service.GenerateOfficePreview(filepath.Ext(file.Name), file.OnlyOfficeId, file.Name, officeUrl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create image for office file: %w", err)
|
||||
@@ -118,14 +124,11 @@ func GeneratePreview(file iteminfo.ExtendedFileInfo, previewSize, officeUrl stri
|
||||
hasher := sha1.New() //nolint:gosec
|
||||
_, _ = hasher.Write([]byte(CacheKey(file.RealPath, previewSize, file.ItemInfo.ModTime, seekPercentage)))
|
||||
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||
outPathPattern := filepath.Join(settings.Config.Server.CacheDir, "thumbnails", "video", hash) + ".jpg"
|
||||
outPathPattern := filepath.Join(settings.Config.Server.CacheDir, "thumbnails", "video", hash)
|
||||
defer os.Remove(outPathPattern) // cleanup
|
||||
if err = service.GenerateVideoPreview(file.RealPath, outPathPattern, seekPercentage); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate video preview: %w", err)
|
||||
}
|
||||
imageBytes, err = os.ReadFile(outPathPattern)
|
||||
imageBytes, err = service.GenerateVideoPreview(file.RealPath, outPathPattern, seekPercentage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read video thumbnail: %w", err)
|
||||
return nil, fmt.Errorf("failed to create image for video file: %w", err)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported media type: %s", ext)
|
||||
@@ -163,7 +166,7 @@ func (s *Service) CreatePreview(data []byte, previewSize string) ([]byte, error)
|
||||
input := bytes.NewReader(data)
|
||||
output := &bytes.Buffer{}
|
||||
|
||||
if err := s.Resize(context.Background(), input, width, height, output, options...); err != nil {
|
||||
if err := s.Resize(input, width, height, output, options...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -188,9 +191,6 @@ func AvailablePreview(file iteminfo.ExtendedFileInfo) bool {
|
||||
if strings.HasPrefix(file.Type, "video") && service.ffmpegPath != "" {
|
||||
return true
|
||||
}
|
||||
if file.OnlyOfficeId != "" {
|
||||
return true
|
||||
}
|
||||
if file.Type == "application/pdf" {
|
||||
return true
|
||||
}
|
||||
@@ -199,13 +199,7 @@ func AvailablePreview(file iteminfo.ExtendedFileInfo) bool {
|
||||
case ".jpg", ".jpeg", ".png", ".bmp", ".tiff":
|
||||
return true
|
||||
}
|
||||
if file.OnlyOfficeId != "" {
|
||||
return true
|
||||
}
|
||||
if file.Type == "application/pdf" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return file.OnlyOfficeId != ""
|
||||
}
|
||||
|
||||
// CheckValidFFmpeg checks for a valid ffmpeg executable.
|
||||
|
||||
@@ -2,6 +2,7 @@ package preview
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -15,7 +16,11 @@ import (
|
||||
// videoPath: path to the input video file.
|
||||
// outputPath: path where the generated preview image will be saved (e.g., "/tmp/preview.jpg").
|
||||
// seekTime: how many seconds into the video to seek before capturing the frame.
|
||||
func (s *Service) GenerateVideoPreview(videoPath, outputPath string, percentageSeek int) error {
|
||||
func (s *Service) GenerateVideoPreview(videoPath, outputPath string, percentageSeek int) ([]byte, error) {
|
||||
if err := s.acquire(context.Background()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.release()
|
||||
// Step 1: Get video duration from the container format
|
||||
probeCmd := exec.Command(
|
||||
s.ffprobePath,
|
||||
@@ -33,23 +38,23 @@ func (s *Service) GenerateVideoPreview(videoPath, outputPath string, percentageS
|
||||
}
|
||||
if err := probeCmd.Run(); err != nil {
|
||||
logger.Errorf("ffprobe command failed on file '%v' : %v", videoPath, err)
|
||||
return fmt.Errorf("ffprobe failed: %w", err)
|
||||
return nil, fmt.Errorf("ffprobe failed: %w", err)
|
||||
}
|
||||
|
||||
durationStr := strings.TrimSpace(probeOut.String())
|
||||
if durationStr == "" || durationStr == "N/A" {
|
||||
logger.Errorf("could not determine video duration for file '%v' using duration info '%v'", videoPath, durationStr)
|
||||
return fmt.Errorf("could not determine video duration")
|
||||
return nil, fmt.Errorf("could not determine video duration")
|
||||
}
|
||||
|
||||
durationFloat, err := strconv.ParseFloat(durationStr, 64)
|
||||
if err != nil {
|
||||
// The original error you saw would be caught here if "N/A" was still the output
|
||||
return fmt.Errorf("invalid duration: %v", err)
|
||||
return nil, fmt.Errorf("invalid duration: %v", err)
|
||||
}
|
||||
|
||||
if durationFloat <= 0 {
|
||||
return fmt.Errorf("video duration must be positive")
|
||||
return nil, fmt.Errorf("video duration must be positive")
|
||||
}
|
||||
|
||||
// The rest of your function remains the same...
|
||||
@@ -76,6 +81,9 @@ func (s *Service) GenerateVideoPreview(videoPath, outputPath string, percentageS
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
}
|
||||
|
||||
return cmd.Run()
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ffmpeg command failed on file '%v' : %w", videoPath, err)
|
||||
}
|
||||
return os.ReadFile(outputPath)
|
||||
}
|
||||
@@ -2006,6 +2006,10 @@ const docTemplate = `{
|
||||
"header": {
|
||||
"description": "required header to use for authentication. Security Warning: FileBrowser blindly accepts the header value as username.",
|
||||
"type": "string"
|
||||
},
|
||||
"logoutRedirectUrl": {
|
||||
"description": "if provider logout url is provided, filebrowser will also redirect to logout url. Custom logout query params are respected.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2081,7 +2085,7 @@ const docTemplate = `{
|
||||
"type": "integer"
|
||||
},
|
||||
"numImageProcessors": {
|
||||
"description": "number of concurrent image processing jobs",
|
||||
"description": "number of concurrent image processing jobs used to create previews, default is number of cpu cores available.",
|
||||
"type": "integer"
|
||||
},
|
||||
"port": {
|
||||
|
||||
@@ -1995,6 +1995,10 @@
|
||||
"header": {
|
||||
"description": "required header to use for authentication. Security Warning: FileBrowser blindly accepts the header value as username.",
|
||||
"type": "string"
|
||||
},
|
||||
"logoutRedirectUrl": {
|
||||
"description": "if provider logout url is provided, filebrowser will also redirect to logout url. Custom logout query params are respected.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2070,7 +2074,7 @@
|
||||
"type": "integer"
|
||||
},
|
||||
"numImageProcessors": {
|
||||
"description": "number of concurrent image processing jobs",
|
||||
"description": "number of concurrent image processing jobs used to create previews, default is number of cpu cores available.",
|
||||
"type": "integer"
|
||||
},
|
||||
"port": {
|
||||
|
||||
@@ -285,6 +285,10 @@ definitions:
|
||||
description: 'required header to use for authentication. Security Warning:
|
||||
FileBrowser blindly accepts the header value as username.'
|
||||
type: string
|
||||
logoutRedirectUrl:
|
||||
description: if provider logout url is provided, filebrowser will also redirect
|
||||
to logout url. Custom logout query params are respected.
|
||||
type: string
|
||||
type: object
|
||||
settings.Recaptcha:
|
||||
properties:
|
||||
@@ -341,7 +345,8 @@ definitions:
|
||||
to be archived (in GB)
|
||||
type: integer
|
||||
numImageProcessors:
|
||||
description: number of concurrent image processing jobs
|
||||
description: number of concurrent image processing jobs used to create previews,
|
||||
default is number of cpu cores available.
|
||||
type: integer
|
||||
port:
|
||||
description: port to listen on
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
server:
|
||||
numImageProcessors: 4 # number of concurrent image processing jobs
|
||||
numImageProcessors: 14 # number of concurrent image processing jobs used to create previews, default is number of cpu cores available.
|
||||
socket: "" # socket to listen on
|
||||
tlsKey: "" # path to TLS key
|
||||
tlsCert: "" # path to TLS cert
|
||||
@@ -49,6 +49,7 @@ auth:
|
||||
enabled: false
|
||||
createUser: false # create user if not exists
|
||||
header: "" # required header to use for authentication. Security Warning: FileBrowser blindly accepts the header value as username.
|
||||
logoutRedirectUrl: "" # if provider logout url is provided, filebrowser will also redirect to logout url. Custom logout query params are respected.
|
||||
noauth: false # if set to true, overrides all other auth methods and disables authentication
|
||||
password: # validate:omitempty
|
||||
enabled: true
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.5 KiB |
@@ -19,7 +19,8 @@
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="isCardView" class="gallery-size">
|
||||
{{ $t('files.size') }}<span class="sr-only">:</span> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
{{ $t("files.size") }}<span class="sr-only">:</span>
|
||||
<!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<input
|
||||
v-model="gallerySize"
|
||||
type="range"
|
||||
@@ -215,6 +216,7 @@ export default {
|
||||
}
|
||||
|
||||
.gallery-size {
|
||||
display: flex;
|
||||
background: var(--alt-background);
|
||||
width: fit-content;
|
||||
padding: 0.5em;
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
:aria-label="baseName(s.path)"
|
||||
>
|
||||
<a :href="getRelative(s.path)" @contextmenu="addSelected(event, s)">
|
||||
<Icon :mimetype="s.type" />
|
||||
<Icon :mimetype="s.type" :filename="s.name" />
|
||||
<span class="text-container">
|
||||
{{ basePath(s.path, s.type == "directory") }}/{{ baseName(s.path) }}/<!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<span v-if="isPreviewImg">
|
||||
<span v-if="isPreviewImg && imageState !== 'error'">
|
||||
<i
|
||||
v-if="hasMotion"
|
||||
class="material-icons"
|
||||
@@ -7,27 +7,35 @@
|
||||
>animation</i
|
||||
>
|
||||
<img
|
||||
:key="thumbnailUrl"
|
||||
:src="imageDisplaySrc"
|
||||
@mouseenter="handleMouseEnter($event)"
|
||||
@mouseleave="handleMouseLeave($event)"
|
||||
v-lazy="thumbnailUrl"
|
||||
:src="currentThumbnail"
|
||||
class="icon"
|
||||
ref="thumbnail"
|
||||
/>
|
||||
</span>
|
||||
<span v-else>
|
||||
<!-- Material Icon -->
|
||||
<i :class="[classes, { active: active }]" class="icon"> {{ materialIcon }} </i>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { onlyOfficeUrl, mediaAvailable } from "@/utils/constants";
|
||||
import { onlyOfficeUrl, mediaAvailable, pdfAvailable, baseURL } from "@/utils/constants";
|
||||
import { getTypeInfo } from "@/utils/mimetype";
|
||||
import { mutations, state } from "@/store";
|
||||
|
||||
// NEW: Define placeholder and error image URLs for easy configuration
|
||||
const PLACEHOLDER_URL = baseURL + "static/img/placeholder.png"; // A generic loading placeholder
|
||||
const ERROR_URL = baseURL + "static/img/placeholder.png";
|
||||
|
||||
export default {
|
||||
name: "Icon",
|
||||
props: {
|
||||
filename: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
mimetype: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -45,11 +53,46 @@ export default {
|
||||
materialIcon: "",
|
||||
classes: "",
|
||||
svgPath: "",
|
||||
previewTimeouts: [], // Store timeout IDs
|
||||
currentThumbnail: this.thumbnailUrl,
|
||||
previewTimeouts: [],
|
||||
// UPDATED: Manage image state directly
|
||||
imageState: "loading", // Can be 'loading', 'loaded', or 'error'
|
||||
imageTargetSrc: this.thumbnailUrl, // The URL we currently want to display
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
pdfConvertable() {
|
||||
const ext = "." + this.filename.split(".").pop().toLowerCase(); // Ensure lowercase and dot
|
||||
const pdfConvertCompatibleFileExtensions = {
|
||||
".pdf": true,
|
||||
".xps": true,
|
||||
".epub": true,
|
||||
".mobi": true,
|
||||
".fb2": true,
|
||||
".cbz": true,
|
||||
".svg": true,
|
||||
".txt": true,
|
||||
".doc": true,
|
||||
".docx": true,
|
||||
".ppt": true,
|
||||
".pptx": true,
|
||||
".xls": true,
|
||||
".xlsx": true,
|
||||
".hwp": true,
|
||||
".hwpx": true, // fix duplication and add this one
|
||||
};
|
||||
return !!pdfConvertCompatibleFileExtensions[ext];
|
||||
},
|
||||
// NEW: A single computed property to determine the final image src
|
||||
imageDisplaySrc() {
|
||||
if (this.imageState === "error") {
|
||||
return ERROR_URL;
|
||||
}
|
||||
// Show placeholder only for the initial load, not during hover animations
|
||||
if (this.imageState === "loading" && this.imageTargetSrc === this.thumbnailUrl) {
|
||||
return PLACEHOLDER_URL;
|
||||
}
|
||||
return this.imageTargetSrc;
|
||||
},
|
||||
showLarger() {
|
||||
return state.user.viewMode === "gallery" || state.user.viewMode === "normal";
|
||||
},
|
||||
@@ -68,10 +111,12 @@ export default {
|
||||
if (this.thumbnailUrl == "") {
|
||||
return false;
|
||||
}
|
||||
// todo support webp previews
|
||||
if (this.mimetype == "text/csv") {
|
||||
return false;
|
||||
}
|
||||
if (this.pdfConvertable && pdfAvailable) {
|
||||
return true;
|
||||
}
|
||||
if (this.getIconForType().simpleType === "image" && state.user.preview?.image) {
|
||||
return true;
|
||||
}
|
||||
@@ -89,25 +134,41 @@ export default {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
this.getIconForType().simpleType === "pdf" &&
|
||||
state.user.preview?.office &&
|
||||
onlyOfficeUrl != ""
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleMouseEnter() {
|
||||
if (state.user.viewMode === "gallery" && !state.user.preview.highQuality) {
|
||||
// NEW: Centralized method to load any image and handle its state
|
||||
loadImage(url) {
|
||||
if (!url) {
|
||||
this.imageState = "error";
|
||||
return;
|
||||
}
|
||||
|
||||
mutations.setPreviewSource(this.thumbnailUrl);
|
||||
this.imageState = "loading";
|
||||
const targetImage = new Image();
|
||||
|
||||
if (!state.user.preview.motionVideoPreview) {
|
||||
targetImage.onload = () => {
|
||||
// Prevent race conditions: only update if this is the image we still want.
|
||||
if (this.imageTargetSrc === url) {
|
||||
this.imageState = "loaded";
|
||||
}
|
||||
};
|
||||
|
||||
targetImage.onerror = () => {
|
||||
// Prevent race conditions: only show an error if this is the image that failed.
|
||||
if (this.imageTargetSrc === url) {
|
||||
this.imageState = "error";
|
||||
}
|
||||
};
|
||||
|
||||
targetImage.src = url;
|
||||
},
|
||||
handleMouseEnter() {
|
||||
if (this.imageState == "loaded") {
|
||||
mutations.setPreviewSource(this.thumbnailUrl);
|
||||
}
|
||||
if (!state.user.preview.motionVideoPreview || !this.hasMotion) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -117,64 +178,62 @@ export default {
|
||||
this.thumbnailUrl + "&atPercentage=50",
|
||||
this.thumbnailUrl + "&atPercentage=75",
|
||||
];
|
||||
|
||||
let index = 0;
|
||||
|
||||
const updateThumbnailUrl = () => {
|
||||
const img = new Image();
|
||||
const updateThumbnail = () => {
|
||||
if (state.popupPreviewSource === "") {
|
||||
this.previewTimeouts.forEach(clearTimeout);
|
||||
this.previewTimeouts = [];
|
||||
return;
|
||||
}
|
||||
// Set the thumbnail or popup preview
|
||||
if (state.user.preview.popup) {
|
||||
mutations.setPreviewSource(sequence[index]);
|
||||
} else {
|
||||
this.currentThumbnail = sequence[index];
|
||||
}
|
||||
|
||||
const currentUrl = sequence[index];
|
||||
// Preload the next image
|
||||
const nextIndex = (index + 1) % sequence.length;
|
||||
const preloadImg = new Image();
|
||||
preloadImg.src = sequence[nextIndex];
|
||||
|
||||
img.onload = () => {
|
||||
if (state.popupPreviewSource === "") {
|
||||
this.previewTimeouts.forEach(clearTimeout);
|
||||
this.previewTimeouts = [];
|
||||
return;
|
||||
}
|
||||
// Set the thumbnail or popup preview
|
||||
if (state.user.preview.popup) {
|
||||
mutations.setPreviewSource(currentUrl);
|
||||
} else {
|
||||
this.currentThumbnail = currentUrl;
|
||||
}
|
||||
|
||||
// Preload the next image if it exists
|
||||
const nextIndex = (index + 1) % sequence.length;
|
||||
const nextUrl = sequence[nextIndex];
|
||||
const preloadImg = new Image();
|
||||
preloadImg.src = nextUrl;
|
||||
|
||||
// Schedule next update
|
||||
index = nextIndex;
|
||||
const timeoutId = setTimeout(updateThumbnailUrl, 750);
|
||||
this.previewTimeouts.push(timeoutId);
|
||||
};
|
||||
|
||||
img.src = currentUrl;
|
||||
// Schedule next update
|
||||
index = nextIndex;
|
||||
const timeoutId = setTimeout(updateThumbnail, 750);
|
||||
this.previewTimeouts.push(timeoutId);
|
||||
};
|
||||
updateThumbnailUrl();
|
||||
updateThumbnail();
|
||||
},
|
||||
handleMouseLeave() {
|
||||
this.previewTimeouts.forEach(clearTimeout);
|
||||
this.previewTimeouts = [];
|
||||
mutations.setPreviewSource("");
|
||||
this.currentThumbnail = this.thumbnailUrl;
|
||||
// UPDATED: Reset to the base thumbnail URL. The watcher will handle reloading it.
|
||||
this.imageTargetSrc = this.thumbnailUrl;
|
||||
},
|
||||
getIconForType() {
|
||||
return getTypeInfo(this.mimetype);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// UPDATED: Added a check for isPreviewImg
|
||||
imageTargetSrc: {
|
||||
handler(newSrc) {
|
||||
// ONLY trigger the image loader if the component is meant to show a preview.
|
||||
if (this.isPreviewImg) {
|
||||
this.loadImage(newSrc);
|
||||
}
|
||||
},
|
||||
immediate: true, // Run this watcher on component mount
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const result = this.getIconForType();
|
||||
this.classes = result.classes || "material-icons"; // Default class
|
||||
this.color = result.color || "lightgray"; // Default color
|
||||
this.classes = result.classes || "material-icons";
|
||||
this.color = result.color || "lightgray";
|
||||
this.materialIcon = result.materialIcon || "";
|
||||
this.svgPath = result.svgPath || ""; // For SVG file paths
|
||||
this.svgPath = result.svgPath || "";
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
@mouseup="cancelContext($event)"
|
||||
>
|
||||
<div @click="toggleClick" :class="{ 'gallery-div': galleryView }">
|
||||
<Icon :mimetype="type" :active="isSelected" :thumbnailUrl="thumbnailUrl" />
|
||||
<Icon :mimetype="type" :active="isSelected" :thumbnailUrl="thumbnailUrl" :filename="name" />
|
||||
</div>
|
||||
|
||||
<div class="text" :class="{ activecontent: isMaximized && isSelected }">
|
||||
@@ -48,6 +48,7 @@
|
||||
<Icon
|
||||
@click="downloadFile"
|
||||
v-if="quickDownloadEnabled"
|
||||
:filename="name"
|
||||
mimetype="file_download"
|
||||
style="padding-right: 0.5em"
|
||||
/>
|
||||
|
||||
@@ -113,6 +113,7 @@ export default {
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease-in-out;
|
||||
background: gray;
|
||||
}
|
||||
|
||||
.popup-preview img {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { reactive } from 'vue';
|
||||
import { detectLocale } from "@/i18n";
|
||||
|
||||
export const state = reactive({
|
||||
previousHash: "",
|
||||
multiButtonState: "menu",
|
||||
multiButtonLastState: "menu",
|
||||
showOverflowMenu: false,
|
||||
|
||||
@@ -57,9 +57,7 @@ export function generateRandomCode(length) {
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
if (state.user.loginMethod == "oidc") {
|
||||
document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
|
||||
mutations.setCurrentUser(null);
|
||||
if (state.user.loginMethod == "oidc" || state.user.loginMethod == "proxy") {
|
||||
let apiPath = getApiPath("api/auth/logout")
|
||||
window.location.href = apiPath;
|
||||
return
|
||||
|
||||
@@ -21,6 +21,7 @@ const serverHasMultipleSources = window.FileBrowser.SourceCount > 1;
|
||||
const oidcAvailable = window.FileBrowser.OidcAvailable;
|
||||
const passwordAvailable = window.FileBrowser.PasswordAvailable;
|
||||
const mediaAvailable = window.FileBrowser.MediaAvailable;
|
||||
const pdfAvailable = window.FileBrowser.PdfAvailable;
|
||||
const origin = window.location.origin;
|
||||
|
||||
const settings = [
|
||||
@@ -32,6 +33,7 @@ const settings = [
|
||||
];
|
||||
|
||||
export {
|
||||
pdfAvailable,
|
||||
mediaAvailable,
|
||||
oidcAvailable,
|
||||
passwordAvailable,
|
||||
|
||||
@@ -28,7 +28,7 @@ run: build-frontend
|
||||
else \
|
||||
sed -i '/func init/,+3d' ./swagger/docs/docs.go; \
|
||||
fi && \
|
||||
FILEBROWSER_NO_EMBEDED=true go run \
|
||||
FILEBROWSER_NO_EMBEDED=true go run -tags "pdf" \
|
||||
--ldflags="-w -s -X 'github.com/gtsteffaniak/filebrowser/backend/version.CommitSHA=testingCommit' -X 'github.com/gtsteffaniak/filebrowser/backend/version.Version=testing'" . -c test_config.yaml
|
||||
|
||||
build-frontend:
|
||||
|
||||
Reference in new issue
Block a user