Compare commits

...
49 Commits
Author SHA1 Message Date
Graham Steffaniak c815a9f623 updated with pdf viewer (#730) 2025-06-06 20:15:28 -05:00
Graham Steffaniak 1d5026984b Empty image error (#726) 2025-06-06 18:34:42 -05:00
Graham Steffaniak 7e80179e74 added docx and epub viewer 2025-06-06 17:26:59 -05:00
Graham Steffaniak 4a7390b0dc Merge branch 'dev/v0.7.7' of github.com:gtsteffaniak/filebrowser into dev/v0.7.7 2025-06-06 16:09:56 -05:00
Graham SteffaniakandGilles 50d3513880 Fix sizes icons (#725)
Co-authored-by: Gilles <gilles@wittezaele.fr>
2025-06-06 15:31:03 -05:00
Graham Steffaniak 9a0cd4552d Merge branch 'main' into dev/v0.7.7 2025-06-06 14:50:00 -05:00
Graham Steffaniak b0aa189d34 fix null tag release 2025-06-06 14:37:59 -05:00
Graham Steffaniak fa0900d4f8 guard against null tag 2025-06-06 14:37:14 -05:00
Graham Steffaniak b57c3adaa1 release full version 2025-06-06 14:31:27 -05:00
Graham Steffaniak 282fd6f227 updated with password login fixes (#724) 2025-06-06 14:26:17 -05:00
Graham Steffaniak cb7ad4af92 fix otp issue (#722) 2025-06-05 20:24:19 -05:00
Graham Steffaniak d896858bcd add more preview options (#721) 2025-06-05 17:09:06 -05:00
Graham Steffaniak 712ad91f20 Add zig cross compile (#720) 2025-06-05 15:53:19 -05:00
Graham Steffaniak cfa1537e2e don't update latest 2025-06-04 20:08:38 -05:00
Graham Steffaniak d4e0c38f06 updated release to only do arm64 2025-06-04 20:05:31 -05:00
Graham Steffaniak 305a54ffb6 updated (#718) 2025-06-04 19:30:34 -05:00
Graham Steffaniak 3acdde80ea updated mupdf 2025-06-04 19:14:28 -05:00
Graham Steffaniak a3d8833a2e updated releaser 2025-06-04 18:59:23 -05:00
Graham Steffaniak 421fd126c8 Merge branch 'dev/v0.7.7' of github.com:gtsteffaniak/filebrowser into dev/v0.7.7 2025-06-04 18:42:00 -05:00
Graham Steffaniak dfdc422ccf Rebased commit (#717) 2025-06-04 18:25:36 -05:00
Graham Steffaniak 114d2136bb Goreleaser (#715) 2025-06-04 18:22:27 -05:00
Graham Steffaniak cfc4a1be78 su do 2025-06-04 18:22:27 -05:00
Graham Steffaniak d172c0fa4e updated cross compilers 2025-06-04 18:22:27 -05:00
Graham Steffaniak fe1984f234 Change name (#713) 2025-06-04 18:22:25 -05:00
Graham Steffaniak 27bfee087c updated release tag 2025-06-04 18:22:07 -05:00
Graham Steffaniak dc96f9d0ae updated 2025-06-04 18:22:07 -05:00
Graham Steffaniak 24c83842a5 updated with build-essential 2025-06-04 18:22:07 -05:00
Graham Steffaniak bd822497e1 Build with cgo (#707) 2025-06-04 18:22:07 -05:00
Graham Steffaniak ea9d2fe6df updated with no config (#706) 2025-06-04 18:22:07 -05:00
Graham Steffaniak 11e0681562 updated workflow (#705) 2025-06-04 18:22:07 -05:00
Graham Steffaniak 51d8df62b5 updated with office preview fixes (#703) 2025-06-04 18:22:07 -05:00
Graham Steffaniak 328872a620 Beta/v0.7.6 (#697) 2025-06-04 18:22:05 -05:00
Graham Steffaniak 3de0848fef Goreleaser (#715) 2025-06-04 17:21:21 -05:00
Graham Steffaniak ff829772d5 su do 2025-06-04 16:54:39 -05:00
Graham Steffaniak 8da3a8d14c updated cross compilers 2025-06-04 16:51:43 -05:00
Graham Steffaniak 49ad06fecd Merge branch 'dev/v0.7.7' of github.com:gtsteffaniak/filebrowser into dev/v0.7.7 2025-06-04 16:32:35 -05:00
Graham Steffaniak 24ca0297ad Change name (#713) 2025-06-04 16:26:04 -05:00
Graham Steffaniak 53f2f3c87c updated release tag 2025-06-04 16:16:34 -05:00
Graham Steffaniak 909987a2cf updated 2025-06-04 16:08:02 -05:00
Graham Steffaniak 91566b2721 updated with build-essential 2025-06-04 15:17:20 -05:00
Graham Steffaniak 19ae24637d Build with cgo (#707) 2025-06-04 14:51:33 -05:00
Graham Steffaniak d5354e556c updated with no config (#706) 2025-06-04 12:54:19 -05:00
Graham Steffaniak ff77854ed6 updated workflow (#705) 2025-06-04 11:12:27 -05:00
Graham Steffaniak d53b769e61 updated with office preview fixes (#703) 2025-06-04 09:45:52 -05:00
Graham Steffaniak b111ec66ec updated with slim build (#694) 2025-06-01 16:05:30 -05:00
Graham Steffaniak 9622f1226c Build multiarch (#693) 2025-06-01 14:25:34 -05:00
Graham Steffaniak 9d1e9e0d72 updated dockerfile and workflow (#692) 2025-06-01 12:41:05 -05:00
Graham Steffaniak 90110ac999 updated dockerfile and workflow (#691) 2025-05-31 20:05:11 -05:00
Graham Steffaniak 81cbaf7766 updated with image placeholders (#690) 2025-05-31 19:21:34 -05:00
77 changed files with 1399 additions and 471 deletions

No files matched your search

+1
View File
@@ -1,5 +1,6 @@
backend/**.db
backend/tmp
backend/backend*
backend/vendor/**
frontend/dist/**
frontend/node_modules/**
+1 -1
View File
@@ -81,7 +81,7 @@ jobs:
file: ./_docker/Dockerfile.playwright-noauth
push: false
push_pr_to_registry:
name: Push PR
name: Build docker image
runs-on: ubuntu-latest
steps:
- name: Checkout
+12 -11
View File
@@ -15,17 +15,18 @@ jobs:
go-version: 'stable'
- working-directory: backend
run: go test -race -v ./...
lint-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- uses: golangci/golangci-lint-action@v5
with:
version: 'v1.64'
working-directory: backend
# golanci-lint is currently disabled due to slowness
# lint-backend:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - uses: actions/setup-go@v5
# with:
# go-version: 'stable'
# - uses: golangci/golangci-lint-action@v5
# with:
# version: 'v1.64'
# working-directory: backend
format-backend:
runs-on: ubuntu-latest
steps:
+1 -1
View File
@@ -56,7 +56,7 @@ jobs:
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
file: ./_docker/Dockerfile.slim
file: ./_docker/Dockerfile
push: true
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
labels: ${{ steps.meta.outputs.labels }}
+14 -12
View File
@@ -21,9 +21,8 @@ jobs:
run: npm i && npm run build
working-directory: frontend
- name: Install OS Requirements
run: sudo apt update && sudo apt install -y upx gcc-multilib g++-multilib
- name: Install Cross Compilers
run: sudo apt update && sudo apt install -y gcc-mingw-w64 gcc-aarch64-linux-gnu
run: sudo apt update && sudo apt install -y upx build-essential musl-tools
- uses: goto-bus-stop/setup-zig@v2
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
@@ -65,7 +64,10 @@ jobs:
run: |
JSON='${{ steps.meta.outputs.json }}'
MODIFIED_JSON=$(echo "$JSON" | jq '
.tags |= map(sub(":v"; ":")) |
.tags |= map(
sub(":v"; ":") # Strip the 'v'
| select(test(":latest") | not)
) |
if any(.tags[]; test("beta")) then
.tags += [
"gtstef/filebrowser:beta",
@@ -126,26 +128,26 @@ jobs:
images: |
gtstef/filebrowser
ghcr.io/gtsteffaniak/filebrowser
- name: Modify tags (strip 'v' and add 'beta'/'stable' if needed)
- name: Modify tags (strip 'v' and add 'slim' if needed)
id: modify-json-slim
run: |
JSON='${{ steps.meta.outputs.json }}'
MODIFIED_JSON=$(echo "$JSON" | jq '
.tags |= map(
sub(":v"; ":") # Strip the 'v'
| select(test(":latest") | not)
| sub(":(.+)"; ":\(.captures[0])-slim")
sub(":v"; ":") # Strip the 'v'
| select(test(":latest") | not) # Remove the "latest" tag
| . + "-slim" # Append "-slim" to the versioned tag
) |
if any(.tags[]; test("beta")) then
.tags += [
"gtstef/filebrowser:beta",
"ghcr.io/gtsteffaniak/filebrowser:beta"
"gtstef/filebrowser:beta-slim",
"ghcr.io/gtsteffaniak/filebrowser:beta-slim"
]
else . end |
if any(.tags[]; test("stable")) then
.tags += [
"gtstef/filebrowser:stable",
"ghcr.io/gtsteffaniak/filebrowser:stable"
"gtstef/filebrowser:stable-slim",
"ghcr.io/gtsteffaniak/filebrowser:stable-slim"
]
else . end
')
+1
View File
@@ -10,6 +10,7 @@ rice-box.go
/backend/filebrowser.exe
/backend/backend.exe
/backend/tmp
/backend/dist
/frontend/dist
/frontend/pkg
/frontend/test-results
+24 -4
View File
@@ -2,9 +2,31 @@
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
## v0.7.7-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.
This release cleans up some of the native preview (image preview) feature logic. And adds simple docx and epub viewers as well. Going through all of this, I think I know how I can add full-fledge google doc and microsoft office viewer support (no edit). But, for now "onlyOffice" remains the most comprehensive solution with most compatibility and ability to fully edit. One day, I think I will be able to integrate a minimal license-free server into the docker image. But that's something for another time.
Native preview (image preview) support is also available for linux arm64 and amd64 binaries, and windows exe.
**New Features**:
- since theres a wider kind of document preview types, a new disableOfficePreviewExt option has been added.
- native (and simple) docx and epub viewers.
- Other documents like xlsx get full size image preview when opened and no onlyoffice support.
**Notes**:
- all text mimetype files have preview support.
- high-quality preview image sizes bumped from 512x512 to 640x640 to help make text previews readable.
- no config is allowed and defaults to on source at current directory.
**BugFixes**:
- fix otp clearing on user save https://github.com/gtsteffaniak/filebrowser/issues/699
- admin special characters and general login improvements https://github.com/gtsteffaniak/filebrowser/issues/594
- updated editor caching behavior https://github.com/gtsteffaniak/filebrowser/issues/701
- move/copy file path issue and overwrite https://github.com/gtsteffaniak/filebrowser/issues/687
- fix popup preview loading on safari
- `preview.highQuality` only affects gallery view mode. popop preview is always high quality, and icons are always low quality.
## v0.7.6-beta
**New Features**:
- native document preview generation enabled for certain document types on the regular docker image (no office integration needed)
@@ -17,11 +39,9 @@ NOTE: if using docker arm32 image, you will need to switch to the slim images. T
- ".cbz", // CBZ
- ".svg", // SVG
- ".txt", // TXT
- ".doc", // DOC
- ".docx", // DOCX
- ".ppt", // PPT
- ".pptx", // PPTX
- ".xls", // excel xls
- ".xlsx", // exel XLSX
- ".hwp", // HWP
- ".hwp", // HWPX
+7 -11
View File
@@ -1,15 +1,16 @@
FROM gtstef/ffmpeg:7.1.1 AS ffmpeg
FROM golang:1.24-alpine AS base
FROM gtstef/ffmpeg:7.1.1-decode AS ffmpeg
FROM golang: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 -tags pdf -ldflags="-w -s \
RUN apk update && apk add --no-cache gcc musl-dev upx
ENV CGO_ENABLED=1
RUN go build -tags mupdf,musl -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 .
RUN upx filebrowser
FROM node:lts-slim AS nbuild
WORKDIR /app
@@ -20,9 +21,6 @@ RUN npm run build-docker
FROM alpine:latest
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"
@@ -31,15 +29,13 @@ 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/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
+5 -28
View File
@@ -1,14 +1,14 @@
FROM golang:1.24-alpine AS base
FROM golang: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 apk update && apk add upx
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 .
RUN upx filebrowser
FROM node:lts-slim AS nbuild
WORKDIR /app
@@ -18,40 +18,17 @@ 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=base --chown=filebrowser:1000 /app/filebrowser ./
COPY --from=base --chown=filebrowser:1000 /app/config.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
+13 -21
View File
@@ -16,29 +16,29 @@ builds:
goos:
- linux
goarch:
- arm64
- amd64
hooks:
post:
- upx {{ .Path }} # Compress the binary with UPX
- upx {{ .Path }}
# Build configuration for linux amd64
# Build configuration for linux arm64
- id: linux-arm64
env:
- CGO_ENABLED=1
- CC=aarch64-linux-gnu-gcc
- CXX=aarch64-linux-gnu-g++
tags:
- mupdf
ldflags: *ldflags
main: main.go
binary: filebrowser
env:
- CGO_ENABLED=1
- CC=zig cc -target aarch64-linux-musl
tags:
- mupdf
- musl
goos:
- linux
goarch:
- arm64
hooks:
post:
- upx {{ .Path }} # Compress the binary with UPX
- upx {{ .Path }}
# Build configuration for linux
- id: linux-arm32
@@ -60,8 +60,7 @@ builds:
- id: windows
env:
- CGO_ENABLED=1
- CC=x86_64-w64-mingw32-gcc
- CXX=x86_64-w64-mingw32-g++
- CC=zig cc -target x86_64-windows
tags:
- mupdf
ldflags: *ldflags
@@ -76,7 +75,7 @@ builds:
- upx {{ .Path }} # Compress the binary with UPX
# Build configuration for freebsd without arm & upx
- id: freeBSD
- id: freeBSD and freebsd-arm
ldflags: *ldflags
main: main.go
binary: filebrowser
@@ -84,9 +83,6 @@ builds:
- freebsd
goarch:
- amd64
hooks:
post:
- upx {{ .Path }} # Compress the binary with UPX
# Build configuration for macos without upx
- id: macos
@@ -96,11 +92,8 @@ builds:
goos:
- darwin
goarch:
- amd64
- arm64
hooks:
post:
- upx {{ .Path }} # Compress the binary with UPX
- amd64
archives:
- name_template: >
@@ -113,4 +106,3 @@ archives:
checksum:
disable: true
@@ -31,10 +31,6 @@ type FileCache struct {
// NewFileCache creates a new FileCache
func NewFileCache(dir string) (*FileCache, error) {
if err := os.MkdirAll(filepath.Join(dir, "thumbnails", "video"), 0700); err != nil {
return nil, fmt.Errorf("can't make directory %s: %v", dir, err)
}
return &FileCache{dir: dir}, nil
}
+2 -3
View File
@@ -20,10 +20,10 @@ type JSONAuth struct {
// Auth authenticates the user via a json in content body.
func (auther JSONAuth) Auth(r *http.Request, userStore *users.Storage) (*users.User, error) {
password := r.URL.Query().Get("password")
username := r.URL.Query().Get("username")
recaptcha := r.URL.Query().Get("recaptcha")
totpCode := r.URL.Query().Get("code")
password := r.Header.Get("X-Password")
totpCode := r.Header.Get("X-Secret")
// If ReCaptcha is enabled, check the code.
if auther.ReCaptcha != nil && len(auther.ReCaptcha.Secret) > 0 {
@@ -54,7 +54,6 @@ func (auther JSONAuth) Auth(r *http.Request, userStore *users.Storage) (*users.U
}
err = VerifyTotpCode(user, totpCode, userStore)
if err != nil {
logger.Error("OTP verification failed")
return nil, err
}
}
+8 -5
View File
@@ -190,11 +190,14 @@ func VerifyTotpCode(user *users.User, code string, userStore *users.Storage) err
user.TOTPSecret = totpSecret // The encrypted or plaintext secret
user.TOTPNonce = totpNonce // The nonce if encrypted, or empty if plaintext
user.OtpEnabled = true // Enable OTP for the user
// save user
if err := userStore.Update(user, user.Permissions.Admin, "TOTPSecret", "TOTPNonce"); err != nil {
logger.Error("error updating user with OTP token:", err)
return fmt.Errorf("error updating user with OTP token: %w", err)
}
} else {
return fmt.Errorf("opt secret is empty, cannot enable TOTP")
}
// save user
if err := userStore.Update(user, user.Permissions.Admin, "TOTPSecret", "TOTPNonce"); err != nil {
logger.Debug("error updating user with OTP token:", err)
return fmt.Errorf("error updating user with OTP token: %w", err)
}
return nil
}
+7
View File
@@ -227,6 +227,13 @@ func loadConfigWithDefaults(configFile string) error {
yamlFile, err := os.Open(configFile)
if err != nil {
logger.Warningf("could not open config file '%v', using default settings", configFile)
Config.Server.Sources = []Source{
{
Path: ".",
},
}
loadEnvConfig()
return nil
}
defer yamlFile.Close()
stat, err := yamlFile.Stat()
+22 -21
View File
@@ -39,10 +39,10 @@ type Server struct {
CacheDir string `json:"cacheDir"` // path to the cache directory, used for thumbnails and other cached files
MaxArchiveSizeGB int64 `json:"maxArchiveSize"` // max pre-archive combined size of files/folder that are allowed to be archived (in GB)
// not exposed to config
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
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"`
MuPdfAvailable bool `json:"-"` // used internally if compiled with mupdf support
}
type Integrations struct {
@@ -113,21 +113,22 @@ type ExternalLink struct {
// UserDefaults is a type that holds the default values
// for some fields on User.
type UserDefaults struct {
StickySidebar bool `json:"stickySidebar"` // keep sidebar open when navigating
DarkMode bool `json:"darkMode"` // should dark mode be enabled
Locale string `json:"locale"` // language to use: eg. de, en, or fr
ViewMode string `json:"viewMode"` // view mode to use: eg. normal, list, grid, or compact
SingleClick bool `json:"singleClick"` // open directory on single click, also enables middle click to open in new tab
ShowHidden bool `json:"showHidden"` // show hidden files in the UI. On windows this includes files starting with a dot and windows hidden files
DateFormat bool `json:"dateFormat"` // when false, the date is relative, when true, the date is an exact timestamp
GallerySize int `json:"gallerySize"` // 0-9 - the size of the gallery thumbnails
ThemeColor string `json:"themeColor"` // theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
QuickDownload bool `json:"quickDownload"` // show icon to download in one click
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // comma separated list of file extensions to disable onlyoffice preview for
LockPassword bool `json:"lockPassword"` // disable the user from changing their password
DisableSettings bool `json:"disableSettings,omitempty"` // disable the user from viewing the settings page
Preview users.Preview `json:"preview"`
DefaultScopes []users.SourceScope `json:"-"`
Permissions users.Permissions `json:"permissions"`
LoginMethod string `json:"loginMethod,omitempty"` // login method to use: eg. password, proxy, oidc
StickySidebar bool `json:"stickySidebar"` // keep sidebar open when navigating
DarkMode bool `json:"darkMode"` // should dark mode be enabled
Locale string `json:"locale"` // language to use: eg. de, en, or fr
ViewMode string `json:"viewMode"` // view mode to use: eg. normal, list, grid, or compact
SingleClick bool `json:"singleClick"` // open directory on single click, also enables middle click to open in new tab
ShowHidden bool `json:"showHidden"` // show hidden files in the UI. On windows this includes files starting with a dot and windows hidden files
DateFormat bool `json:"dateFormat"` // when false, the date is relative, when true, the date is an exact timestamp
GallerySize int `json:"gallerySize"` // 0-9 - the size of the gallery thumbnails
ThemeColor string `json:"themeColor"` // theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
QuickDownload bool `json:"quickDownload"` // show icon to download in one click
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // comma separated list of file extensions to disable onlyoffice preview for
DisableOfficePreviewExt string `json:"disableOfficePreviewExt"` // comma separated list of file extensions to disable office preview for
LockPassword bool `json:"lockPassword"` // disable the user from changing their password
DisableSettings bool `json:"disableSettings,omitempty"` // disable the user from viewing the settings page
Preview users.Preview `json:"preview"`
DefaultScopes []users.SourceScope `json:"-"`
Permissions users.Permissions `json:"permissions"`
LoginMethod string `json:"loginMethod,omitempty"` // login method to use: eg. password, proxy, oidc
}
+9 -1
View File
@@ -126,6 +126,14 @@ func (st usersBackend) Update(user *users.User, actorIsAdmin bool, fields ...str
// Get the value to be stored
val := fieldValue.Interface()
if field == "OtpEnabled" {
otpEnabled, _ := val.(bool)
if otpEnabled {
continue
}
field = "TOTPSecret" // if otp is disabled, we also want to clear the TOTPSecret
val = "" // clear the TOTPSecret
}
// Update the database
if err := st.db.UpdateField(existingUser, field, val); err != nil {
return fmt.Errorf("failed to update user field: %s, error: %v", field, err)
@@ -226,7 +234,7 @@ func parseFields(user *users.User, fields []string, actorIsAdmin bool) ([]string
field := t.Field(i)
// which=all can't update password
switch strings.ToLower(field.Name) {
case "id", "username", "loginmethod", "password":
case "id", "username", "loginmethod", "password", "apikeys", "totpenabled", "totpsecret", "totpnonce":
// Skip these fields
continue
}
+15 -14
View File
@@ -75,20 +75,21 @@ type SourceScope struct {
// json tags must match variable name with smaller case first letter
type NonAdminEditable struct {
Preview Preview `json:"preview"`
StickySidebar bool `json:"stickySidebar"` // keep sidebar open when navigating
DarkMode bool `json:"darkMode"` // should dark mode be enabled
Password string `json:"password,omitempty"`
Locale string `json:"locale"` // language to use: eg. de, en, or fr
ViewMode string `json:"viewMode"` // view mode to use: eg. normal, list, grid, or compact
SingleClick bool `json:"singleClick"` // open directory on single click, also enables middle click to open in new tab
Sorting Sorting `json:"sorting"`
ShowHidden bool `json:"showHidden"` // show hidden files in the UI. On windows this includes files starting with a dot and windows hidden files
DateFormat bool `json:"dateFormat"` // when false, the date is relative, when true, the date is an exact timestamp
GallerySize int `json:"gallerySize"` // 0-9 - the size of the gallery thumbnails
ThemeColor string `json:"themeColor"` // theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
QuickDownload bool `json:"quickDownload"` // show icon to download in one click
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // comma separated list of file extensions to disable onlyoffice preview for
Preview Preview `json:"preview"`
StickySidebar bool `json:"stickySidebar"` // keep sidebar open when navigating
DarkMode bool `json:"darkMode"` // should dark mode be enabled
Password string `json:"password,omitempty"`
Locale string `json:"locale"` // language to use: eg. de, en, or fr
ViewMode string `json:"viewMode"` // view mode to use: eg. normal, list, grid, or compact
SingleClick bool `json:"singleClick"` // open directory on single click, also enables middle click to open in new tab
Sorting Sorting `json:"sorting"`
ShowHidden bool `json:"showHidden"` // show hidden files in the UI. On windows this includes files starting with a dot and windows hidden files
DateFormat bool `json:"dateFormat"` // when false, the date is relative, when true, the date is an exact timestamp
GallerySize int `json:"gallerySize"` // 0-9 - the size of the gallery thumbnails
ThemeColor string `json:"themeColor"` // theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
QuickDownload bool `json:"quickDownload"` // show icon to download in one click
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // comma separated list of file extensions to disable onlyoffice preview for
DisableOfficePreviewExt string `json:"disableOfficePreviewExt"` // comma separated list of file extensions to disable office preview for
}
var PublicUser = User{
+2 -2
View File
@@ -5,7 +5,6 @@ go 1.24.2
require (
github.com/asdine/storm/v3 v3.2.1
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
@@ -14,6 +13,7 @@ require (
github.com/google/go-cmp v0.7.0
github.com/gtsteffaniak/go-cache v0.0.0-20250521142451-edc77dfcb063
github.com/gtsteffaniak/go-logger v0.1.2
github.com/kovidgoyal/imaging v1.6.4
github.com/pquerna/otp v1.5.0
github.com/shirou/gopsutil/v3 v3.24.5
github.com/spf13/afero v1.14.0
@@ -101,7 +101,7 @@ require (
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/golang/geo v0.0.0-20250516193853-92f93c4cb289 // indirect
github.com/golang/geo v0.0.0-20250602202553-92e1c6cfbc31 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
github.com/golangci/go-printf-func-name v0.1.0 // indirect
+4 -2
View File
@@ -147,8 +147,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=
github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
@@ -276,6 +274,8 @@ github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgR
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20250516193853-92f93c4cb289 h1:HeOFbnyPys/vx/t+d4fwZM782mnjRVtbjxVkDittTUs=
github.com/golang/geo v0.0.0-20250516193853-92f93c4cb289/go.mod h1:Vaw7L5b+xa3Rj4/pRtrQkymn3lSBRB/NAEdbF9YEVLA=
github.com/golang/geo v0.0.0-20250602202553-92e1c6cfbc31 h1:E1wdqK72/SVuxkxQ2DSE7YZEmcUZvExysw58zmkLKik=
github.com/golang/geo v0.0.0-20250602202553-92e1c6cfbc31/go.mod h1:Vaw7L5b+xa3Rj4/pRtrQkymn3lSBRB/NAEdbF9YEVLA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -423,6 +423,8 @@ github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/tt
github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kovidgoyal/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk=
github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+60 -7
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"runtime"
"strings"
"time"
@@ -114,7 +115,7 @@ func userWithoutOTPhelper(fn handleFunc) handleFunc {
// This middleware is used when no user authentication is required
// Call the actual handler function with the updated context
username := r.URL.Query().Get("username")
password := r.URL.Query().Get("password")
password := r.Header.Get("X-Password")
proxyUser := r.Header.Get(config.Auth.Methods.ProxyAuth.Header)
if config.Auth.Methods.ProxyAuth.Enabled && proxyUser != "" {
user, err := setupProxyUser(r, &requestContext{}, proxyUser)
@@ -345,28 +346,80 @@ func setUserInResponseWriter(w http.ResponseWriter, user *users.User) {
// LoggingMiddleware logs each request and its status code.
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// DEFER RECOVERY FUNCTION
defer func() {
if rcv := recover(); rcv != nil {
// Log detailed information about the panic
// Extract as much context as possible for logging
method := r.Method
url := r.URL.String()
remoteAddr := r.RemoteAddr
username := "unknown" // Default username
// Wrap the ResponseWriter to capture the status code.
// Attempt to get username from ResponseWriterWrapper if it's set
if ww, ok := w.(*ResponseWriterWrapper); ok && ww.User != "" {
username = ww.User
}
// Or try to get it from request context if your other middleware populates it
// This depends on your context setup; example:
// if dataCtx, ok := r.Context().Value("requestData").(*requestContext); ok && dataCtx.user != nil {
// username = dataCtx.user.Username
// }
// Get Go-level stack trace
buf := make([]byte, 16384) // Increased buffer size for potentially long CGo traces
n := runtime.Stack(buf, false) // false for current goroutine only
stackTrace := string(buf[:n])
logger.Errorf("PANIC RECOVERED: %v\nUser: %s\nMethod: %s\nURL: %s\nRemoteAddr: %s\nGo Stack Trace:\n%s",
rcv, username, method, url, remoteAddr, stackTrace)
// Attempt to send a 500 error response to the client
// This is a best-effort; the connection might be broken or process too unstable.
if ww, ok := w.(*ResponseWriterWrapper); ok { // Check if it's our wrapper
if !ww.wroteHeader { // Only write if headers haven't been sent
ww.Header().Set("Content-Type", "application/json; charset=utf-8")
ww.WriteHeader(http.StatusInternalServerError)
}
} else {
_, _ = renderJSON(w, r, &HttpResponse{
Status: 500,
Message: "A critical internal error occurred. Please try again later.",
})
}
// IMPORTANT: After a SIGSEGV from C code, the process might be unstable.
// Even if Go recovers, continuing to run the process is risky.
// Consider a strategy to gracefully shut down or signal an external supervisor
// to restart the process after logging. For now, this will allow other requests
// to proceed if the process doesn't die, but be wary.
}
}() // End of deferred recovery function
start := time.Now()
wrappedWriter := &ResponseWriterWrapper{ResponseWriter: w, StatusCode: http.StatusOK}
// Call the next handler.
// Call the next handler in the chain
next.ServeHTTP(wrappedWriter, r)
// Capture the full URL path including the query parameters.
// Existing logging logic for normal requests
fullURL := r.URL.Path
if r.URL.RawQuery != "" {
fullURL += "?" + r.URL.RawQuery
}
truncUser := wrappedWriter.User
if len(truncUser) > 12 {
if truncUser == "" {
truncUser = "N/A" // Handle case where user might not be set (e.g., if panic occurred before user auth)
} else if len(truncUser) > 12 {
truncUser = truncUser[:10] + ".."
}
duration := time.Since(start)
// Use the StatusCode from wrappedWriter, which might have been set to 500 by the recover logic
logger.Api(wrappedWriter.StatusCode,
fmt.Sprintf("%-7s | %3d | %-15s | %-12s | %-12s | \"%s\"",
r.Method,
wrappedWriter.StatusCode, // Captured status code
wrappedWriter.StatusCode,
r.RemoteAddr,
truncUser,
fmt.Sprintf("%vms", duration.Milliseconds()),
+1 -2
View File
@@ -30,7 +30,6 @@ type userInfo struct {
// If ID token verification or essential claims extraction fails, it falls back to the UserInfo endpoint.
func oidcCallbackHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
ctx := r.Context()
oidcCfg := settings.Config.Auth.Methods.OidcAuth
if oidcCfg.Provider == nil || oidcCfg.Verifier == nil {
// Ensure Provider and Verifier are initialized on application startup
@@ -50,7 +49,7 @@ func oidcCallbackHandler(w http.ResponseWriter, r *http.Request, d *requestConte
}
ctx = oidc.ClientContext(ctx, customClient)
}
code := r.URL.Query().Get("code")
code := r.Header.Get("X-Secret")
// state := r.URL.Query().Get("state") // You might want to validate the state parameter for CSRF protection
// The redirect URI MUST match the one registered with the OIDC provider
+2 -2
View File
@@ -97,8 +97,8 @@ func rawFileHandler(w http.ResponseWriter, r *http.Request, file iteminfo.Extend
func previewHelperFunc(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
previewSize := r.URL.Query().Get("size")
if previewSize != "small" {
previewSize = "large"
if !(previewSize == "large" || previewSize == "original") {
previewSize = "small"
}
if d.fileInfo.Type == "directory" {
return http.StatusBadRequest, fmt.Errorf("can't create preview for directory")
+1 -1
View File
@@ -58,7 +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,
"MuPdfAvailable": config.Server.MuPdfAvailable,
}
b, err := json.Marshal(data)
+1 -1
View File
@@ -43,7 +43,7 @@ func generateOTPHandler(w http.ResponseWriter, r *http.Request, d *requestContex
// @Failure 401 {object} map[string]string "Unauthorized - invalid TOTP token"
// @Router /api/auth/otp/verify [post]
func verifyOTPHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
code := r.URL.Query().Get("code")
code := r.Header.Get("X-Secret")
if code == "" {
return http.StatusUnauthorized, fmt.Errorf("code is required")
}
+8 -6
View File
@@ -20,7 +20,7 @@ var AllFiletypeOptions = []string{
"text",
}
var PdfConvertable = []string{
var MuPdfConvertable = []string{
".pdf", // PDF
".xps", // XPS
".epub", // EPUB
@@ -29,14 +29,13 @@ var PdfConvertable = []string{
".cbz", // CBZ
".svg", // SVG
".txt", // TXT
".doc", // DOC
".docx", // DOCX
".ppt", // PPT
".pptx", // PPTX
".xls", // XLS
".xlsx", // XLSX
".hwp", // HWP
".hwp", // HWPX
".md", // Markdown
}
// Known bundle-style extensions that are technically directories but treated as files
@@ -375,12 +374,15 @@ func hasBundleExtension(name string) bool {
return false
}
func HasDocConvertableExtension(name string) bool {
if !settings.Config.Server.PdfAvailable {
func HasDocConvertableExtension(name, mimetype string) bool {
if !settings.Config.Server.MuPdfAvailable {
return false
}
if strings.HasPrefix(mimetype, "text") {
return true
}
ext := strings.ToLower(filepath.Ext(name))
for _, e := range PdfConvertable {
for _, e := range MuPdfConvertable {
if ext == e {
return true
}
+67 -12
View File
@@ -1,15 +1,19 @@
//go:build pdf
// +build pdf
//go:build mupdf
// +build mupdf
package preview
import (
"bytes"
"context"
"fmt"
"image/jpeg"
"io"
"os"
"runtime"
"strings"
"github.com/gen2brain/go-fitz"
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
)
func docEnabled() bool {
@@ -18,21 +22,72 @@ func docEnabled() bool {
return true
}
func (s *Service) GenerateImageFromDoc(docPath string, pageNumber int) ([]byte, error) {
if err := s.acquire(context.Background()); err != nil {
return nil, err
func (s *Service) GenerateImageFromDoc(file iteminfo.ExtendedFileInfo, tempFilePath string, pageNumber int) ([]byte, error) {
// 1. Serialize access to the entire go-fitz operation block
s.docGenMutex.Lock()
defer s.docGenMutex.Unlock()
// 2. Lock the current goroutine to a single OS thread for CGo calls
runtime.LockOSThread()
defer runtime.UnlockOSThread()
docPath := file.RealPath
// copy file to a temporary location if needed
if strings.HasPrefix(file.Type, "text") && !strings.HasSuffix(file.RealPath, ".txt") {
originalFile, err := os.Open(file.RealPath)
if err != nil {
return nil, fmt.Errorf("text snippet: failed to open original file '%s': %w", file.RealPath, err)
}
defer originalFile.Close() // Ensure original file is closed
buffer := make([]byte, 1024) // Buffer for up to 1KB
n, readErr := originalFile.Read(buffer)
if readErr != nil && readErr != io.EOF { // io.EOF is not an error if some bytes were read
return nil, fmt.Errorf("text snippet: failed to read from original file '%s': %w", file.RealPath, readErr)
}
if n == 0 {
return nil, fmt.Errorf("text snippet: original file '%s' is empty or unreadable", file.RealPath)
} else {
tempFile, err := os.Create(tempFilePath)
if err != nil {
return nil, fmt.Errorf("text snippet: failed to create temporary file '%s': %w", tempFilePath, err)
}
defer os.Remove(tempFilePath) // Ensure cleanup on error
// Write the read content (up to 1KB or EOF) to the temporary file
if _, err := tempFile.Write(buffer[:n]); err != nil {
tempFile.Close() // Attempt to close
os.Remove(tempFilePath) // Clean up on error
return nil, fmt.Errorf("text snippet: failed to write to temporary file '%s': %w", tempFilePath, err)
}
// Close the temporary file so it can be reliably opened by path by other processes/functions
if err := tempFile.Close(); err != nil {
os.Remove(tempFilePath) // Clean up on error
return nil, fmt.Errorf("text snippet: failed to close temporary file '%s': %w", tempFilePath, err)
}
docPath = tempFilePath // Update docPath to point to the new temporary text snippet file
}
}
defer s.release()
doc, err := fitz.New(docPath)
doc, err := fitz.New(docPath) // This calls the CGo version
if err != nil {
return nil, fmt.Errorf("failed to open PDF: %w", err)
// The error message you received: "failed to open PDF: fitz: cannot open memory"
return nil, fmt.Errorf("failed to open PDF from memory for file '%s': %w", docPath, 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
// Ensure pageNumber is valid (e.g., >= 0 for 0-indexed or >= 1 for 1-indexed, check go-fitz docs)
// And pageNumber < doc.NumPage()
numPages := doc.NumPage()
if pageNumber < 0 || pageNumber >= numPages { // Assuming 0-indexed for Image()
return nil, fmt.Errorf("invalid page number %d for PDF with %d pages ('%s')", pageNumber, numPages, docPath)
}
img, err := doc.Image(pageNumber)
if err != nil {
return nil, fmt.Errorf("failed to get image from page %d: %w", pageNumber, err)
return nil, fmt.Errorf("failed to get image from page %d of '%s': %w", pageNumber, docPath, err)
}
// Create a new buffer to hold the image bytes
@@ -41,7 +96,7 @@ func (s *Service) GenerateImageFromDoc(docPath string, pageNumber int) ([]byte,
// 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 nil, fmt.Errorf("failed to encode image to jpeg for '%s': %w", docPath, err)
}
// Return the byte slice from the buffer
+8 -3
View File
@@ -1,14 +1,19 @@
//go:build !pdf
// +build !pdf
//go:build !mupdf
// +build !mupdf
package preview
import "github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
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) {
func (s *Service) GenerateImageFromDoc(file iteminfo.ExtendedFileInfo, tempFilePath string, pageNumber int) ([]byte, error) { // 1. Serialize access to the entire go-fitz operation block
s.docGenMutex.Lock()
defer s.docGenMutex.Unlock()
return nil, nil
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"image"
"io"
"github.com/disintegration/imaging"
"github.com/kovidgoyal/imaging"
exif "github.com/dsoprea/go-exif/v3"
exifcommon "github.com/dsoprea/go-exif/v3/common"
)
+45 -21
View File
@@ -3,7 +3,7 @@ package preview
import (
"bytes"
"context"
"crypto/sha1"
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
@@ -12,6 +12,7 @@ import (
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/diskcache"
@@ -32,6 +33,7 @@ type Service struct {
ffprobePath string
fileCache diskcache.Interface
debug bool
docGenMutex sync.Mutex // Mutex to serialize access to doc generation
}
func NewPreviewGenerator(concurrencyLimit int, ffmpegPath string, cacheDir string) *Service {
@@ -50,7 +52,15 @@ func NewPreviewGenerator(concurrencyLimit int, ffmpegPath string, cacheDir strin
// No-op cache if no cacheDir is specified
fileCache = diskcache.NewNoOp()
}
// Create directories recursively with 0755 permissions
err := os.MkdirAll(filepath.Join(settings.Config.Server.CacheDir, "thumbnails", "docs"), 0755)
if err != nil {
logger.Error(err)
}
err = os.MkdirAll(filepath.Join(settings.Config.Server.CacheDir, "thumbnails", "videos"), 0755)
if err != nil {
logger.Error(err)
}
ffmpegMainPath, err := CheckValidFFmpeg(ffmpegPath)
if err != nil && ffmpegPath != "" {
logger.Fatalf("the configured ffmpeg path does not contain a valid ffmpeg binary %s, err: %v", ffmpegPath, err)
@@ -63,7 +73,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()
settings.Config.Server.MuPdfAvailable = docEnabled()
return &Service{
sem: make(chan struct{}, concurrencyLimit),
ffmpegPath: ffmpegMainPath,
@@ -98,10 +108,14 @@ func GeneratePreview(file iteminfo.ExtendedFileInfo, previewSize, officeUrl stri
err error
imageBytes []byte
)
// Generate thumbnail image from video
hasher := md5.New() //nolint:gosec
_, _ = hasher.Write([]byte(CacheKey(file.RealPath, previewSize, file.ItemInfo.ModTime, seekPercentage)))
hash := hex.EncodeToString(hasher.Sum(nil))
// Generate an image from office document
if iteminfo.HasDocConvertableExtension(file.Name) {
imageBytes, err = service.GenerateImageFromDoc(file.RealPath, 0) // 0 for the first page
if iteminfo.HasDocConvertableExtension(file.Name, file.Type) {
tempFilePath := filepath.Join(settings.Config.Server.CacheDir, "thumbnails", "docs", hash) + ".txt"
imageBytes, err = service.GenerateImageFromDoc(file, tempFilePath, 0) // 0 for the first page
if err != nil {
return nil, fmt.Errorf("failed to create image for PDF file: %w", err)
}
@@ -120,11 +134,7 @@ func GeneratePreview(file iteminfo.ExtendedFileInfo, previewSize, officeUrl stri
if seekPercentage == 0 {
seekPercentage = 10
}
// Generate thumbnail image from video
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)
outPathPattern := filepath.Join(settings.Config.Server.CacheDir, "thumbnails", "videos", hash) + ".jpg"
defer os.Remove(outPathPattern) // cleanup
imageBytes, err = service.GenerateVideoPreview(file.RealPath, outPathPattern, seekPercentage)
if err != nil {
@@ -133,16 +143,30 @@ func GeneratePreview(file iteminfo.ExtendedFileInfo, previewSize, officeUrl stri
} else {
return nil, fmt.Errorf("unsupported media type: %s", ext)
}
resizedBytes, err := service.CreatePreview(imageBytes, previewSize)
if err != nil {
return nil, fmt.Errorf("failed to resize preview image: %w", err)
if len(imageBytes) < 100 {
return nil, fmt.Errorf("generated image is too small, likely an error occurred: %d bytes", len(imageBytes))
}
// Cache and return
cacheKey := CacheKey(file.RealPath, previewSize, file.ItemInfo.ModTime, seekPercentage)
if err := service.fileCache.Store(context.Background(), cacheKey, resizedBytes); err != nil {
logger.Errorf("failed to cache resized image: %v", err)
if previewSize != "original" {
// resize image
resizedBytes, err := service.CreatePreview(imageBytes, previewSize)
if err != nil {
return nil, fmt.Errorf("failed to resize preview image: %w", err)
}
// Cache and return
cacheKey := CacheKey(file.RealPath, previewSize, file.ItemInfo.ModTime, seekPercentage)
if err := service.fileCache.Store(context.Background(), cacheKey, resizedBytes); err != nil {
logger.Errorf("failed to cache resized image: %v", err)
}
return resizedBytes, nil
} else {
cacheKey := CacheKey(file.RealPath, previewSize, file.ItemInfo.ModTime, seekPercentage)
if err := service.fileCache.Store(context.Background(), cacheKey, imageBytes); err != nil {
logger.Errorf("failed to cache resized image: %v", err)
}
return imageBytes, nil
}
return resizedBytes, nil
}
func (s *Service) CreatePreview(data []byte, previewSize string) ([]byte, error) {
@@ -154,7 +178,7 @@ func (s *Service) CreatePreview(data []byte, previewSize string) ([]byte, error)
switch previewSize {
case "large":
width, height = 512, 512
width, height = 640, 640
options = []Option{WithMode(ResizeModeFit), WithQuality(QualityHigh), WithFormat(FormatJpeg)}
case "small":
width, height = 256, 256
@@ -191,7 +215,7 @@ func AvailablePreview(file iteminfo.ExtendedFileInfo) bool {
if strings.HasPrefix(file.Type, "video") && service.ffmpegPath != "" {
return true
}
if file.Type == "application/pdf" {
if iteminfo.HasDocConvertableExtension(file.Name, file.Type) {
return true
}
ext := strings.ToLower(filepath.Ext(file.Name))
+8
View File
@@ -2222,6 +2222,10 @@ const docTemplate = `{
"description": "when false, the date is relative, when true, the date is an exact timestamp",
"type": "boolean"
},
"disableOfficePreviewExt": {
"description": "comma separated list of file extensions to disable office preview for",
"type": "string"
},
"disableOnlyOfficeExt": {
"description": "comma separated list of file extensions to disable onlyoffice preview for",
"type": "string"
@@ -2443,6 +2447,10 @@ const docTemplate = `{
"description": "when false, the date is relative, when true, the date is an exact timestamp",
"type": "boolean"
},
"disableOfficePreviewExt": {
"description": "comma separated list of file extensions to disable office preview for",
"type": "string"
},
"disableOnlyOfficeExt": {
"description": "comma separated list of file extensions to disable onlyoffice preview for",
"type": "string"
+8
View File
@@ -2211,6 +2211,10 @@
"description": "when false, the date is relative, when true, the date is an exact timestamp",
"type": "boolean"
},
"disableOfficePreviewExt": {
"description": "comma separated list of file extensions to disable office preview for",
"type": "string"
},
"disableOnlyOfficeExt": {
"description": "comma separated list of file extensions to disable onlyoffice preview for",
"type": "string"
@@ -2432,6 +2436,10 @@
"description": "when false, the date is relative, when true, the date is an exact timestamp",
"type": "boolean"
},
"disableOfficePreviewExt": {
"description": "comma separated list of file extensions to disable office preview for",
"type": "string"
},
"disableOnlyOfficeExt": {
"description": "comma separated list of file extensions to disable onlyoffice preview for",
"type": "string"
+8
View File
@@ -443,6 +443,10 @@ definitions:
description: when false, the date is relative, when true, the date is an exact
timestamp
type: boolean
disableOfficePreviewExt:
description: comma separated list of file extensions to disable office preview
for
type: string
disableOnlyOfficeExt:
description: comma separated list of file extensions to disable onlyoffice
preview for
@@ -604,6 +608,10 @@ definitions:
description: when false, the date is relative, when true, the date is an exact
timestamp
type: boolean
disableOfficePreviewExt:
description: comma separated list of file extensions to disable office preview
for
type: string
disableOnlyOfficeExt:
description: comma separated list of file extensions to disable onlyoffice
preview for
+2
View File
@@ -27,9 +27,11 @@
"clipboard": "^2.0.4",
"css-vars-ponyfill": "^2.4.3",
"dompurify": "^3.2.4",
"epubjs": "^0.3.93",
"file-loader": "^6.2.0",
"glob": "^9.3.5",
"highlight.js": "^11.11.1",
"mammoth": "^1.9.1",
"marked": "^15.0.6",
"normalize.css": "^8.0.1",
"qrcode.vue": "^3.4.1",
+2 -1
View File
@@ -1,5 +1,5 @@
server:
numImageProcessors: 14 # number of concurrent image processing jobs used to create previews, default is number of cpu cores available.
numImageProcessors: 8 # 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
@@ -97,6 +97,7 @@ userDefaults:
themeColor: var(--blue) # theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
quickDownload: false # show icon to download in one click
disableOnlyOfficeExt: .txt .csv .html .pdf # comma separated list of file extensions to disable onlyoffice preview for
disableOfficePreviewExt: "" # comma separated list of file extensions to disable office preview for
lockPassword: false # disable the user from changing their password
disableSettings: false # disable the user from viewing the settings page
preview:
-1
View File
@@ -229,7 +229,6 @@ export function getPreviewURL(source, path, modified) {
try {
const params = {
path: encodeURIComponent(path),
size: state.user.preview.highQuality ? 'large' : 'small',
key: Date.parse(modified), // Use modified date as cache key
source: source,
inline: 'true'
+1 -2
View File
@@ -1,5 +1,4 @@
import { fetchURL, fetchJSON, adjustedData } from "./utils";
import { state } from '@/store'
import { notify } from "@/notify";
import { getApiPath,removePrefix } from "@/utils/url.js";
import { externalUrl,baseURL } from "@/utils/constants";
@@ -54,7 +53,7 @@ export function getPreviewURL(hash, path) {
try {
const params = {
path: encodeURIComponent(path),
size: state.user.preview.highQuality ? 'large' : 'small',
size: 'small',
hash: hash,
inline: 'true'
}
+18 -6
View File
@@ -14,13 +14,13 @@ export async function getAllUsers () {
}
export async function generateOTP (username, password) {
const params = { username, password }
const params = { username }
try {
let apiPath = getApiPath('api/auth/otp/generate', params)
const res = await fetch(apiPath, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
'X-Password': password,
}
})
return await res.json()
@@ -31,13 +31,14 @@ export async function generateOTP (username, password) {
}
export async function verifyOtp (username, password, otp) {
const params = { username, password, code: otp }
const params = { username }
try {
let apiPath = getApiPath('api/auth/otp/verify', params)
const res = await fetch(apiPath, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
'X-Password': password,
'X-Secret': otp,
}
})
if (res.status != 200) {
@@ -50,12 +51,23 @@ export async function verifyOtp (username, password, otp) {
}
export async function login(username, password, recaptcha, otp) {
const params = { username, password, recaptcha, code: otp };
if (!otp) {
otp = ''
}
if (!recaptcha) {
recaptcha = ''
}
if (!password) {
password = ''
}
const params = { username, recaptcha };
let apiPath = getApiPath('api/auth/login', params);
const res = await fetch(apiPath, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
'X-Password': password,
'X-Secret': otp,
}
});
+3 -1
View File
@@ -82,7 +82,9 @@ export default {
currentView == "editor" ||
currentView == "preview" ||
currentView == "onlyOfficeEditor" ||
currentView == "markdownViewer"
currentView == "markdownViewer" ||
currentView == "epubViewer" ||
currentView == "docViewer"
) {
mutations.setMultiButtonState("close");
} else {
+1 -2
View File
@@ -19,8 +19,7 @@
</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"
+16 -5
View File
@@ -55,7 +55,7 @@
@action="toggleMultipleSelection"
/>
<action
v-if="!showCreate && selectedCount > 0"
v-if="(!showCreate && selectedCount > 0)"
icon="file_download"
:label="$t('buttons.download')"
@action="startDownload"
@@ -103,9 +103,10 @@
class="button no-select"
:class="{ 'dark-mode': isDarkMode }"
>
<action v-if="showDelete" icon="delete" :label="$t('buttons.delete')" show="delete" />
<action v-if="showSave" icon="save" :label="$t('buttons.save')" @action="save()" />
<action v-if="isPreview" icon="file_download" :label="$t('buttons.download')" @action="startDownload" />
<action v-if="showEdit" icon="edit" :label="$t('buttons.edit')" @action="edit()" />
<action v-if="showSave" icon="save" :label="$t('buttons.save')" @action="save()" />
<action v-if="showDelete" icon="delete" :label="$t('buttons.delete')" show="delete" />
</div>
</template>
@@ -135,10 +136,20 @@ export default {
return !this.showEdit && !this.showSave && !this.showDelete;
},
showEdit() {
return window.location.hash != "#edit" && state.user.permissions.modify;
return getters.currentView() == "markdownViewer" && state.user.permissions.modify;
},
showDelete() {
return state.user.permissions.modify && getters.currentView() == "preview";
return state.user.permissions.modify && this.isPreview;
},
isPreview() {
const cv = getters.currentView();
return (
cv == "preview" ||
cv == "onlyOfficeEditor" ||
cv == "markdownViewer" ||
cv == "epubViewer" ||
cv == "docViewer"
);
},
showSave() {
return getters.currentView() == "editor" && state.user.permissions.modify;
+34 -16
View File
@@ -1,5 +1,9 @@
<template>
<span v-if="isPreviewImg && imageState !== 'error'">
<span
@mouseenter="handleMouseEnter($event)"
@mouseleave="handleMouseLeave($event)"
v-if="isPreviewImg && imageState !== 'error'"
>
<i
v-if="hasMotion"
class="material-icons"
@@ -7,10 +11,8 @@
>animation</i
>
<img
:key="thumbnailUrl"
:key="imageTargetSrc"
:src="imageDisplaySrc"
@mouseenter="handleMouseEnter($event)"
@mouseleave="handleMouseLeave($event)"
class="icon"
ref="thumbnail"
/>
@@ -21,7 +23,12 @@
</template>
<script>
import { onlyOfficeUrl, mediaAvailable, pdfAvailable, baseURL } from "@/utils/constants";
import {
onlyOfficeUrl,
mediaAvailable,
muPdfAvailable,
baseURL,
} from "@/utils/constants";
import { getTypeInfo } from "@/utils/mimetype";
import { mutations, state } from "@/store";
@@ -56,10 +63,16 @@ export default {
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: {
imageTargetSrc() {
if (this.showLargeIcon) {
// Use the large version of the thumbnail if available
return this.thumbnailUrl + "&size=large" || PLACEHOLDER_URL;
}
return this.thumbnailUrl || PLACEHOLDER_URL;
},
pdfConvertable() {
const ext = "." + this.filename.split(".").pop().toLowerCase(); // Ensure lowercase and dot
const pdfConvertCompatibleFileExtensions = {
@@ -71,16 +84,17 @@ export default {
".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
".hwpx": true,
".md": true,
};
return !!pdfConvertCompatibleFileExtensions[ext];
const textType = this.mimetype.startsWith("text/");
const disabled = state.user.disableOfficePreviewExt?.includes(ext);
return (!!pdfConvertCompatibleFileExtensions[ext] || textType) && !disabled;
},
// NEW: A single computed property to determine the final image src
imageDisplaySrc() {
@@ -93,6 +107,9 @@ export default {
}
return this.imageTargetSrc;
},
showLargeIcon() {
return state.user.viewMode === "gallery" && state.user.preview.highQuality;
},
showLarger() {
return state.user.viewMode === "gallery" || state.user.viewMode === "normal";
},
@@ -114,7 +131,7 @@ export default {
if (this.mimetype == "text/csv") {
return false;
}
if (this.pdfConvertable && pdfAvailable) {
if (this.pdfConvertable && muPdfAvailable) {
return true;
}
if (this.getIconForType().simpleType === "image" && state.user.preview?.image) {
@@ -165,18 +182,19 @@ export default {
targetImage.src = url;
},
handleMouseEnter() {
const imageUrl = this.thumbnailUrl + "&size=large";
if (this.imageState == "loaded") {
mutations.setPreviewSource(this.thumbnailUrl);
mutations.setPreviewSource(imageUrl);
}
if (!state.user.preview.motionVideoPreview || !this.hasMotion) {
return;
}
const sequence = [
this.thumbnailUrl,
this.thumbnailUrl + "&atPercentage=25",
this.thumbnailUrl + "&atPercentage=50",
this.thumbnailUrl + "&atPercentage=75",
imageUrl,
imageUrl + "&atPercentage=25",
imageUrl + "&atPercentage=50",
imageUrl + "&atPercentage=75",
];
let index = 0;
+10
View File
@@ -34,6 +34,7 @@
{{ $t("buttons.cancel") }}
</button>
<button
:disabled="destContainsSrc"
class="button button--flat"
@click="copy"
:aria-label="$t('buttons.copy')"
@@ -52,6 +53,7 @@ import FileList from "./FileList.vue";
import { filesApi } from "@/api";
import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
import { removeLastDir } from "@/utils/url";
import { notify } from "@/notify";
export default {
@@ -65,6 +67,14 @@ export default {
};
},
computed: {
destContainsSrc() {
if (!this.dest) {
return false; // If dest is not set, we can't check containment
}
const itemPath = this.items[0]?.from
const parentDir = removeLastDir(itemPath) + "/"
return this.dest == parentDir || this.dest.startsWith(itemPath);
},
user() {
return state.user;
},
+5 -4
View File
@@ -1,6 +1,7 @@
<template>
<div>
<div aria-label="filelist-path" class="searchContext">{{$t('search.path')}} {{ nav }}</div>
<div>Source: {{ sourcePath.source }} </div> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
<div aria-label="filelist-path" class="searchContext">{{$t('search.path')}} {{ sourcePath.path }}</div>
<ul class="file-list">
<li
@click="itemClick"
@@ -23,7 +24,7 @@
<script>
import { state, mutations } from "@/store";
import url from "@/utils/url.js";
import { removePrefix } from "@/utils/url.js";
import { extractSourceFromPath } from "@/utils/url.js";
import { filesApi } from "@/api";
export default {
@@ -40,8 +41,8 @@ export default {
};
},
computed: {
nav() {
return removePrefix(decodeURIComponent(this.current), "files");
sourcePath() {
return extractSourceFromPath(decodeURIComponent(this.current), "files");
},
},
mounted() {
+10
View File
@@ -33,6 +33,7 @@
{{ $t("buttons.cancel") }}
</button>
<button
:disabled="destContainsSrc"
class="button button--flat"
@click="move"
:aria-label="$t('buttons.move')"
@@ -52,6 +53,7 @@ import { filesApi } from "@/api";
import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
import { notify } from "@/notify";
import { removeLastDir } from "@/utils/url";
export default {
name: "move",
@@ -64,6 +66,14 @@ export default {
};
},
computed: {
destContainsSrc() {
if (!this.dest) {
return false; // If dest is not set, we can't check containment
}
const itemPath = this.items[0]?.from
const parentDir = removeLastDir(itemPath) + "/"
return this.dest == parentDir || this.dest.startsWith(itemPath);
},
user() {
return state.user;
},
@@ -41,7 +41,7 @@
</template>
<script>
import { getters } from "@/store"; // Import your custom store
import { getters, mutations } from "@/store"; // Import your custom store
export default {
name: "replace-rename",
@@ -50,5 +50,10 @@ export default {
return getters.currentPrompt(); // Access the getter directly from the store
},
},
methods: {
closeHovers() {
mutations.closeHovers();
},
},
};
</script>
+16 -10
View File
@@ -4,8 +4,8 @@
<h2>{{ $t("otp.name") }}</h2>
</div>
<div v-if="error !== ''" class="wrong-login">{{ error }}</div>
<div class="card-content">
<div v-if="succeeded" >{{ $t("otp.verificationSucceed") }}</div>
<div v-if="!succeeded" class="card-content">
<p v-if="generate">{{ $t("otp.generate") }}</p>
<div v-if="generate" class="share__box__element share__box__center">
<p>{{ this.url }}</p>
@@ -20,16 +20,18 @@
:placeholder="$t('otp.codeInputPlaceholder')"
/>
</div>
<div class="card-action">
<button
@click="closeHovers"
class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
:aria-label="succeeded ? $t('buttons.close') : $t('buttons.cancel')"
:title="succeeded ? $t('buttons.close') : $t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
{{ succeeded ? $t('buttons.close') : $t('buttons.cancel') }}
</button>
<button
v-if="!succeeded"
class="button button--flat button--blue"
@click="verifyCode"
:title="$t('buttons.verify')"
@@ -57,6 +59,7 @@ export default {
error: "",
code: "",
url: "",
succeeded: false,
};
},
props: {
@@ -104,18 +107,21 @@ export default {
if (this.redirect != "") {
await usersApi.login(this.username, this.password, this.redirect, this.code);
await initAuth();
if (this.redirect) {
this.$router.push(this.redirect);
}
} else {
mutations.closeHovers();
this.$router.push(this.redirect);
}
mutations.updateCurrentUser({ otpEnabled: true });
this.succeeded = true
this.error = "";
notify.showSuccess(this.$t("otp.verificationSucceed"));
} catch (error) {
this.error = this.$t("otp.verificationFailed");
return;
}
},
closeHovers() {
if (!this.succeeded) {
mutations.updateCurrentUser({ otpEnabled: false });
}
mutations.closeHovers();
},
},
@@ -233,6 +233,10 @@ export default {
this.user.scopes = newVal ? { default: "" } : this.originalUserScope;
this.emitUserUpdate();
},
stateUser() {
this.user.otpEnabled = state.user.otpEnabled;
this.emitUserUpdate();
},
},
computed: {
invalidPassword() {
+7 -2
View File
@@ -283,7 +283,11 @@
"enterPasswordAgain": "أدخل كلمة المرور مرة أخرى",
"passwordsDoNotMatch": "كلمات المرور غير متطابقة",
"loginMethod": "طريقة تسجيل الدخول",
"shareSettings": "مشاركة الإعدادات"
"shareSettings": "مشاركة الإعدادات",
"disableOfficeEditorDescription": "قائمة مفصولة بمسافات لامتدادات الملفات لتعطيل عارض OnlyOffice لها. (على سبيل المثال، \".txt .html\").",
"disableOfficeEditor": "تعطيل عارض المكتب لامتدادات ملفات معينة",
"disableOfficePreviews": "تعطيل معاينات المكتب لامتدادات ملفات معينة",
"disableOfficePreviewsDescription": "قائمة مفصولة بمسافة من امتدادات الملفات لتعطيل المعاينات المكتبية لها. (على سبيل المثال، \".txt .html\")."
},
"sidebar": {
"help": "مساعدة",
@@ -365,7 +369,8 @@
"highQualityPreview": "معاينة عالية الجودة",
"previewOffice": "معاينة الملفات المكتبية",
"popupPreview": "تمكين المعاينة المنبثقة",
"filePreviewOptions": "خيارات معاينة الملف"
"filePreviewOptions": "خيارات معاينة الملف",
"editOffice": "تحرير/عرض ملفات المكتب"
},
"otp": {
"name": "المصادقة الثنائية",
+7 -2
View File
@@ -283,7 +283,11 @@
"enterPasswordAgain": "Geben Sie das Passwort erneut ein",
"passwordsDoNotMatch": "Passwörter stimmen nicht überein",
"loginMethod": "Login-Methode",
"shareSettings": "Einstellungen teilen"
"shareSettings": "Einstellungen teilen",
"disableOfficeEditorDescription": "Eine durch Leerzeichen getrennte Liste von Dateierweiterungen, für die der OnlyOffice-Viewer deaktiviert werden soll. (z. B. '.txt .html').",
"disableOfficeEditor": "Office Viewer für bestimmte Dateierweiterungen deaktivieren",
"disableOfficePreviews": "Office-Vorschauen für bestimmte Dateierweiterungen deaktivieren",
"disableOfficePreviewsDescription": "Eine durch Leerzeichen getrennte Liste von Dateierweiterungen, für die die Office-Vorschau deaktiviert werden soll. (z. B. '.txt .html')."
},
"sidebar": {
"help": "Hilfe",
@@ -365,7 +369,8 @@
"highQualityPreview": "Hochwertige Vorschau",
"previewOffice": "Vorschau von Office-Dateien",
"popupPreview": "Pop-up-Vorschau einschalten",
"filePreviewOptions": "Optionen für die Dateivorschau"
"filePreviewOptions": "Optionen für die Dateivorschau",
"editOffice": "Office-Dateien bearbeiten/anzeigen"
},
"otp": {
"name": "Zwei-Faktoren-Authentifizierung",
+7 -2
View File
@@ -287,7 +287,11 @@
"enterPasswordAgain": "Πληκτρολογήστε ξανά τον κωδικό πρόσβασης",
"passwordsDoNotMatch": "Οι κωδικοί πρόσβασης δεν ταιριάζουν",
"loginMethod": "Μέθοδος σύνδεσης",
"shareSettings": "Ρυθμίσεις κοινής χρήσης"
"shareSettings": "Ρυθμίσεις κοινής χρήσης",
"disableOfficeEditorDescription": "Ένας κατάλογος επεκτάσεων αρχείων με διαχωρισμό διαστήματος για την απενεργοποίηση της προβολής OnlyOffice. (π.χ. '.txt .html').",
"disableOfficeEditor": "Απενεργοποίηση της προβολής γραφείου για ορισμένες επεκτάσεις αρχείων",
"disableOfficePreviews": "Απενεργοποίηση προεπισκοπήσεων γραφείου για ορισμένες επεκτάσεις αρχείων",
"disableOfficePreviewsDescription": "Μια λίστα επεκτάσεων αρχείων με διαχωρισμό διαστήματος για την απενεργοποίηση των προεπισκοπήσεων του Office. (π.χ., '.txt .html')."
},
"sidebar": {
"help": "Βοήθεια",
@@ -366,7 +370,8 @@
"highQualityPreview": "Προεπισκόπηση υψηλής ποιότητας",
"previewOffice": "Προεπισκόπηση αρχείων γραφείου",
"popupPreview": "Ενεργοποίηση αναδυόμενης προεπισκόπησης",
"filePreviewOptions": "Επιλογές προεπισκόπησης αρχείων"
"filePreviewOptions": "Επιλογές προεπισκόπησης αρχείων",
"editOffice": "Επεξεργασία/προβολή αρχείων γραφείου"
},
"otp": {
"name": "Αυθεντικοποίηση δύο παραγόντων",
+6 -4
View File
@@ -194,8 +194,8 @@
"previewImages": "Preview images",
"previewVideos": "Preview videos",
"previewMotionVideos": "Motion preview for videos",
"highQualityPreview": "High quality preview",
"previewOffice": "Preview office files",
"highQualityPreview": "High quality preview for gallery images",
"editOffice": "Edit/view office files",
"popupPreview": "Enable pop-up preview",
"filePreviewOptions": "File preview options"
},
@@ -208,8 +208,10 @@
"modifyOtherUser": "Modify user:",
"modifyCurrentUser": "Modify current user",
"themeColor": "Theme Color",
"disableOfficePreviewDescription": "A space-separated list of file extensions to disable the OnlyOffice viewer for. (e.g., '.txt .html').",
"disableOfficePreview": "Disable onlyoffice viewer for certain file extensions",
"disableOfficeEditorDescription": "A space-separated list of file extensions to disable the OnlyOffice viewer for. (e.g., '.txt .html').",
"disableOfficeEditor": "Disable office viewer for certain file extensions",
"disableOfficePreviews": "Disable office previews for certain file extensions",
"disableOfficePreviewsDescription": "A space-separated list of file extensions to disable office previews for. (e.g., '.txt .html').",
"listingOptions": "Listing options",
"emptyGlobal": "Currently there are no global config options, You can edit the configuration yaml to adjust system settings.",
"scopes": "Define allowed sources and the user scope within them",
+7 -2
View File
@@ -283,7 +283,11 @@
"enterPasswordAgain": "Vuelva a introducir la contraseña",
"passwordsDoNotMatch": "Las contraseñas no coinciden",
"loginMethod": "Método de inicio de sesión",
"shareSettings": "Compartir configuración"
"shareSettings": "Compartir configuración",
"disableOfficeEditorDescription": "Una lista separada por espacios de las extensiones de archivo para las que desea desactivar el visor de OnlyOffice. (por ejemplo, '.txt .html').",
"disableOfficeEditor": "Desactivar el visor de office para determinadas extensiones de archivo",
"disableOfficePreviews": "Desactivar la vista previa de office para determinadas extensiones de archivo",
"disableOfficePreviewsDescription": "Una lista separada por espacios de extensiones de archivo para las que deshabilitar las vistas previas de office. (por ejemplo, '.txt .html')."
},
"sidebar": {
"help": "Ayuda",
@@ -365,7 +369,8 @@
"highQualityPreview": "Vista previa de alta calidad",
"previewOffice": "Vista previa de archivos ofimáticos",
"popupPreview": "Activar la vista previa emergente",
"filePreviewOptions": "Opciones de previsualización de archivos"
"filePreviewOptions": "Opciones de previsualización de archivos",
"editOffice": "Editar/ver archivos ofimáticos"
},
"otp": {
"name": "Autenticación de dos factores",
+6 -1
View File
@@ -283,7 +283,11 @@
"enterPasswordAgain": "Saisir à nouveau le mot de passe",
"passwordsDoNotMatch": "Les mots de passe ne correspondent pas",
"loginMethod": "Méthode de connexion",
"shareSettings": "Paramètres des partages"
"shareSettings": "Paramètres des partages",
"disableOfficeEditorDescription": "Une liste d'extensions de fichiers séparées par des espaces pour lesquelles le visualisateur OnlyOffice doit être désactivé. (par exemple, '.txt .html').",
"disableOfficeEditor": "Désactiver la visionneuse d'office pour certaines extensions de fichiers",
"disableOfficePreviews": "Désactiver les aperçus Office pour certaines extensions de fichiers",
"disableOfficePreviewsDescription": "Une liste d'extensions de fichiers séparées par des espaces pour lesquelles les aperçus Office doivent être désactivés. (par exemple, '.txt .html')."
},
"sidebar": {
"help": "Aide",
@@ -365,6 +369,7 @@
"highQualityPreview": "Aperçu de haute qualité",
"previewOffice": "Prévisualisation des fichiers bureautique",
"popupPreview": "Activer la prévisualisation dans les fenêtres contextuelles",
"editOffice": "Editer/visualiser des fichiers bureautiques",
"filePreviewOptions": "Options de prévisualisation des fichiers"
},
"otp": {
+119 -11
View File
@@ -35,7 +35,12 @@
"toggleSidebar": "פתיחת / סגירת סרגל צד",
"update": "עדכון",
"upload": "העלאה",
"openFile": "פתח קובץ"
"openFile": "פתח קובץ",
"generateNewOtp": "אפס וצור קוד דו-גורמי חדש",
"verify": "אמת",
"copyDownloadLinkToClipboard": "העתק את קישור ההורדה ללוח",
"continue": "המשך",
"edit": "ערוך"
},
"download": {
"downloadFile": "הורד קובץ",
@@ -116,7 +121,8 @@
"submit": "התחברות",
"username": "שם משתמש",
"usernameTaken": "שם משתמש כבר קיים",
"wrongCredentials": "פרטי התחברות שגויים"
"wrongCredentials": "פרטי התחברות שגויים",
"or": "או"
},
"permanent": "קבוע",
"prompts": {
@@ -153,7 +159,11 @@
"upload": "העלאה",
"uploadFiles": "מעלה {files} קבצים...",
"uploadMessage": "בחר אפשרות העלאה.",
"optionalPassword": "סיסמא אופציונלית"
"optionalPassword": "סיסמא אופציונלית",
"deleteUserMessage": "האם אתה בטוח שברצונך למחוק משתמש זה?",
"selected": "נבחר",
"filesSelected": "קבצים שנבחרו",
"typeName": "סוג:"
},
"search": {
"images": "תמונות",
@@ -163,7 +173,12 @@
"search": "חפש...",
"typeToSearch": "הקלד לחיפוש...",
"types": "סוג",
"video": "וידאו"
"video": "וידאו",
"path": "נתיב:",
"smallerThan": "קטן מ:",
"largerThan": "גדול מ:",
"helpText1": "החיפוש מתבצע על כל תו שאתה מקליד (מינימום 3 תווים עבור מונחי חיפוש).",
"helpText2": "האינדקס: החיפוש משתמש באינדקס שמתעדכן אוטומטית במרווח הזמן שהוגדר (ברירת מחדל: 5 דקות). חיפוש בעת הפעלת התוכנית עלול להניב תוצאות חלקיות."
},
"settings": {
"admin": "מנהל",
@@ -212,7 +227,10 @@
"execute": "ביצוע פקודות",
"modify": "עריכת קבצים קבצים",
"rename": "שינוי שם או העברת קבצים ותקיות",
"share": "שיתוף קבצים"
"share": "שיתוף קבצים",
"admin": "מנהל",
"api": "יצירת מפתחות API ארוכי טווח וניהולם",
"realtime": "אפשר חיבורים ועדכונים בזמן אמת"
},
"permissions-name": "הרשאות",
"permissionsHelp": "אתה יכול להגדיר את המשתמש להיות מנהל מערכת או לבחור את ההרשאות בנפרד. אם תבחר \"מנהל מערכת\", כל ההרשאות ייבחרו אוטומטית. ניהול המשתמשים נשאר הרשאה של מנהל מערכת.\n",
@@ -242,7 +260,31 @@
"userManagement": "ניהול משתמש",
"userUpdated": "המשתמש עודכן!",
"username": "שם משתמש",
"users": "משתמשים"
"users": "משתמשים",
"enterPassword": "הזן סיסמה",
"enterPasswordAgain": "הזן את הסיסמה שוב",
"passwordsDoNotMatch": "הסיסמאות אינן תואמות",
"loginMethod": "שיטת כניסה",
"loginMethodDescription": "שיטת כניסה מותרת למשתמש.",
"modifyOtherUser": "שינוי משתמש:",
"modifyCurrentUser": "שינוי המשתמש הנוכחי",
"themeColor": "צבע נושא",
"disableOfficeEditorDescription": "רשימה מופרדת ברווחים של סיומות קבצים שיש להשבית את הצופה OnlyOffice עבורן. (לדוגמה, '.txt .html').",
"disableOfficeEditor": "השבת תצוגת Office עבור סיומות קבצים מסוימות",
"disableOfficePreviews": "השבת תצוגה מקדימה של Office עבור סיומות קבצים מסוימות",
"disableOfficePreviewsDescription": "רשימה מופרדת ברווחים של סיומות קבצים שצפייה מקדימה שלהם במשרד תושבת. (לדוגמה, '.txt .html').",
"listingOptions": "אפשרויות רישום",
"emptyGlobal": "כרגע אין אפשרויות תצורה גלובליות. ניתן לערוך את קובץ ה-yaml כדי לשנות את הגדרות המערכת.",
"scopes": "הגדר מקורות מותרים והיקף המשתמשים בתוכם",
"UserManagement": "ניהול משתמשים",
"userNotAdmin": "המשתמש אינו מנהל",
"tusUploads": "העלאות מקובצות",
"tusUploadsHelp": "דפדפן הקבצים תומך בהעלאת קבצים מקוטעים, מה שמאפשר יצירת העלאות קבצים יעילות, אמינות, ניתנות לחידוש ולקטעים, אפילו ברשתות לא אמינות.",
"tusUploadsChunkSize": "ציון הגודל המרבי של בקשה (העלאות קטנות יותר ייעשה באמצעות העלאה ישירה). ניתן להזין מספר שלם המציין את גודל הקובץ בבytes או מחרוזת כגון 10MB, 1GB וכו'.",
"tusUploadsRetryCount": "מספר הניסיונות שיש לבצע אם העלאת קטע נכשלה.",
"disableUsedDiskPercentage": "השבת גרף אחוזי השימוש בדיסק",
"shareSettings": "הגדרות שיתוף",
"api": "מפתחות API"
},
"sidebar": {
"help": "עזרה",
@@ -255,7 +297,8 @@
"preview": "תצוגה מקדימה",
"settings": "הגדרות",
"signup": "הרשמה",
"siteSettings": "הגדרות"
"siteSettings": "הגדרות",
"sources": "מקורות"
},
"success": {
"linkCopied": "הקישור הועתק!"
@@ -267,8 +310,73 @@
"seconds": "שניות",
"unit": "יחידת זמן"
},
"upload": {},
"share": {},
"api": {},
"index": {}
"upload": {
"abortUpload": "האם אתה בטוח שברצונך לבטל?"
},
"share": {
"notice": "הערה: כל מי שיש לו גישה לקישור (ולסיסמה האופציונלית) יכול לגשת לקבצים המשותפים. אין צורך להיות מחובר."
},
"api": {
"title": "מפתחות API",
"keyName": "שם מפתח API:",
"createdAt": "נוצר ב:",
"expiresAt": "תוקף ב:",
"permissions": "הרשאות:",
"createTitle": "צור מפתח API",
"keyNamePlaceholder": "הזן שם מפתח API ייחודי:",
"tokenDuration": "משך תוקף האסימון:",
"durationNumberPlaceholder": "מספר",
"days": "ימים",
"months": "חודשים",
"permissionNote": "בחר לפחות הרשאה אחת עבור המפתח. גם למשתמש שלך חייבת להיות הרשאה זו.",
"createKeyFailed": "לא ניתן ליצור מפתח API.",
"createKeySuccess": "מפתח API נוצר בהצלחה.",
"description": "מפתחות API מבוססים על המשתמש שיצר אותם. ראה",
"swaggerLinkText": "דף סוואגר",
"name": "שם",
"created": "נוצר",
"expires": "תוקף",
"actions": "פעולות",
"enabled": "מופעל",
"disabled": "נכה"
},
"index": {
"status": "סטטוס",
"assessment": "הערכה",
"files": "קבצים",
"folders": "תיקיות",
"lastScanned": "נסרק לאחרונה",
"quickScan": "סריקה מהירה בשניות",
"fullScan": "שניות סריקה מלאה",
"settingsHover": "הגדרות למשתמש",
"logout": "התנתק",
"toggleClick": "החלף לחיצה אחת",
"toggleDark": "הפעל מצב כהה",
"toggleSticky": "הפעל/כבה מצב דביק",
"of": "של",
"used": "משומש"
},
"profileSettings": {
"setDateFormat": "הגדר פורמט תאריך מדויק",
"showHiddenFiles": "הצג קבצים מוסתרים",
"showQuickDownload": "הצג סמל הורדה לגישה מהירה",
"previewImages": "תצוגה מקדימה של תמונות",
"previewVideos": "הצגת סרטונים",
"previewMotionVideos": "תצוגה מקדימה של סרטונים",
"highQualityPreview": "תצוגה מקדימה באיכות גבוהה",
"editOffice": "ערוך/הצג קבצי Office",
"popupPreview": "הפעל תצוגה מקדימה מוקפצת",
"filePreviewOptions": "אפשרויות תצוגה מקדימה של קבצים"
},
"otp": {
"name": "אימות דו-גורמי",
"verifyInstructions": "הזן את הקוד מהאפליקציה המאמתת שלך",
"generate": "סרוק את קוד ה-QR באמצעות אפליקציית האימות שלך",
"codeInputPlaceholder": "קוד בן 6 ספרות",
"invalidCodeType": "קוד האימות צריך להיות בן 6 ספרות באותיות לטיניות",
"enabledSuccessfully": "OTP הופעל בהצלחה",
"verificationFailed": "האימות נכשל",
"verificationSucceed": "האימות הצליח",
"generationFailed": "דור שנכשל"
}
}
+7 -2
View File
@@ -283,7 +283,11 @@
"enterPasswordAgain": "Adja meg újra a jelszót",
"passwordsDoNotMatch": "A jelszavak nem egyeznek",
"loginMethod": "Bejelentkezési módszer",
"shareSettings": "Beállítások megosztása"
"shareSettings": "Beállítások megosztása",
"disableOfficeEditorDescription": "Azon fájlkiterjesztések szóközzel elválasztott listája, amelyek esetében az OnlyOffice megjelenítőjét le kell tiltani. (pl. '.txt .html').",
"disableOfficeEditor": "Bizonyos fájlkiterjesztéseknél letilthatja az Office Viewer-t",
"disableOfficePreviews": "Office előnézet letiltása bizonyos fájlkiterjesztésekhez",
"disableOfficePreviewsDescription": "Az irodai előnézetek letiltásának szóközzel elválasztott fájlkiterjesztések listája. (pl. '.txt .html')."
},
"sidebar": {
"help": "Súgó",
@@ -365,7 +369,8 @@
"highQualityPreview": "Kiváló minőségű előnézet",
"previewOffice": "Office fájlok előnézete",
"popupPreview": "Pop-up előnézet engedélyezése",
"filePreviewOptions": "Fájl előnézeti beállítások"
"filePreviewOptions": "Fájl előnézeti beállítások",
"editOffice": "Office fájlok szerkesztése/megjelenítése"
},
"otp": {
"name": "Kétfaktoros hitelesítés",
+7 -2
View File
@@ -283,7 +283,11 @@
"enterPasswordAgain": "Immettere nuovamente la password",
"passwordsDoNotMatch": "Le password non corrispondono",
"loginMethod": "Metodo di accesso",
"shareSettings": "Condividi le impostazioni"
"shareSettings": "Condividi le impostazioni",
"disableOfficeEditorDescription": "Un elenco separato da spazi di estensioni di file per cui disabilitare il visualizzatore di OnlyOffice. (ad esempio, '.txt .html').",
"disableOfficeEditor": "Disattivare il visualizzatore di Office per alcune estensioni di file",
"disableOfficePreviews": "Disattivare le anteprime di Office per alcune estensioni di file",
"disableOfficePreviewsDescription": "Un elenco separato da spazi di estensioni di file per cui disabilitare le anteprime di Office. (ad esempio, '.txt .html')."
},
"sidebar": {
"help": "Aiuto",
@@ -365,7 +369,8 @@
"highQualityPreview": "Anteprima di alta qualità",
"previewOffice": "Anteprima dei file office",
"popupPreview": "Abilita l'anteprima a comparsa",
"filePreviewOptions": "Opzioni di anteprima dei file"
"filePreviewOptions": "Opzioni di anteprima dei file",
"editOffice": "Modificare/visualizzare i file dell'ufficio"
},
"otp": {
"name": "Autenticazione a due fattori",
+7 -2
View File
@@ -283,7 +283,11 @@
"enterPasswordAgain": "もう一度パスワードを入力する",
"passwordsDoNotMatch": "パスワードが一致しない",
"loginMethod": "ログイン方法",
"shareSettings": "共有設定"
"shareSettings": "共有設定",
"disableOfficeEditorDescription": "OnlyOffice ビューアを無効にするファイル拡張子をスペース区切りで指定します。(例: '.txt .html')。",
"disableOfficeEditor": "特定のファイル拡張子のオフィスビューアを無効にする",
"disableOfficePreviews": "特定のファイル拡張子でオフィスのプレビューを無効にする",
"disableOfficePreviewsDescription": "オフィスプレビューを無効にするファイル拡張子をスペースで区切ったリスト。(例: '.txt .html')。"
},
"sidebar": {
"help": "ヘルプ",
@@ -365,7 +369,8 @@
"highQualityPreview": "高画質プレビュー",
"previewOffice": "オフィスファイルのプレビュー",
"popupPreview": "ポップアップ・プレビューを有効にする",
"filePreviewOptions": "ファイルプレビューオプション"
"filePreviewOptions": "ファイルプレビューオプション",
"editOffice": "オフィスファイルの編集/閲覧"
},
"otp": {
"name": "二要素認証",
+7 -2
View File
@@ -283,7 +283,11 @@
"enterPasswordAgain": "비밀번호를 다시 입력합니다.",
"passwordsDoNotMatch": "비밀번호가 일치하지 않습니다.",
"loginMethod": "로그인 방법",
"shareSettings": "공유 설정"
"shareSettings": "공유 설정",
"disableOfficeEditorDescription": "OnlyOffice 뷰어를 비활성화할 파일 확장명을 공백으로 구분한 목록입니다. (예: '.txt .html').",
"disableOfficeEditor": "특정 파일 확장자에 대해 Office 뷰어 사용 안 함",
"disableOfficePreviews": "특정 파일 확장명에 대한 Office 미리 보기 사용 안 함",
"disableOfficePreviewsDescription": "Office 미리 보기를 사용하지 않도록 설정할 파일 확장명을 공백으로 구분한 목록입니다. (예: '.txt .html')."
},
"sidebar": {
"help": "도움말",
@@ -365,7 +369,8 @@
"highQualityPreview": "고품질 미리보기",
"previewOffice": "오피스 파일 미리 보기",
"popupPreview": "팝업 미리 보기 사용",
"filePreviewOptions": "파일 미리 보기 옵션"
"filePreviewOptions": "파일 미리 보기 옵션",
"editOffice": "오피스 파일 편집/보기"
},
"otp": {
"name": "2단계 인증",
+7 -2
View File
@@ -283,7 +283,11 @@
"enterPasswordAgain": "Wprowadź hasło ponownie",
"passwordsDoNotMatch": "Hasła nie są zgodne",
"loginMethod": "Metoda logowania",
"shareSettings": "Ustawienia udostępniania"
"shareSettings": "Ustawienia udostępniania",
"disableOfficeEditorDescription": "Rozdzielana spacjami lista rozszerzeń plików, dla których przeglądarka OnlyOffice ma zostać wyłączona. (np. \".txt .html\").",
"disableOfficeEditor": "Wyłącz przeglądarkę Office dla niektórych rozszerzeń plików",
"disableOfficePreviews": "Wyłącz podglądy Office dla niektórych rozszerzeń plików",
"disableOfficePreviewsDescription": "Rozdzielana spacjami lista rozszerzeń plików do wyłączenia podglądu plików biurowych. (np. \".txt .html\")."
},
"sidebar": {
"help": "Pomoc",
@@ -365,7 +369,8 @@
"highQualityPreview": "Podgląd wysokiej jakości",
"previewOffice": "Podgląd plików biurowych",
"popupPreview": "Włącz wyskakujący podgląd",
"filePreviewOptions": "Opcje podglądu plików"
"filePreviewOptions": "Opcje podglądu plików",
"editOffice": "Edycja/przeglądanie plików biurowych"
},
"otp": {
"name": "Uwierzytelnianie dwuskładnikowe",
+7 -2
View File
@@ -283,7 +283,11 @@
"enterPasswordAgain": "Digite a senha novamente",
"passwordsDoNotMatch": "As senhas não correspondem",
"loginMethod": "Método de login",
"shareSettings": "Compartilhar configurações"
"shareSettings": "Compartilhar configurações",
"disableOfficeEditorDescription": "Uma lista de extensões de arquivo separada por espaços para desativar o visualizador do OnlyOffice. (por exemplo, '.txt .html').",
"disableOfficeEditor": "Desativar o visualizador do Office para determinadas extensões de arquivo",
"disableOfficePreviews": "Desativar as visualizações do Office para determinadas extensões de arquivo",
"disableOfficePreviewsDescription": "Uma lista de extensões de arquivo separada por espaços para desativar as visualizações do Office. (por exemplo, '.txt .html')."
},
"sidebar": {
"help": "Ajuda",
@@ -365,7 +369,8 @@
"highQualityPreview": "Visualização de alta qualidade",
"previewOffice": "Visualizar arquivos do Office",
"popupPreview": "Ativar visualização pop-up",
"filePreviewOptions": "Opções de visualização de arquivos"
"filePreviewOptions": "Opções de visualização de arquivos",
"editOffice": "Editar/visualizar arquivos de escritório"
},
"otp": {
"name": "Autenticação de dois fatores",
+7 -2
View File
@@ -283,7 +283,11 @@
"enterPasswordAgain": "Introduceți din nou parola",
"passwordsDoNotMatch": "Parolele nu se potrivesc",
"loginMethod": "Metoda de conectare",
"shareSettings": "Setări de partajare"
"shareSettings": "Setări de partajare",
"disableOfficeEditorDescription": "O listă de extensii de fișiere separate prin spațiu pentru care se dezactivează vizualizatorul OnlyOffice. (de exemplu, \".txt .html\").",
"disableOfficeEditor": "Dezactivați vizualizatorul office pentru anumite extensii de fișiere",
"disableOfficePreviews": "Dezactivați previzualizările office pentru anumite extensii de fișiere",
"disableOfficePreviewsDescription": "O listă de extensii de fișiere separate prin spațiu pentru care se dezactivează previzualizările office. (de exemplu, \".txt .html\")."
},
"sidebar": {
"help": "Ajutor",
@@ -365,7 +369,8 @@
"highQualityPreview": "Previzualizare de înaltă calitate",
"previewOffice": "Previzualizare fișiere office",
"popupPreview": "Activați previzualizarea pop-up",
"filePreviewOptions": "Opțiuni de previzualizare a fișierelor"
"filePreviewOptions": "Opțiuni de previzualizare a fișierelor",
"editOffice": "Editarea/vizualizarea fișierelor office"
},
"otp": {
"name": "Autentificare cu doi factori",
+7 -2
View File
@@ -283,7 +283,11 @@
"enterPasswordAgain": "Введите пароль еще раз",
"passwordsDoNotMatch": "Пароли не совпадают",
"loginMethod": "Метод входа в систему",
"shareSettings": "Поделиться настройками"
"shareSettings": "Поделиться настройками",
"disableOfficeEditorDescription": "Список расширений файлов, для которых необходимо отключить программу просмотра OnlyOffice, разделенный пробелами. (например, '.txt .html').",
"disableOfficeEditor": "Отключение просмотра офисных файлов для определенных расширений файлов",
"disableOfficePreviews": "Отключение предварительного просмотра в офисе для определенных расширений файлов",
"disableOfficePreviewsDescription": "Список расширений файлов, для которых необходимо отключить предварительный просмотр в офисе, разделенный пробелами. (например, '.txt .html')."
},
"sidebar": {
"help": "Помощь",
@@ -365,7 +369,8 @@
"highQualityPreview": "Высококачественный предварительный просмотр",
"previewOffice": "Предварительный просмотр офисных файлов",
"popupPreview": "Включить предварительный просмотр всплывающих окон",
"filePreviewOptions": "Параметры предварительного просмотра файлов"
"filePreviewOptions": "Параметры предварительного просмотра файлов",
"editOffice": "Редактирование/просмотр офисных файлов"
},
"otp": {
"name": "Двухфакторная аутентификация",
+7 -2
View File
@@ -283,7 +283,11 @@
"enterPasswordAgain": "Znovu zadajte heslo",
"passwordsDoNotMatch": "Heslá sa nezhodujú",
"loginMethod": "Spôsob prihlásenia",
"shareSettings": "Zdieľať nastavenia"
"shareSettings": "Zdieľať nastavenia",
"disableOfficeEditorDescription": "Medzerou oddelený zoznam prípon súborov, pre ktoré sa má prehliadač OnlyOffice vypnúť. (napr. \".txt .html\").",
"disableOfficeEditor": "Zakázanie prehliadača Office pre určité prípony súborov",
"disableOfficePreviews": "Zakázanie náhľadov kancelárskych súborov pre určité prípony súborov",
"disableOfficePreviewsDescription": "Medzerou oddelený zoznam prípon súborov, pre ktoré chcete vypnúť náhľady kancelárskych súborov. (napr. \".txt .html\")."
},
"sidebar": {
"help": "Pomoc",
@@ -365,7 +369,8 @@
"highQualityPreview": "Vysokokvalitný náhľad",
"previewOffice": "Náhľad kancelárskych súborov",
"popupPreview": "Povolenie pop-up náhľadu",
"filePreviewOptions": "Možnosti náhľadu súborov"
"filePreviewOptions": "Možnosti náhľadu súborov",
"editOffice": "Úprava/zobrazenie kancelárskych súborov"
},
"otp": {
"name": "Dvojfaktorové overovanie",
+7 -2
View File
@@ -283,7 +283,11 @@
"enterPasswordAgain": "Şifreyi tekrar girin",
"passwordsDoNotMatch": "Parolalar eşleşmiyor",
"loginMethod": "Giriş yöntemi",
"shareSettings": "Paylaşım Ayarları"
"shareSettings": "Paylaşım Ayarları",
"disableOfficeEditorDescription": "OnlyOffice görüntüleyicisinin devre dışı bırakılacağı dosya uzantılarının boşluk bırakılarak ayrılmış bir listesi. (örneğin, '.txt .html').",
"disableOfficeEditor": "Belirli dosya uzantıları için ofis görüntüleyiciyi devre dışı bırakma",
"disableOfficePreviews": "Belirli dosya uzantıları için ofis önizlemelerini devre dışı bırakma",
"disableOfficePreviewsDescription": "Ofis önizlemelerinin devre dışı bırakılacağı dosya uzantılarının boşluk bırakılarak ayrılmış bir listesi. (örneğin, '.txt .html')."
},
"sidebar": {
"help": "Yardım",
@@ -365,7 +369,8 @@
"highQualityPreview": "Yüksek kaliteli önizleme",
"previewOffice": "Ofis dosyalarını önizleme",
"popupPreview": "Açılır pencere önizlemesini etkinleştir",
"filePreviewOptions": "Dosya önizleme seçenekleri"
"filePreviewOptions": "Dosya önizleme seçenekleri",
"editOffice": "Ofis dosyalarını düzenleme/görüntüleme"
},
"otp": {
"name": "İki Faktörlü Kimlik Doğrulama",
+174 -135
View File
@@ -1,163 +1,181 @@
import { removePrefix } from "@/utils/url.js";
import { getFileExtension } from "@/utils/files.js";
import { state,mutations } from "@/store";
import { noAuth } from "@/utils/constants.js";
import { getTypeInfo } from "@/utils/mimetype";
import { fromNow } from "@/utils/moment";
import * as i18n from "@/i18n";
import { removePrefix } from '@/utils/url.js'
import { getFileExtension } from '@/utils/files.js'
import { state, mutations } from '@/store'
import { noAuth } from '@/utils/constants.js'
import { getTypeInfo } from '@/utils/mimetype'
import { fromNow } from '@/utils/moment'
import * as i18n from '@/i18n'
export const getters = {
getTime: (timestamp) => {
getTime: timestamp => {
if (state.user.dateFormat) {
// Truncate the fractional seconds to 3 digits (milliseconds)
const sanitizedString = timestamp.replace(/\.\d+/, (match) =>
const sanitizedString = timestamp.replace(/\.\d+/, match =>
match.slice(0, 4)
);
)
// Parse the sanitized string into a Date object
const date = new Date(sanitizedString);
return date.toLocaleString();
const date = new Date(sanitizedString)
return date.toLocaleString()
}
return fromNow(timestamp, state.user.locale);
return fromNow(timestamp, state.user.locale)
},
isScrollable: () => {
const currentView = getters.currentView();
if (currentView == "preview") {
return false;
const cv = getters.currentView()
if (
cv == 'preview' ||
cv == 'onlyOfficeEditor' ||
cv == 'markdownViewer' ||
cv == 'epubViewer' ||
cv == 'docViewer' ||
cv == 'editor'
) {
return false
}
return true
},
previewType: () => getTypeInfo(state.req.type).simpleType,
isCardView: () => (state.user.viewMode == "gallery" || state.user.viewMode == "normal" ) && getters.currentView() == "listingView" ,
currentHash: () => state.route.hash.replace("#", ""),
isCardView: () =>
(state.user.viewMode == 'gallery' || state.user.viewMode == 'normal') &&
getters.currentView() == 'listingView',
currentHash: () => state.route.hash.replace('#', ''),
isMobile: () => state.isMobile,
isLoading: () => Object.keys(state.loading).length > 0,
isSettings: () => getters.currentView() === "settings",
isShare: () => getters.currentView() === "share",
isSettings: () => getters.currentView() === 'settings',
isShare: () => getters.currentView() === 'share',
isDarkMode: () => {
if (state.user == null) {
return true;
return true
}
return state.user.darkMode === true;
return state.user.darkMode === true
},
isLoggedIn: () => {
if (state.user == null) {
return false;
return false
}
if (state.user.locale == undefined || state.user.locale == null) {
let savedLocale = localStorage.getItem("userLocale");
let savedLocale = localStorage.getItem('userLocale')
if (!savedLocale) {
savedLocale = i18n.detectLocale();
savedLocale = i18n.detectLocale()
}
mutations.updateCurrentUser({ locale: savedLocale });
mutations.updateCurrentUser({ locale: savedLocale })
}
if (noAuth) {
return true
}
if (state.user !== null && state.user?.username != "" && state.user?.username != "publicUser") {
return true;
if (
state.user !== null &&
state.user?.username != '' &&
state.user?.username != 'publicUser'
) {
return true
}
return false
},
isAdmin: () => state.user.permissions?.admin == true,
isFiles: () => state.route.name === "Files",
isListing: () => getters.isFiles() && state.req.type === "directory",
selectedCount: () => Array.isArray(state.selected) ? state.selected.length : 0,
isFiles: () => state.route.name === 'Files',
isListing: () => getters.isFiles() && state.req.type === 'directory',
selectedCount: () =>
Array.isArray(state.selected) ? state.selected.length : 0,
getFirstSelected: () => state.req.items[state.selected[0]],
isSingleFileSelected: () => getters.selectedCount() === 1 && getters.getFirstSelected()?.type != "directory",
selectedDownloadUrl() {
isSingleFileSelected: () =>
getters.selectedCount() === 1 &&
getters.getFirstSelected()?.type != 'directory',
selectedDownloadUrl () {
if (state.isSearchActive) {
return state.selected[0].url;
return state.selected[0].url
}
let selectedItem = state.selected[0]
return state.req.items[selectedItem].url;
return state.req.items[selectedItem].url
},
reqNumDirs: () => {
let dirCount = 0;
let dirCount = 0
if (!state.req.items) {
return 0;
return 0
}
state.req.items.forEach((item) => {
state.req.items.forEach(item => {
// Check if the item is a directory
if (item.type == "directory") {
if (item.type == 'directory') {
// Otherwise, count this directory
dirCount++;
dirCount++
}
});
})
// Return the directory count
return dirCount;
return dirCount
},
reqNumFiles: () => {
let fileCount = 0;
let fileCount = 0
if (!state.req.items) {
return 0;
return 0
}
state.req.items.forEach((item) => {
state.req.items.forEach(item => {
// Check if the item is a directory
if (item.type != "directory") {
if (item.type != 'directory') {
// Otherwise, count this directory
fileCount++;
fileCount++
}
});
})
// Return the directory count
return fileCount;
return fileCount
},
reqItems: () => {
if (state.user == null) {
return {};
return {}
}
const dirs = [];
const files = [];
const dirs = []
const files = []
if (!state.req.items) {
return { dirs, files };
return { dirs, files }
}
state.req.items.forEach((item) => {
if (item.type == "directory") {
dirs.push(item);
state.req.items.forEach(item => {
if (item.type == 'directory') {
dirs.push(item)
} else {
item.Path = state.req.Path;
files.push(item);
item.Path = state.req.Path
files.push(item)
}
});
return { dirs, files };
})
return { dirs, files }
},
isSidebarVisible: () => {
const currentView = getters.currentView();
let visible = (state.showSidebar || getters.isStickySidebar()) && state.user.username != "publicUser"
if (currentView == "settings") {
visible = !getters.isMobile();
const currentView = getters.currentView()
let visible =
(state.showSidebar || getters.isStickySidebar()) &&
state.user.username != 'publicUser'
if (currentView == 'settings') {
visible = !getters.isMobile()
}
if (currentView == "share") {
if (currentView == 'share') {
visible = false
}
if (typeof getters.currentPromptName() === "string" && !getters.isStickySidebar()) {
visible = false;
if (
typeof getters.currentPromptName() === 'string' &&
!getters.isStickySidebar()
) {
visible = false
}
if (currentView == "preview") {
if (currentView == 'preview') {
const contentType = getters.previewType()
if (
contentType == "audio" ||
contentType == "video" ||
contentType == "image" ||
contentType == "pdf"
contentType == 'audio' ||
contentType == 'video' ||
contentType == 'image' ||
contentType == 'pdf'
) {
visible = false
}
}
if (
currentView == "editor" ||
currentView == "onlyOfficeEditor"
) {
visible = false;
if (currentView == 'editor' || currentView == 'onlyOfficeEditor') {
visible = false
}
return visible
},
isStickySidebar: () => {
let sticky = state.user?.stickySidebar;
const currentView = getters.currentView();
if (currentView == "settings") {
let sticky = state.user?.stickySidebar
const currentView = getters.currentView()
if (currentView == 'settings') {
sticky = true
}
if (currentView == null && !getters.isLoading() && getters.isShare() ) {
if (currentView == null && !getters.isLoading() && getters.isShare()) {
sticky = true
}
if (getters.isMobile()) {
@@ -166,83 +184,101 @@ export const getters = {
return sticky
},
showOverlay: () => {
const hasPrompt = getters.currentPrompt() !== null && getters.currentPromptName() !== "more"
const showForSidebar = getters.isSidebarVisible() && !getters.isStickySidebar()
return hasPrompt || showForSidebar || state.isSearchActive;
const hasPrompt =
getters.currentPrompt() !== null && getters.currentPromptName() !== 'more'
const showForSidebar =
getters.isSidebarVisible() && !getters.isStickySidebar()
return hasPrompt || showForSidebar || state.isSearchActive
},
showBreadCrumbs: () => {
return getters.currentView() == "listingView" ;
return getters.currentView() == 'listingView'
},
routePath: (trimModifier="") => {
return removePrefix(state.route.path,trimModifier)
routePath: (trimModifier = '') => {
return removePrefix(state.route.path, trimModifier)
},
sharePathBase: () => {
let urlPath = getters.routePath("share");
let urlPath = getters.routePath('share')
// Step 1: Split the path by '/'
let parts = urlPath.split("/");
let parts = urlPath.split('/')
// Step 2: Assign hash to the second part (index 2) and join the rest for subPath
return "/share/"+ parts[1] + "/";
return '/share/' + parts[1] + '/'
},
currentView: () => {
let listingView = null
const pathname = getters.routePath()
if (pathname.startsWith(`/settings`)) {
listingView = "settings"
listingView = 'settings'
} else if (pathname.startsWith(`/share`)) {
listingView = "share"
listingView = 'share'
} else if (pathname.startsWith(`/files`)) {
if (state.req.type !== undefined) {
if (state.req.type == "directory") {
listingView = "listingView";
if (state.req.type == 'directory') {
listingView = 'listingView'
} else if (getters.onlyOfficeEnabled(state.req.name)) {
listingView = "onlyOfficeEditor";
} else if ("content" in state.req && state.req.type == "text/markdown" && window.location.hash != "#edit") {
listingView = "markdownViewer";
} else if ("content" in state.req) {
listingView = "editor";
listingView = 'onlyOfficeEditor'
} else if (
'content' in state.req &&
state.req.type == 'text/markdown' &&
window.location.hash != '#edit'
) {
listingView = 'markdownViewer'
} else if ('content' in state.req) {
listingView = 'editor'
} else if (state.req.type.startsWith('application/epub')) {
listingView = 'epubViewer'
} else if (
state.req.type.startsWith(
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
)
) {
listingView = 'docViewer'
} else {
listingView = "preview";
listingView = 'preview'
}
} else {
listingView = "listingView"
listingView = 'listingView'
}
}
return listingView
},
progress: () => {
// Check if state.upload is defined and valid
if (!state.upload || !Array.isArray(state.upload.progress) || !Array.isArray(state.upload.sizes)) {
return 0;
if (
!state.upload ||
!Array.isArray(state.upload.progress) ||
!Array.isArray(state.upload.sizes)
) {
return 0
}
// Handle cases where progress or sizes arrays might be empty
if (state.upload.progress.length === 0 || state.upload.sizes.length === 0) {
return 0;
return 0
}
// Calculate totalSize
let totalSize = state.upload.sizes.reduce((a, b) => a + b, 0);
let totalSize = state.upload.sizes.reduce((a, b) => a + b, 0)
// Calculate sum of progress
let sum = state.upload.progress.reduce((acc, val) => acc + val, 0);
let sum = state.upload.progress.reduce((acc, val) => acc + val, 0)
// Return progress as a percentage
return Math.ceil((sum / totalSize) * 100);
return Math.ceil((sum / totalSize) * 100)
},
filesInUploadCount: () => {
const uploadsCount = state.upload.length
const queueCount = state.queue.length
return uploadsCount + queueCount;
return uploadsCount + queueCount
},
currentPrompt: () => {
// Ensure state.prompts is an array
if (!Array.isArray(state.prompts)) {
return null;
return null
}
if (state.prompts.length === 0) {
return null;
return null
}
return state.prompts[state.prompts.length - 1]
},
@@ -250,59 +286,62 @@ export const getters = {
currentPromptName: () => {
// Ensure state.prompts is an array
if (!Array.isArray(state.prompts) || state.prompts.length === 0) {
return null;
return null
}
// Check if the name property is a string
const lastPrompt = state.prompts[state.prompts.length - 1];
if (typeof lastPrompt?.name !== "string") {
return null;
const lastPrompt = state.prompts[state.prompts.length - 1]
if (typeof lastPrompt?.name !== 'string') {
return null
}
return lastPrompt.name;
return lastPrompt.name
},
filesInUpload: () => {
// Ensure state.upload.uploads is an object and state.upload.sizes is an array
if (typeof state.upload.uploads !== 'object' || !Array.isArray(state.upload.sizes)) {
return [];
if (
typeof state.upload.uploads !== 'object' ||
!Array.isArray(state.upload.sizes)
) {
return []
}
let files = [];
let files = []
for (let index in state.upload.uploads) {
let upload = state.upload.uploads[index];
let id = upload.id;
let type = upload.type;
let name = upload.file.name;
let size = state.upload.sizes[id] || 0; // Default to 0 if size is undefined
let isDir = upload.file.type == "directory";
let upload = state.upload.uploads[index]
let id = upload.id
let type = upload.type
let name = upload.file.name
let size = state.upload.sizes[id] || 0 // Default to 0 if size is undefined
let isDir = upload.file.type == 'directory'
let progress = isDir
? 100
: Math.ceil((state.upload.progress[id] || 0 / size) * 100); // Default to 0 if progress is undefined
: Math.ceil((state.upload.progress[id] || 0 / size) * 100) // Default to 0 if progress is undefined
files.push({
id,
name,
progress,
type,
isDir,
});
isDir
})
}
return files.sort((a, b) => a.progress - b.progress);
return files.sort((a, b) => a.progress - b.progress)
},
onlyOfficeEnabled: (filename) => {
onlyOfficeEnabled: filename => {
if (!state.req?.onlyOfficeId) {
return false
}
const ext = getFileExtension(filename);
const ext = getFileExtension(filename)
if (state.user.disableOnlyOfficeExt) {
const disabledList = state.user.disableOnlyOfficeExt.split(" ");
const disabledList = state.user.disableOnlyOfficeExt.split(' ')
for (const e of disabledList) {
if (e.trim().toLowerCase() === ext.toLowerCase()) {
return false;
return false
}
}
}
return true;
},
};
return true
}
}
+2 -2
View File
@@ -21,7 +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 muPdfAvailable = window.FileBrowser.MuPdfAvailable;
const origin = window.location.origin;
const settings = [
@@ -33,7 +33,7 @@ const settings = [
];
export {
pdfAvailable,
muPdfAvailable,
mediaAvailable,
oidcAvailable,
passwordAvailable,
+7 -1
View File
@@ -5,7 +5,13 @@ import { removePrefix } from "@/utils/url.js";
import { publicApi } from "@/api";
export default function download() {
if (getters.currentView() === "share") {
const currentView = getters.currentView();
if (currentView === "preview") {
startDownload(null, [state.req.url]);
return;
}
if (currentView === "share") {
let urlPath = getters.routePath("share");
let parts = urlPath.split("/");
const hash = parts[1];
+4
View File
@@ -25,6 +25,8 @@ import Preview from "@/views/files/Preview.vue";
import ListingView from "@/views/files/ListingView.vue";
import Editor from "@/views/files/Editor.vue";
import OnlyOfficeEditor from "./files/OnlyOfficeEditor.vue";
import EpubViewer from "./files/EpubViewer.vue";
import DocViewer from "./files/DocViewer.vue";
import MarkdownViewer from "./files/MarkdownViewer.vue";
import { state, mutations, getters } from "@/store";
import { url } from "@/utils";
@@ -41,6 +43,8 @@ export default {
Preview,
ListingView,
Editor,
EpubViewer,
DocViewer,
OnlyOfficeEditor,
MarkdownViewer,
PopupPreview,
+165
View File
@@ -0,0 +1,165 @@
<template>
<div class="viewer-background">
<div v-if="loading" class="status-text">Loading document...</div>
<div v-else-if="error" class="status-text error">{{ error }}</div>
<div v-else class="docx-page" v-html="docxHtml"></div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import * as mammoth from "mammoth";
import { filesApi } from "@/api";
import { state,mutations } from "@/store";
export default defineComponent({
name: "DocxViewer",
data() {
return {
docxHtml: "",
loading: false,
error: "",
};
},
watch: {
"state.req.path": {
handler(newPath, oldPath) {
// Added a console log to the watcher to see when it runs
console.log(
`[DEBUG] Watcher triggered: Path changed from "${oldPath}" to "${newPath}"`
);
this.loadFile();
},
immediate: true,
},
},
mounted() {
mutations.resetSelected();
mutations.addSelected({
name: state.req.name,
path: state.req.path,
size: state.req.size,
type: state.req.type,
source: state.req.source,
url: state.req.url,
});
},
methods: {
async loadFile() {
console.log("%c[1] loadFile triggered.", "font-weight: bold; color: blue;");
try {
// Using JSON.stringify to snapshot the state object at this exact moment
console.log(
"[2] Current state.req object:",
JSON.parse(JSON.stringify(state.req))
);
const filename = state.req.name;
console.log(`[3] Checking filename: "${filename}"`);
// Check if the filename is valid and ends with .docx
if (!filename || !filename.toLowerCase().endsWith(".docx")) {
this.error = `This viewer only supports .docx files. Current file: "${
filename || "Not available"
}"`;
console.error(`[3a] Filename check FAILED. Stopping execution.`);
this.docxHtml = ""; // Ensure view is cleared
return;
}
console.log("[3a] Filename check PASSED.");
this.loading = true;
this.error = "";
this.docxHtml = "";
console.log("[4] Getting download URL from filesApi...");
const downloadUrl = filesApi.getDownloadURL(
state.req.source,
state.req.path,
false,
true
);
if (!downloadUrl) {
throw new Error("Could not retrieve a valid download URL from the API.");
}
console.log("[5] Got download URL:", downloadUrl);
console.log("[6] Fetching data from URL...");
const response = await fetch(downloadUrl);
console.log("[7] Fetch response received. Status:", response.status);
if (!response.ok) {
throw new Error(`Failed to download file (Status: ${response.status})`);
}
console.log("[8] Getting ArrayBuffer from response...");
const arrayBuffer = await response.arrayBuffer();
console.log(`[8a] Got ArrayBuffer. Size: ${arrayBuffer.byteLength} bytes.`);
if (arrayBuffer.byteLength === 0) {
throw new Error("Downloaded file is empty (0 bytes).");
}
console.log("[9] Passing ArrayBuffer to mammoth.js for conversion...");
const result = await mammoth.convertToHtml({ arrayBuffer });
this.docxHtml = result.value;
console.log(
"%c[10] SUCCESS: Document rendered.",
"font-weight: bold; color: green;"
);
} catch (e) {
console.error("%c[X] CATCH BLOCK ERROR:", "font-weight: bold; color: red;", e);
this.error = e.message || "An unknown error occurred.";
} finally {
this.loading = false;
console.log("[F] FINALLY block executed.");
}
},
},
});
</script>
<style scoped>
/* Styles remain the same */
.viewer-background {
width: 100%;
height: 100%;
overflow-y: auto;
background-color: #f0f2f5;
padding: 2em;
box-sizing: border-box;
}
.docx-page {
background: white;
width: 8.5in;
min-height: 11in;
margin: 0 auto;
padding: 1in;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
box-sizing: border-box;
}
.status-text {
text-align: center;
padding: 3em;
font-family: sans-serif;
color: #333;
font-size: 1.2em;
}
.status-text.error {
color: #d9534f;
}
@media (max-width: 8.5in) {
.viewer-background {
padding: 0;
}
.docx-page {
width: 100%;
min-height: 100%;
margin: 0;
padding: 1em;
box-shadow: none;
}
}
</style>
+69 -37
View File
@@ -6,7 +6,7 @@
<script>
import { eventBus } from "@/store/eventBus";
import { state, getters } from "@/store";
import { state, getters, mutations } from "@/store";
import { filesApi } from "@/api";
import ace, { version as ace_version } from "ace-builds";
import modelist from "ace-builds/src-noconflict/ext-modelist";
@@ -16,63 +16,95 @@ import "ace-builds/src-min-noconflict/theme-twilight";
export default {
name: "editor",
data: function () {
return {};
return {
editor: null, // The editor instance
};
},
computed: {
isDarkMode() {
return getters.isDarkMode();
},
},
watch: {
$route() {
if (this.editor) {
this.editor.destroy();
this.editor = null;
}
// Wait for the DOM to update after the route change
this.$nextTick(() => {
this.setupEditor();
});
},
},
created() {
window.addEventListener("keydown", this.keyEvent);
},
beforeUnmount() {
window.removeEventListener("keydown", this.keyEvent);
this.editor.destroy();
if (this.editor) {
this.editor.destroy();
}
},
mounted: function () {
ace.config.set(
"basePath",
`https://cdn.jsdelivr.net/npm/ace-builds@${ace_version}/src-min-noconflict/`
);
// this is empty content string "empty-file-x6OlSil" which is used to represent empty text file
const fileContent =
state.req.content == "empty-file-x6OlSil" ? "" : state.req.content || "";
this.editor = ace.edit("editor", {
mode: modelist.getModeForPath(state.req.name).mode,
value: fileContent,
showPrintMargin: false,
theme: "ace/theme/chrome",
readOnly: state.req.type === "textImmutable",
wrap: false,
mutations.resetSelected();
mutations.addSelected({
name: state.req.name,
path: state.req.path,
size: state.req.size,
type: state.req.type,
source: state.req.source,
url: state.req.url,
});
// Wait for the initial DOM render to complete
this.$nextTick(() => {
this.setupEditor();
});
// Set the basePath for Ace Editor
if (this.isDarkMode) {
this.editor.setTheme("ace/theme/twilight");
}
eventBus.on("handleEditorValueRequest", this.handleEditorValueRequest);
},
methods: {
setupEditor() {
const editorEl = document.getElementById("editor");
if (!editorEl) {
console.warn(
"Editor component mounted, but #editor div was not found in the DOM. Aborting setup."
);
return;
}
ace.config.set(
"basePath",
`https://cdn.jsdelivr.net/npm/ace-builds@${ace_version}/src-min-noconflict/`
);
const fileContent =
state.req.content == "empty-file-x6OlSil" ? "" : state.req.content || "";
this.editor = ace.edit(editorEl, {
mode: modelist.getModeForPath(state.req.name).mode,
value: fileContent,
showPrintMargin: false,
theme: "ace/theme/chrome",
readOnly: state.req.type === "textImmutable",
wrap: false,
});
if (this.isDarkMode) {
this.editor.setTheme("ace/theme/twilight");
}
},
handleEditorValueRequest() {
filesApi.put(state.req.path, state.req.source, this.editor.getValue());
if (this.editor) {
filesApi.put(state.req.path, state.req.source, this.editor.getValue());
}
},
keyEvent(event) {
const { key, ctrlKey, metaKey } = event;
if (getters.currentPromptName() != null) {
return;
}
if (!ctrlKey && !metaKey) {
return;
}
switch (key.toLowerCase()) {
case "s":
event.preventDefault();
this.handleEditorValueRequest();
break;
default:
// No action for other keys
return;
if (getters.currentPromptName() != null) return;
if (!ctrlKey && !metaKey) return;
if (key.toLowerCase() === "s") {
event.preventDefault();
this.handleEditorValueRequest();
}
},
},
+177
View File
@@ -0,0 +1,177 @@
<template>
<div class="epub-container">
<div v-if="!isReady" class="loading-indicator">
<p>{{ $t("files.loading") }}</p>
</div>
<div id="viewer" :class="{ ready: isReady }"></div>
<div v-if="isReady" class="navigation">
<button @click="prevPage" class="nav-button">&lt;</button>
<button @click="nextPage" class="nav-button">&gt;</button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import ePub, { type Book, type Rendition } from "epubjs";
import { state,mutations } from "@/store"; // Assuming your store setup
import { filesApi } from "@/api"; // Assuming your api setup
import { removeLastDir } from "@/utils/url"; // Assuming your utils setup
export default defineComponent({
name: "epubViewer",
data() {
return {
isReady: false, // Flag to indicate when the book is loaded
floatIn: false, // Flag for the float-in animation
book: null as Book | null,
rendition: null as Rendition | null,
};
},
async mounted() {
mutations.resetSelected();
mutations.addSelected({
name: state.req.name,
path: state.req.path,
size: state.req.size,
type: state.req.type,
source: state.req.source,
url: state.req.url,
});
try {
// 1. Fetch the download URL for the EPUB file
const epubUrl = await filesApi.getDownloadURL(
state.req.source,
state.req.path,
false,
false
);
// 2. Initialize the EPUB book
this.book = ePub(epubUrl);
// 3. Render the book to the "viewer" div
this.rendition = this.book.renderTo("viewer", {
width: "100%",
height: "100%",
spread: "auto", // Automatically handle single or double page spreads
flow: "paginated", // Standard book-like pagination
});
// 4. Display the rendered book
await this.rendition.display();
// Set flags to show the book and trigger animations
this.isReady = true;
setTimeout(() => {
this.floatIn = true;
}, 100); // slight delay to allow rendering
} catch (error) {
this.onLoadComponentError(error);
}
},
beforeUnmount() {
// Clean up and destroy the epub instance to free up memory
if (this.book) {
this.book.destroy();
}
},
methods: {
// Navigate to the next page
nextPage() {
this.rendition?.next();
},
// Navigate to the previous page
prevPage() {
this.rendition?.prev();
},
// Close the viewer and navigate away
close() {
const current = window.location.pathname;
const newPath = removeLastDir(current);
window.location.href = newPath + "#" + state.req.name;
},
// Error handler
onLoadComponentError(error: any) {
console.error("Error loading EPUB file:", error);
// You could add logic here to display an error message to the user
},
},
});
</script>
<style scoped>
.epub-container {
width: 100%;
height: 100%;
background-color: var(--surfaceSecondary); /* A light background for the reader */
z-index: 1000;
}
.loading-indicator {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-size: 1.2em;
color: #6c757d;
}
/* The viewer must have a defined height for epub.js to work */
#viewer {
width: 100%;
height: 100%;
visibility: hidden; /* Hide until ready to prevent flicker */
}
#viewer.ready {
visibility: visible;
}
.navigation {
position: fixed;
bottom: 1.5em;
left: 50%;
transform: translateX(-50%);
z-index: 1001; /* Ensure controls are on top */
display: flex;
gap: 1em;
background-color: rgba(255, 255, 255, 0.8);
padding: 0.5em;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.nav-button {
background-color: transparent;
border: none;
font-size: 1.5em;
color: #343a40;
cursor: pointer;
padding: 0.25em 1em;
}
/* Copied directly from your example */
.floating-close {
position: fixed;
left: 50%;
transform: translate(-50%, -5em); /* Start offscreen */
transition: transform 0.4s ease;
background: var(--surfaceSecondary);
font-size: 0.5em;
top: 0;
z-index: 1002;
}
.float-in {
transform: translate(-50%, 2.75em); /* Animate to final position */
}
.floating-close i {
font-size: 2em;
padding-right: 1em;
padding-left: 1em;
}
</style>
+10 -1
View File
@@ -5,7 +5,7 @@
<script lang="ts">
import { marked } from "marked";
import DOMPurify from 'dompurify';
import { state } from "@/store";
import { state, mutations } from "@/store";
import hljs from 'highlight.js';
// --- We have removed all `marked.use()` configuration ---
@@ -73,6 +73,15 @@ export default {
},
},
mounted() {
mutations.resetSelected();
mutations.addSelected({
name: state.req.name,
path: state.req.path,
size: state.req.size,
type: state.req.type,
source: state.req.source,
url: state.req.url,
});
this.setHighlightTheme(state.user.darkMode);
// Set initial content. The `watch` will trigger the first highlight.
const fileContent = state.req.content == "empty-file-x6OlSil" ? "" : state.req.content || "";
+25 -1
View File
@@ -1,7 +1,8 @@
<template>
<div id="previewer" @mousemove="toggleNavigation" @touchstart="toggleNavigation">
<div class="preview">
<ExtendedImage v-if="previewType == 'image'" :src="raw"> </ExtendedImage>
<ExtendedImage v-if="previewType == 'image' || pdfConvertable" :src="raw">
</ExtendedImage>
<audio
v-else-if="previewType == 'audio'"
ref="player"
@@ -111,6 +112,23 @@ export default {
};
},
computed: {
pdfConvertable() {
const ext = "." + state.req.name.split(".").pop().toLowerCase(); // Ensure lowercase and dot
const pdfConvertCompatibleFileExtensions = {
".xps": true,
".mobi": true,
".fb2": true,
".cbz": true,
".svg": true,
".docx": true,
".ppt": true,
".pptx": true,
".xlsx": true,
".hwp": true,
".hwpx": true,
};
return !!pdfConvertCompatibleFileExtensions[ext];
},
sidebarShowing() {
return getters.isSidebarVisible();
},
@@ -118,6 +136,12 @@ export default {
return getters.previewType();
},
raw() {
if (this.pdfConvertable) {
return (
filesApi.getPreviewURL(state.req.source, state.req.path, state.req.modified) +
"&size=original"
);
}
return filesApi.getDownloadURL(state.req.source, state.req.path, true);
},
isDarkMode() {
+39 -1
View File
@@ -78,6 +78,24 @@
</div>
</div>
<div v-if="muPdfAvailable">
<h3> {{ $t('settings.disableOfficePreviews')}} </h3>
<p>
{{ $t('settings.disableOfficePreviewsDescription') }}
</p>
<div class="form-group">
<input
class="input input--block form-form flat-right"
:class="{ 'invalid-form': !formValidationOfficePreviews() }"
type="text"
placeholder="enter file extensions"
id="officePreviewExt"
v-model="formOfficePreviewExt"
/>
<button type="button" class="button form-button" @click="submitOfficePreviewsChange"> {{ $t('buttons.save') }} </button>
</div>
</div>
<h3> {{ $t('settings.themeColor') }} </h3>
<ButtonGroup
:buttons="colorChoices"
@@ -98,7 +116,7 @@
<script>
import { notify } from "@/notify";
import { onlyOfficeUrl, mediaAvailable } from "@/utils/constants.js";
import { onlyOfficeUrl, mediaAvailable, muPdfAvailable } from "@/utils/constants.js";
import { state, mutations } from "@/store";
import { usersApi } from "@/api";
import Languages from "@/components/settings/Languages.vue";
@@ -117,6 +135,7 @@ export default {
localuser: { preview: {} },
initialized: false,
formOnlyOfficeExt: "", // holds temporary input before saving
formOfficePreviewExt: "", // holds temporary input before saving
colorChoices: [
{ label: "blue", value: "var(--blue)" },
{ label: "red", value: "var(--red)" },
@@ -139,6 +158,9 @@ export default {
},
},
computed: {
muPdfAvailable() {
return muPdfAvailable;
},
hasOnlyOfficeEnabled() {
return onlyOfficeUrl != "";
},
@@ -155,6 +177,7 @@ export default {
mounted() {
this.localuser = { ...state.user };
this.formOnlyOfficeExt = this.localuser.disableOnlyOfficeExt;
this.formOfficePreviewExt = this.localuser.disableOfficePreviewExt;
},
methods: {
formValidation() {
@@ -171,6 +194,20 @@ export default {
}
this.localuser.disableOnlyOfficeExt = this.formOnlyOfficeExt;
},
formValidationOfficePreviews() {
if (this.formOfficePreviewExt == "") {
return true;
}
let regex = /^\.\w+(?: \.\w+)*$/;
return regex.test(this.formOfficePreviewExt);
},
submitOfficePreviewsChange() {
if (!this.formValidationOfficePreviews()) {
notify.showError("Invalid input, does not match requirement.");
return;
}
this.localuser.disableOfficePreviewExt = this.formOfficePreviewExt;
},
setColor(string) {
this.localuser.themeColor = string;
},
@@ -194,6 +231,7 @@ export default {
"themeColor",
"quickDownload",
"disableOnlyOfficeExt",
"disableOfficePreviewExt",
"preview",
]);
notify.showSuccess(this.$t("settings.settingsUpdated"));
+1
View File
@@ -61,6 +61,7 @@ export default {
username: "",
password: "",
permissions: { admin: false },
otpEnabled: false,
},
showDelete: false,
createUserDir: false,
@@ -71,7 +71,7 @@ test("2x copy from listing to new folder", async({ page, checkForErrors, context
await page.locator('.selected-count-header').waitFor({ state: 'visible' });
await expect(page.locator('.selected-count-header')).toHaveText('1 selected');
await page.locator('button[aria-label="Copy file"]').click();
await expect(page.locator('div[aria-label="filelist-path"]')).toHaveText('Path: /playwright-files/');
await expect(page.locator('div[aria-label="filelist-path"]')).toHaveText('Path: /');
await expect(page.locator('li[aria-selected="true"]')).toHaveCount(0);
await page.locator('li[aria-label="myfolder"]').click();
await expect(page.locator('li[aria-selected="true"]')).toHaveCount(1);
@@ -101,7 +101,7 @@ test("2x copy from listing to new folder", async({ page, checkForErrors, context
await page.locator('.selected-count-header').waitFor({ state: 'visible' });
await expect(page.locator('.selected-count-header')).toHaveText('1 selected');
await page.locator('button[aria-label="Copy file"]').click();
await expect(page.locator('div[aria-label="filelist-path"]')).toHaveText('Path: /playwright-files/myfolder/');
await expect(page.locator('div[aria-label="filelist-path"]')).toHaveText('Path: /myfolder/');
await page.locator('li[aria-label="newfolder"]').click();
await page.locator('button[aria-label="Copy"]').click();
const popup2 = page.locator('#popup-notification-content');
+1 -1
View File
@@ -28,7 +28,7 @@ run: build-frontend
else \
sed -i '/func init/,+3d' ./swagger/docs/docs.go; \
fi && \
FILEBROWSER_NO_EMBEDED=true go run -tags "pdf" \
FILEBROWSER_NO_EMBEDED=true CGO_ENABLED=1 go run -tags mupdf \
--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: