Beta/v0.5.3 (#374)

This commit is contained in:
Graham Steffaniak 2025-02-16 09:07:38 -05:00 committed by GitHub
parent c84a3b0d41
commit c4ccfd48f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 1461 additions and 509 deletions

View File

@ -24,7 +24,7 @@ jobs:
go-version: 'stable'
- uses: golangci/golangci-lint-action@v5
with:
version: v1.60
version: 'v1.64'
working-directory: backend
format-backend:
runs-on: ubuntu-latest

View File

@ -56,7 +56,7 @@ jobs:
- name: Create Release
uses: softprops/action-gh-release@v2
with:
target_commitish: ${{ steps.extract_branch.outputs.branch_name }}
target_commitish: ${{ github.sha }}
token: ${{ secrets.PAT }}
tag_name: ${{ steps.extract_branch.outputs.tag_name }}
prerelease: false # change this to false when stable gets released

View File

@ -56,7 +56,7 @@ jobs:
- name: Create Release
uses: softprops/action-gh-release@v2
with:
target_commitish: ${{ steps.extract_branch.outputs.branch_name }}
target_commitish: ${{ github.sha }}
token: ${{ secrets.PAT }}
tag_name: ${{ steps.extract_branch.outputs.tag_name }}
prerelease: false

View File

@ -2,6 +2,27 @@
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.5.3-beta
**New Features**
- onlyoffice disable filetypes for user specified file types. https://github.com/gtsteffaniak/filebrowser/issues/346
**Notes**:
- navbar/sidebar lightmode style tweaks.
- any item that has utf formatted text will get editor.
- tweaks to create options on context menu.
- removed small delay on preview before detecting the file.
**BugFixes**:
- fix `/files/` prefix loading issue https://github.com/gtsteffaniak/filebrowser/issues/362
- fix special characters in filename issue https://github.com/gtsteffaniak/filebrowser/issues/357
- fix drag and drop issue https://github.com/gtsteffaniak/filebrowser/issues/361
- fix conflict issue with creating same file after deletion.
- fix mimetype detection https://github.com/gtsteffaniak/filebrowser/issues/327
- subtitles for videos https://github.com/gtsteffaniak/filebrowser/issues/358
- supports caption sidecar files : ".vtt", ".srt", ".lrc", ".sbv", ".ass", ".ssa", ".sub", ".smi"
- embedded subtitles not yet supported.
## v0.5.2-beta
**New Features**:

View File

@ -6,6 +6,9 @@
[![DockerHub Pulls](https://img.shields.io/docker/pulls/gtstef/filebrowser?label=latest%20Docker%20pulls)](https://hub.docker.com/r/gtstef/filebrowser)
[![Apache-2.0 License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
[![Poll](https://img.shields.io/badge/poll-vote_most_important_features-purple)](https://github.com/gtsteffaniak/filebrowser/discussions/368)
[![Donate with PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/donate/?business=W5XKNXHJM2WPE&no_recurring=0&currency_code=USD)
<img width="150" src="https://github.com/user-attachments/assets/59986a2a-f960-4536-aa35-4a9a7c98ad48" title="Logo">
<h3>FileBrowser Quantum</h3>
A modern web-based file manager

View File

@ -1,4 +1,4 @@
FROM golang:1.23-alpine AS base
FROM golang:1.24-alpine AS base
ARG VERSION
ARG REVISION
WORKDIR /app

View File

@ -24,6 +24,7 @@ import (
"github.com/gtsteffaniak/filebrowser/backend/cache"
"github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/backend/fileutils"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/backend/utils"
@ -54,7 +55,7 @@ type FileInfo struct {
// for efficiency, a response will be a pointer to the data
// extra calculated fields can be added here
type ExtendedFileInfo struct {
*FileInfo
FileInfo
Content string `json:"content,omitempty"` // text content of a file, if requested
Subtitles []string `json:"subtitles,omitempty"` // subtitles for video files
Checksums map[string]string `json:"checksums,omitempty"` // checksums for the file
@ -129,7 +130,7 @@ func FileInfoFaster(opts FileOptions) (ExtendedFileInfo, error) {
}
info, exists := index.GetReducedMetadata(opts.Path, opts.IsDir)
if !exists {
return response, err
return response, fmt.Errorf("could not get metadata for path: %v", opts.Path)
}
if opts.Content {
content, err := getContent("default", opts.Path)
@ -138,11 +139,15 @@ func FileInfoFaster(opts FileOptions) (ExtendedFileInfo, error) {
}
response.Content = content
}
response.FileInfo = info
response.FileInfo = *info
response.RealPath = realPath
response.Source = opts.Source
if settings.Config.Integrations.OnlyOffice.Secret != "" && info.Type != "directory" && isOnlyOffice(info.Name) {
response.OnlyOfficeId = generateOfficeId(realPath)
}
if strings.HasPrefix(info.Type, "video") {
response.detectSubtitles(realPath)
}
return response, nil
}
@ -365,42 +370,32 @@ func (i *ItemInfo) DetectType(realPath string, saveContent bool) {
// TODO add subtitles back
// detectSubtitles detects subtitles for video files.
//func (i *FileInfo) detectSubtitles(path string) {
// if i.Type != "video" {
// return
// }
// parentDir := filepath.Dir(path)
// fileName := filepath.Base(path)
// i.Subtitles = []string{}
// ext := filepath.Ext(fileName)
// dir, err := os.Open(parentDir)
// if err != nil {
// // Directory must have been deleted, remove it from the index
// return
// }
// defer dir.Close() // Ensure directory handle is closed
//
// files, err := dir.Readdir(-1)
// if err != nil {
// return
// }
//
// base := strings.TrimSuffix(fileName, ext)
// subtitleExts := []string{".vtt", ".txt", ".srt", ".lrc"}
//
// for _, f := range files {
// if f.IsDir() || !strings.HasPrefix(f.Name(), base) {
// continue
// }
//
// for _, subtitleExt := range subtitleExts {
// if strings.HasSuffix(f.Name(), subtitleExt) {
// i.Subtitles = append(i.Subtitles, filepath.Join(parentDir, f.Name()))
// break
// }
// }
// }
//}
func (i *ExtendedFileInfo) detectSubtitles(path string) {
if !strings.HasPrefix(i.Type, "video") {
logger.Debug("subtitles are not supported for this file : " + path)
return
}
idx := GetIndex(i.Source)
parentInfo, exists := idx.GetReducedMetadata(filepath.Dir(i.Path), true)
if !exists {
return
}
base := strings.Split(i.Name, ".")[0]
for _, f := range parentInfo.Files {
baseName := strings.Split(f.Name, ".")[0]
if baseName != base {
continue
}
for _, subtitleExt := range []string{".vtt", ".srt", ".lrc", ".sbv", ".ass", ".ssa", ".sub", ".smi"} {
if strings.HasSuffix(f.Name, subtitleExt) {
fullPathBase := strings.Split(i.Path, ".")[0]
i.Subtitles = append(i.Subtitles, fullPathBase+subtitleExt)
}
}
}
}
func IsNamedPipe(mode os.FileMode) bool {
return mode&os.ModeNamedPipe != 0

View File

@ -54,11 +54,11 @@ func Initialize(source settings.Source) {
if !newIndex.Source.Config.Disabled {
time.Sleep(time.Second)
logger.Info("Initializing index and assessing file system complexity")
logger.Info(fmt.Sprintf("initializing index: [%v]", newIndex.Source.Name))
newIndex.RunIndexing("/", false)
go newIndex.setupIndexingScanners()
} else {
logger.Debug("Indexing disabled for source: " + newIndex.Source.Name)
logger.Debug("indexing disabled for source: " + newIndex.Source.Name)
}
}
@ -122,7 +122,7 @@ func (idx *Index) indexDirectory(adjustedPath string, quick, recursive bool) err
// Process each file and directory in the current directory
for _, file := range files {
isHidden := isHidden(file, idx.Path+combinedPath)
isHidden := isHidden(file, idx.Source.Path+combinedPath)
isDir := file.IsDir()
fullCombined := combinedPath + file.Name()
if idx.shouldSkip(isDir, isHidden, fullCombined) {

View File

@ -76,9 +76,9 @@ func (idx *Index) RunIndexing(origin string, quick bool) {
prevNumDirs := idx.NumDirs
prevNumFiles := idx.NumFiles
if quick {
logger.Debug("Starting quick scan")
logger.Debug(fmt.Sprintf("Starting quick scan for [%v]", idx.Source.Name))
} else {
logger.Debug("Starting full scan")
logger.Debug(fmt.Sprintf("Starting full scan for [%v]", idx.Source.Name))
idx.NumDirs = 0
idx.NumFiles = 0
}
@ -106,18 +106,18 @@ func (idx *Index) RunIndexing(origin string, quick bool) {
idx.assessment = "normal"
}
if firstRun {
logger.Info(fmt.Sprintf("Index assessment : complexity=%v directories=%v files=%v", idx.assessment, idx.NumDirs, idx.NumFiles))
logger.Info(fmt.Sprintf("Index assessment : index=%v complexity=%v directories=%v files=%v", idx.Source.Name, idx.assessment, idx.NumDirs, idx.NumFiles))
} else {
logger.Debug(fmt.Sprintf("Index assessment : complexity=%v directories=%v files=%v", idx.assessment, idx.NumDirs, idx.NumFiles))
logger.Debug(fmt.Sprintf("Index assessment : iindex=%v complexity=%v directories=%v files=%v", idx.Source.Name, idx.assessment, idx.NumDirs, idx.NumFiles))
}
if idx.NumDirs != prevNumDirs || idx.NumFiles != prevNumFiles {
idx.FilesChangedDuringIndexing = true
}
}
if firstRun {
logger.Info(fmt.Sprintf("Time spent indexing : %v seconds", idx.indexingTime))
logger.Info(fmt.Sprintf("Time spent indexing [%v]: %v seconds", idx.Source.Name, idx.indexingTime))
} else {
logger.Debug(fmt.Sprintf("Time spent indexing : %v seconds", idx.indexingTime))
logger.Debug(fmt.Sprintf("Time spent indexing [%v]: %v seconds", idx.Source.Name, idx.indexingTime))
}
}

612
backend/files/mime.go Normal file
View File

@ -0,0 +1,612 @@
package files
// This file contains code primarily sourced from::
// github.com/kataras/iris
import (
"mime"
)
const (
// ContentBinaryHeaderValue header value for binary data.
ContentBinaryHeaderValue = "application/octet-stream"
// ContentWebassemblyHeaderValue header value for web assembly files.
ContentWebassemblyHeaderValue = "application/wasm"
// ContentHTMLHeaderValue is the string of text/html response header's content type value.
ContentHTMLHeaderValue = "text/html"
// ContentJSONHeaderValue header value for JSON data.
ContentJSONHeaderValue = "application/json"
// ContentJSONProblemHeaderValue header value for JSON API problem error.
// Read more at: https://tools.ietf.org/html/rfc7807
ContentJSONProblemHeaderValue = "application/problem+json"
// ContentXMLProblemHeaderValue header value for XML API problem error.
// Read more at: https://tools.ietf.org/html/rfc7807
ContentXMLProblemHeaderValue = "application/problem+xml"
// ContentJavascriptHeaderValue header value for JSONP & Javascript data.
ContentJavascriptHeaderValue = "text/javascript"
// ContentTextHeaderValue header value for Text data.
ContentTextHeaderValue = "text/plain"
// ContentXMLHeaderValue header value for XML data.
ContentXMLHeaderValue = "text/xml"
// ContentXMLUnreadableHeaderValue obsolete header value for XML.
ContentXMLUnreadableHeaderValue = "application/xml"
// ContentMarkdownHeaderValue custom key/content type, the real is the text/html.
ContentMarkdownHeaderValue = "text/markdown"
// ContentYAMLHeaderValue header value for YAML data.
ContentYAMLHeaderValue = "application/x-yaml"
// ContentYAMLTextHeaderValue header value for YAML plain text.
ContentYAMLTextHeaderValue = "text/yaml"
// ContentProtobufHeaderValue header value for Protobuf messages data.
ContentProtobufHeaderValue = "application/x-protobuf"
// ContentMsgPackHeaderValue header value for MsgPack data.
ContentMsgPackHeaderValue = "application/msgpack"
// ContentMsgPack2HeaderValue alternative header value for MsgPack data.
ContentMsgPack2HeaderValue = "application/x-msgpack"
// ContentFormHeaderValue header value for post form data.
ContentFormHeaderValue = "application/x-www-form-urlencoded"
// ContentFormMultipartHeaderValue header value for post multipart form data.
ContentFormMultipartHeaderValue = "multipart/form-data"
// ContentMultipartRelatedHeaderValue header value for multipart related data.
ContentMultipartRelatedHeaderValue = "multipart/related"
// ContentGRPCHeaderValue Content-Type header value for gRPC.
ContentGRPCHeaderValue = "application/grpc"
)
var types = map[string]string{
".3dm": "x-world/x-3dmf",
".3dmf": "x-world/x-3dmf",
".7z": "application/x-7z-compressed",
".a": "application/octet-stream",
".aab": "application/x-authorware-bin",
".aam": "application/x-authorware-map",
".aas": "application/x-authorware-seg",
".abc": "text/vndabc",
".ace": "application/x-ace-compressed",
".acgi": "text/html",
".afl": "video/animaflex",
".ai": "application/postscript",
".aif": "audio/aiff",
".aifc": "audio/aiff",
".aiff": "audio/aiff",
".aim": "application/x-aim",
".aip": "text/x-audiosoft-intra",
".alz": "application/x-alz-compressed",
".ani": "application/x-navi-animation",
".aos": "application/x-nokia-9000-communicator-add-on-software",
".aps": "application/mime",
".apk": "application/vnd.android.package-archive",
".arc": "application/x-arc-compressed",
".arj": "application/arj",
".art": "image/x-jg",
".asf": "video/x-ms-asf",
".asm": "text/x-asm",
".asp": "text/asp",
".asx": "application/x-mplayer2",
".au": "audio/basic",
".avi": "video/x-msvideo",
".avs": "video/avs-video",
".bcpio": "application/x-bcpio",
".bin": "application/mac-binary",
".bmp": "image/bmp",
".boo": "application/book",
".book": "application/book",
".boz": "application/x-bzip2",
".bsh": "application/x-bsh",
".bz2": "application/x-bzip2",
".bz": "application/x-bzip",
".c++": ContentTextHeaderValue,
".c": "text/x-c",
".cab": "application/vnd.ms-cab-compressed",
".cat": "application/vndms-pkiseccat",
".cc": "text/x-c",
".ccad": "application/clariscad",
".cco": "application/x-cocoa",
".cdf": "application/cdf",
".cer": "application/pkix-cert",
".cha": "application/x-chat",
".chat": "application/x-chat",
".chrt": "application/vnd.kde.kchart",
".class": "application/java",
".com": ContentTextHeaderValue,
".conf": ContentTextHeaderValue,
".cpio": "application/x-cpio",
".cpp": "text/x-c",
".cpt": "application/mac-compactpro",
".crl": "application/pkcs-crl",
".crt": "application/pkix-cert",
".crx": "application/x-chrome-extension",
".csh": "text/x-scriptcsh",
".css": "text/css",
".csv": "text/csv",
".cxx": ContentTextHeaderValue,
".dar": "application/x-dar",
".dcr": "application/x-director",
".deb": "application/x-debian-package",
".deepv": "application/x-deepv",
".def": ContentTextHeaderValue,
".der": "application/x-x509-ca-cert",
".dif": "video/x-dv",
".dir": "application/x-director",
".divx": "video/divx",
".dl": "video/dl",
".dmg": "application/x-apple-diskimage",
".doc": "application/msword",
".dot": "application/msword",
".dp": "application/commonground",
".drw": "application/drafting",
".dump": "application/octet-stream",
".dv": "video/x-dv",
".dvi": "application/x-dvi",
".dwf": "drawing/x-dwf=(old)",
".dwg": "application/acad",
".dxf": "application/dxf",
".dxr": "application/x-director",
".el": "text/x-scriptelisp",
".elc": "application/x-bytecodeelisp=(compiled=elisp)",
".eml": "message/rfc822",
".env": "application/x-envoy",
".eps": "application/postscript",
".es": "application/x-esrehber",
".etx": "text/x-setext",
".evy": "application/envoy",
".exe": "application/octet-stream",
".f77": "text/x-fortran",
".f90": "text/x-fortran",
".f": "text/x-fortran",
".fdf": "application/vndfdf",
".fif": "application/fractals",
".fli": "video/fli",
".flo": "image/florian",
".flv": "video/x-flv",
".flx": "text/vndfmiflexstor",
".fmf": "video/x-atomic3d-feature",
".for": "text/x-fortran",
".fpx": "image/vndfpx",
".frl": "application/freeloader",
".funk": "audio/make",
".g3": "image/g3fax",
".g": ContentTextHeaderValue,
".gif": "image/gif",
".gl": "video/gl",
".gsd": "audio/x-gsm",
".gsm": "audio/x-gsm",
".gsp": "application/x-gsp",
".gss": "application/x-gss",
".gtar": "application/x-gtar",
".gz": "application/x-compressed",
".gzip": "application/x-gzip",
".h": "text/x-h",
".hdf": "application/x-hdf",
".help": "application/x-helpfile",
".hgl": "application/vndhp-hpgl",
".hh": "text/x-h",
".hlb": "text/x-script",
".hlp": "application/hlp",
".hpg": "application/vndhp-hpgl",
".hpgl": "application/vndhp-hpgl",
".hqx": "application/binhex",
".hta": "application/hta",
".htc": "text/x-component",
".htm": "text/html",
".html": "text/html",
".htmls": "text/html",
".htt": "text/webviewhtml",
".htx": "text/html",
".ice": "x-conference/x-cooltalk",
".ico": "image/x-icon",
".ics": "text/calendar",
".icz": "text/calendar",
".idc": ContentTextHeaderValue,
".ief": "image/ief",
".iefs": "image/ief",
".iges": "application/iges",
".igs": "application/iges",
".ima": "application/x-ima",
".imap": "application/x-httpd-imap",
".inf": "application/inf",
".ins": "application/x-internett-signup",
".ip": "application/x-ip2",
".isu": "video/x-isvideo",
".it": "audio/it",
".iv": "application/x-inventor",
".ivr": "i-world/i-vrml",
".ivy": "application/x-livescreen",
".jam": "audio/x-jam",
".jav": "text/x-java-source",
".java": "text/x-java-source",
".jcm": "application/x-java-commerce",
".jfif-tbnl": "image/jpeg",
".jfif": "image/jpeg",
".jnlp": "application/x-java-jnlp-file",
".jpe": "image/jpeg",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".jps": "image/x-jps",
".js": ContentJavascriptHeaderValue,
".mjs": ContentJavascriptHeaderValue,
".json": ContentJSONHeaderValue,
".vue": ContentJavascriptHeaderValue,
".jut": "image/jutvision",
".kar": "audio/midi",
".karbon": "application/vnd.kde.karbon",
".kfo": "application/vnd.kde.kformula",
".flw": "application/vnd.kde.kivio",
".kml": "application/vnd.google-earth.kml+xml",
".kmz": "application/vnd.google-earth.kmz",
".kon": "application/vnd.kde.kontour",
".kpr": "application/vnd.kde.kpresenter",
".kpt": "application/vnd.kde.kpresenter",
".ksp": "application/vnd.kde.kspread",
".kwd": "application/vnd.kde.kword",
".kwt": "application/vnd.kde.kword",
".ksh": "text/x-scriptksh",
".la": "audio/nspaudio",
".lam": "audio/x-liveaudio",
".latex": "application/x-latex",
".lha": "application/lha",
".lhx": "application/octet-stream",
".list": ContentTextHeaderValue,
".lma": "audio/nspaudio",
".log": ContentTextHeaderValue,
".lsp": "text/x-scriptlisp",
".lst": ContentTextHeaderValue,
".lsx": "text/x-la-asf",
".ltx": "application/x-latex",
".lzh": "application/octet-stream",
".lzx": "application/lzx",
".m1v": "video/mpeg",
".m2a": "audio/mpeg",
".m2v": "video/mpeg",
".m3u": "audio/x-mpegurl",
".m": "text/x-m",
".man": "application/x-troff-man",
".manifest": "text/cache-manifest",
".map": "application/x-navimap",
".mar": ContentTextHeaderValue,
".mbd": "application/mbedlet",
".mc$": "application/x-magic-cap-package-10",
".mcd": "application/mcad",
".mcf": "text/mcf",
".mcp": "application/netmc",
".me": "application/x-troff-me",
".mht": "message/rfc822",
".mhtml": "message/rfc822",
".mid": "application/x-midi",
".midi": "application/x-midi",
".mif": "application/x-frame",
".mime": "message/rfc822",
".mjf": "audio/x-vndaudioexplosionmjuicemediafile",
".mjpg": "video/x-motion-jpeg",
".mm": "application/base64",
".mme": "application/base64",
".mod": "audio/mod",
".moov": "video/quicktime",
".mov": "video/quicktime",
".movie": "video/x-sgi-movie",
".mp2": "audio/mpeg",
".mp3": "audio/mpeg",
".mp4": "video/mp4",
".mpa": "audio/mpeg",
".mpc": "application/x-project",
".mpe": "video/mpeg",
".mpeg": "video/mpeg",
".mpg": "video/mpeg",
".mpga": "audio/mpeg",
".mpp": "application/vndms-project",
".mpt": "application/x-project",
".mpv": "application/x-project",
".mpx": "application/x-project",
".mrc": "application/marc",
".ms": "application/x-troff-ms",
".mv": "video/x-sgi-movie",
".my": "audio/make",
".mzz": "application/x-vndaudioexplosionmzz",
".nap": "image/naplps",
".naplps": "image/naplps",
".nc": "application/x-netcdf",
".ncm": "application/vndnokiaconfiguration-message",
".nif": "image/x-niff",
".niff": "image/x-niff",
".nix": "application/x-mix-transfer",
".nsc": "application/x-conference",
".nvd": "application/x-navidoc",
".o": "application/octet-stream",
".oda": "application/oda",
".odb": "application/vnd.oasis.opendocument.database",
".odc": "application/vnd.oasis.opendocument.chart",
".odf": "application/vnd.oasis.opendocument.formula",
".odg": "application/vnd.oasis.opendocument.graphics",
".odi": "application/vnd.oasis.opendocument.image",
".odm": "application/vnd.oasis.opendocument.text-master",
".odp": "application/vnd.oasis.opendocument.presentation",
".ods": "application/vnd.oasis.opendocument.spreadsheet",
".odt": "application/vnd.oasis.opendocument.text",
".oga": "audio/ogg",
".ogg": "audio/ogg",
".ogv": "video/ogg",
".omc": "application/x-omc",
".omcd": "application/x-omcdatamaker",
".omcr": "application/x-omcregerator",
".otc": "application/vnd.oasis.opendocument.chart-template",
".otf": "application/vnd.oasis.opendocument.formula-template",
".otg": "application/vnd.oasis.opendocument.graphics-template",
".oth": "application/vnd.oasis.opendocument.text-web",
".oti": "application/vnd.oasis.opendocument.image-template",
".otm": "application/vnd.oasis.opendocument.text-master",
".otp": "application/vnd.oasis.opendocument.presentation-template",
".ots": "application/vnd.oasis.opendocument.spreadsheet-template",
".ott": "application/vnd.oasis.opendocument.text-template",
".p10": "application/pkcs10",
".p12": "application/pkcs-12",
".p7a": "application/x-pkcs7-signature",
".p7c": "application/pkcs7-mime",
".p7m": "application/pkcs7-mime",
".p7r": "application/x-pkcs7-certreqresp",
".p7s": "application/pkcs7-signature",
".p": "text/x-pascal",
".part": "application/pro_eng",
".pas": "text/pascal",
".pbm": "image/x-portable-bitmap",
".pcl": "application/vndhp-pcl",
".pct": "image/x-pict",
".pcx": "image/x-pcx",
".pdb": "chemical/x-pdb",
".pdf": "application/pdf",
".pfunk": "audio/make",
".pgm": "image/x-portable-graymap",
".pic": "image/pict",
".pict": "image/pict",
".pkg": "application/x-newton-compatible-pkg",
".pko": "application/vndms-pkipko",
".pl": "text/x-scriptperl",
".plx": "application/x-pixclscript",
".pm4": "application/x-pagemaker",
".pm5": "application/x-pagemaker",
".pm": "text/x-scriptperl-module",
".png": "image/png",
".pnm": "application/x-portable-anymap",
".pot": "application/mspowerpoint",
".pov": "model/x-pov",
".ppa": "application/vndms-powerpoint",
".ppm": "image/x-portable-pixmap",
".pps": "application/mspowerpoint",
".ppt": "application/mspowerpoint",
".ppz": "application/mspowerpoint",
".pre": "application/x-freelance",
".prt": "application/pro_eng",
".ps": "application/postscript",
".psd": "application/octet-stream",
".pvu": "paleovu/x-pv",
".pwz": "application/vndms-powerpoint",
".py": "text/x-scriptphyton",
".pyc": "application/x-bytecodepython",
".qcp": "audio/vndqcelp",
".qd3": "x-world/x-3dmf",
".qd3d": "x-world/x-3dmf",
".qif": "image/x-quicktime",
".qt": "video/quicktime",
".qtc": "video/x-qtc",
".qti": "image/x-quicktime",
".qtif": "image/x-quicktime",
".ra": "audio/x-pn-realaudio",
".ram": "audio/x-pn-realaudio",
".rar": "application/x-rar-compressed",
".ras": "application/x-cmu-raster",
".rast": "image/cmu-raster",
".rexx": "text/x-scriptrexx",
".rf": "image/vndrn-realflash",
".rgb": "image/x-rgb",
".rm": "application/vndrn-realmedia",
".rmi": "audio/mid",
".rmm": "audio/x-pn-realaudio",
".rmp": "audio/x-pn-realaudio",
".rng": "application/ringing-tones",
".rnx": "application/vndrn-realplayer",
".roff": "application/x-troff",
".rp": "image/vndrn-realpix",
".rpm": "audio/x-pn-realaudio-plugin",
".rt": "text/vndrn-realtext",
".rtf": "text/richtext",
".rtx": "text/richtext",
".rv": "video/vndrn-realvideo",
".s": "text/x-asm",
".s3m": "audio/s3m",
".s7z": "application/x-7z-compressed",
".saveme": "application/octet-stream",
".sbk": "application/x-tbook",
".scm": "text/x-scriptscheme",
".sdml": ContentTextHeaderValue,
".sdp": "application/sdp",
".sdr": "application/sounder",
".sea": "application/sea",
".set": "application/set",
".sgm": "text/x-sgml",
".sgml": "text/x-sgml",
".sh": "text/x-scriptsh",
".shar": "application/x-bsh",
".shtml": "text/x-server-parsed-html",
".sid": "audio/x-psid",
".skd": "application/x-koan",
".skm": "application/x-koan",
".skp": "application/x-koan",
".skt": "application/x-koan",
".sit": "application/x-stuffit",
".sitx": "application/x-stuffitx",
".sl": "application/x-seelogo",
".smi": "application/smil",
".smil": "application/smil",
".snd": "audio/basic",
".sol": "application/solids",
".spc": "text/x-speech",
".spl": "application/futuresplash",
".spr": "application/x-sprite",
".sprite": "application/x-sprite",
".spx": "audio/ogg",
".src": "application/x-wais-source",
".srt": "text/plain",
".sbv": "text/plain",
".ssa": "text/plain",
".ssi": "text/x-server-parsed-html",
".ssm": "application/streamingmedia",
".sst": "application/vndms-pkicertstore",
".step": "application/step",
".stl": "application/sla",
".stp": "application/step",
".sv4cpio": "application/x-sv4cpio",
".sv4crc": "application/x-sv4crc",
".svf": "image/vnddwg",
".svg": "image/svg+xml",
".svr": "application/x-world",
".swf": "application/x-shockwave-flash",
".t": "application/x-troff",
".talk": "text/x-speech",
".tar": "application/x-tar",
".tbk": "application/toolbook",
".tcl": "text/x-scripttcl",
".tcsh": "text/x-scripttcsh",
".tex": "application/x-tex",
".texi": "application/x-texinfo",
".texinfo": "application/x-texinfo",
".text": ContentTextHeaderValue,
".tgz": "application/gnutar",
".tif": "image/tiff",
".tiff": "image/tiff",
".tr": "application/x-troff",
".tsi": "audio/tsp-audio",
".tsp": "application/dsptype",
".tsv": "text/tab-separated-values",
".turbot": "image/florian",
".txt": ContentTextHeaderValue,
".uil": "text/x-uil",
".uni": "text/uri-list",
".unis": "text/uri-list",
".unv": "application/i-deas",
".uri": "text/uri-list",
".uris": "text/uri-list",
".ustar": "application/x-ustar",
".uu": "text/x-uuencode",
".uue": "text/x-uuencode",
".vcd": "application/x-cdlink",
".vcf": "text/x-vcard",
".vcard": "text/x-vcard",
".vcs": "text/x-vcalendar",
".vda": "application/vda",
".vdo": "video/vdo",
".vew": "application/groupwise",
".viv": "video/vivo",
".vivo": "video/vivo",
".vmd": "application/vocaltec-media-desc",
".vmf": "application/vocaltec-media-file",
".voc": "audio/voc",
".vos": "video/vosaic",
".vox": "audio/voxware",
".vqe": "audio/x-twinvq-plugin",
".vqf": "audio/x-twinvq",
".vql": "audio/x-twinvq-plugin",
".vrml": "application/x-vrml",
".vrt": "x-world/x-vrt",
".vsd": "application/x-visio",
".vst": "application/x-visio",
".vsw": "application/x-visio",
".w60": "application/wordperfect60",
".w61": "application/wordperfect61",
".w6w": "application/msword",
".wav": "audio/wav",
".wb1": "application/x-qpro",
".wbmp": "image/vnd.wap.wbmp",
".web": "application/vndxara",
".wiz": "application/msword",
".wk1": "application/x-123",
".wmf": "windows/metafile",
".wml": "text/vnd.wap.wml",
".wmlc": "application/vnd.wap.wmlc",
".wmls": "text/vnd.wap.wmlscript",
".wmlsc": "application/vnd.wap.wmlscriptc",
".word": "application/msword",
".wp5": "application/wordperfect",
".wp6": "application/wordperfect",
".wp": "application/wordperfect",
".wpd": "application/wordperfect",
".wq1": "application/x-lotus",
".wri": "application/mswrite",
".wrl": "application/x-world",
".wrz": "model/vrml",
".wsc": "text/scriplet",
".wsrc": "application/x-wais-source",
".wtk": "application/x-wintalk",
".x-png": "image/png",
".xbm": "image/x-xbitmap",
".xdr": "video/x-amt-demorun",
".xgz": "xgl/drawing",
".xif": "image/vndxiff",
".xl": "application/excel",
".xla": "application/excel",
".xlb": "application/excel",
".xlc": "application/excel",
".xld": "application/excel",
".xlk": "application/excel",
".xll": "application/excel",
".xlm": "application/excel",
".xls": "application/excel",
".xlt": "application/excel",
".xlv": "application/excel",
".xlw": "application/excel",
".xm": "audio/xm",
".xml": ContentXMLHeaderValue,
".xmz": "xgl/movie",
".xpix": "application/x-vndls-xpix",
".xpm": "image/x-xpixmap",
".xsr": "video/x-amt-showrun",
".xwd": "image/x-xwd",
".xyz": "chemical/x-pdb",
".z": "application/x-compress",
".zip": "application/zip",
".zoo": "application/octet-stream",
".zsh": "text/x-scriptzsh",
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".docm": "application/vnd.ms-word.document.macroEnabled.12",
".dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
".dotm": "application/vnd.ms-word.template.macroEnabled.12",
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12",
".xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
".xltm": "application/vnd.ms-excel.template.macroEnabled.12",
".xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
".xlam": "application/vnd.ms-excel.addin.macroEnabled.12",
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
".pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
".ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
".ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
".potx": "application/vnd.openxmlformats-officedocument.presentationml.template",
".potm": "application/vnd.ms-powerpoint.template.macroEnabled.12",
".ppam": "application/vnd.ms-powerpoint.addin.macroEnabled.12",
".sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide",
".sldm": "application/vnd.ms-powerpoint.slide.macroEnabled.12",
".thmx": "application/vnd.ms-officetheme",
".onetoc": "application/onenote",
".onetoc2": "application/onenote",
".onetmp": "application/onenote",
".onepkg": "application/onenote",
".xpi": "application/x-xpinstall",
".wasm": "application/wasm",
".m4a": "audio/mp4",
".flac": "audio/x-flac",
".amr": "audio/amr",
".aac": "audio/aac",
".opus": "video/ogg",
".m4v": "video/mp4",
".mkv": "video/x-matroska",
".caf": "audio/x-caf",
".m3u8": "application/x-mpegURL",
".mpd": "application/dash+xml",
".webp": "image/webp",
".epub": "application/epub+zip",
}
//nolint:gochecknoinits
func init() {
for ext, typ := range types {
// skip errors
_ = mime.AddExtensionType(ext, typ)
}
}

View File

@ -1,7 +1,6 @@
package fileutils
import (
"fmt"
"io"
"os"
"path"
@ -12,21 +11,15 @@ import (
// By default, the rename system call is used. If src and dst point to different volumes,
// the file copy is used as a fallback.
func MoveFile(src, dst string) error {
fmt.Println("moving", src, dst)
if os.Rename(src, dst) == nil {
return nil
}
fmt.Println("copyfile instead", src, dst)
// fallback
err := CopyFile(src, dst)
if err != nil {
fmt.Println("ok it errored too", err)
_ = os.Remove(dst)
return err
}
fmt.Println("removing", src)
if err := os.Remove(src); err != nil {
return err
}

View File

@ -9,7 +9,7 @@ require (
github.com/disintegration/imaging v1.6.2
github.com/dsoprea/go-exif/v3 v3.0.1
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568
github.com/goccy/go-yaml v1.15.17
github.com/goccy/go-yaml v1.15.23
github.com/golang-jwt/jwt/v4 v4.5.1
github.com/google/go-cmp v0.6.0
github.com/shirou/gopsutil/v3 v3.24.5
@ -43,8 +43,8 @@ require (
github.com/swaggo/files v1.0.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/tools v0.29.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/tools v0.30.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -46,8 +46,8 @@ github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9Z
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/goccy/go-yaml v1.15.17 h1:dK4FbbTTEOZTLH/NW3/xBqg0JdC14YKVmYwS9GT3H60=
github.com/goccy/go-yaml v1.15.17/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.15.23 h1:WS0GAX1uNPDLUvLkNU2vXq6oTnsmfVFocjQ/4qA48qo=
github.com/goccy/go-yaml v1.15.23/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
@ -112,8 +112,8 @@ golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+o
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -125,8 +125,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
@ -160,8 +160,8 @@ golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=

View File

@ -11,7 +11,7 @@ import (
"sync"
"time"
"github.com/golang-jwt/jwt/v4"
jwt "github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v4/request"
"golang.org/x/crypto/bcrypt"

View File

@ -9,7 +9,7 @@ import (
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
jwt "github.com/golang-jwt/jwt/v4"
"github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/runner"

View File

@ -7,7 +7,7 @@ import (
"testing"
"time"
"github.com/asdine/storm/v3"
storm "github.com/asdine/storm/v3"
"github.com/gtsteffaniak/filebrowser/backend/diskcache"
"github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/backend/img"
@ -50,7 +50,7 @@ func mockFileInfoFaster(t *testing.T) {
// Mock the function to skip execution
FileInfoFasterFunc = func(opts files.FileOptions) (files.ExtendedFileInfo, error) {
return files.ExtendedFileInfo{
FileInfo: &files.FileInfo{
FileInfo: files.FileInfo{
Path: opts.Path,
ItemInfo: files.ItemInfo{
Name: "mocked_file",

View File

@ -11,7 +11,7 @@ import (
"strconv"
"strings"
"github.com/golang-jwt/jwt/v4"
jwt "github.com/golang-jwt/jwt/v4"
"github.com/gtsteffaniak/filebrowser/backend/cache"
"github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/backend/settings"
@ -56,7 +56,7 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
path := pathParts[len(pathParts)-1]
urlFirst := pathParts[0]
if settings.Config.Server.InternalUrl != "" {
urlFirst = settings.Config.Server.InternalUrl
urlFirst = strings.TrimSuffix(settings.Config.Server.InternalUrl, "/")
replacement := strings.Split(url, "/api/raw")[0]
url = strings.Replace(url, replacement, settings.Config.Server.InternalUrl, 1)
}

View File

@ -153,7 +153,7 @@ func previewCacheKey(realPath, previewSize string, modTime time.Time) string {
return fmt.Sprintf("%x%x%x", realPath, modTime.Unix(), previewSize)
}
func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo) (int, error) {
func rawFileHandler(w http.ResponseWriter, r *http.Request, file files.FileInfo) (int, error) {
idx := files.GetIndex("default")
realPath, _, _ := idx.GetRealPath(file.Path)
fd, err := os.Open(realPath)

View File

@ -57,7 +57,7 @@ func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int,
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
}
fileList := strings.Split(files, ",")
fileList := strings.Split(files, ",|")
for i, f := range fileList {
fileList[i] = filepath.Join(filePrefix, f)
}
@ -196,6 +196,7 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
// Set headers and serve the file
setContentDisposition(w, r, fileName)
w.Header().Set("Cache-Control", "private")
w.Header().Set("X-Content-Type-Options", "nosniff")
// Serve the content
http.ServeContent(w, r, fileName, fileInfo.ModTime(), fd)

View File

@ -10,7 +10,7 @@ import (
"io"
"github.com/disintegration/imaging"
"github.com/dsoprea/go-exif/v3"
exif "github.com/dsoprea/go-exif/v3"
exifcommon "github.com/dsoprea/go-exif/v3/common"
)

View File

@ -19,7 +19,7 @@ import (
"runtime"
"unicode"
"github.com/flynn/go-shlex"
shlex "github.com/flynn/go-shlex"
)
const (

View File

@ -25,7 +25,7 @@ func (r *Runner) RunHook(fn func() error, evt, path, dst string, user *users.Use
dst, _, _ = idx.GetRealPath(user.Scope, dst)
if r.Enabled {
if val, ok := r.Commands["before_"+evt]; ok {
if val, ok := r.Settings.Commands["before_"+evt]; ok {
for _, command := range val {
err := r.exec(command, "before_"+evt, path, dst, user)
if err != nil {
@ -41,7 +41,7 @@ func (r *Runner) RunHook(fn func() error, evt, path, dst string, user *users.Use
}
if r.Enabled {
if val, ok := r.Commands["after_"+evt]; ok {
if val, ok := r.Settings.Commands["after_"+evt]; ok {
for _, command := range val {
err := r.exec(command, "after_"+evt, path, dst, user)
if err != nil {

View File

@ -7,7 +7,7 @@ import (
"path/filepath"
"strings"
"github.com/goccy/go-yaml"
yaml "github.com/goccy/go-yaml"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/backend/version"
@ -155,15 +155,16 @@ func setDefaults() Settings {
Name: "FileBrowser Quantum",
},
UserDefaults: UserDefaults{
StickySidebar: true,
Scope: ".",
LockPassword: false,
ShowHidden: false,
DarkMode: true,
DisableSettings: false,
ViewMode: "normal",
Locale: "en",
GallerySize: 3,
DisableOnlyOfficeExt: ".txt .csv .html",
StickySidebar: true,
Scope: ".",
LockPassword: false,
ShowHidden: false,
DarkMode: true,
DisableSettings: false,
ViewMode: "normal",
Locale: "en",
GallerySize: 3,
Permissions: users.Permissions{
Create: false,
Rename: false,

View File

@ -4,7 +4,7 @@ import (
"fmt"
"testing"
"github.com/goccy/go-yaml"
yaml "github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/gtsteffaniak/filebrowser/backend/logger"
)

View File

@ -156,11 +156,12 @@ type UserDefaults struct {
By string `json:"by"`
Asc bool `json:"asc"`
} `json:"sorting"`
Perm users.Permissions `json:"perm"`
Permissions users.Permissions `json:"permissions"`
Commands []string `json:"commands,omitempty"`
ShowHidden bool `json:"showHidden"`
DateFormat bool `json:"dateFormat"`
ThemeColor string `json:"themeColor"`
QuickDownload bool `json:"quickDownload"`
Perm users.Permissions `json:"perm"`
Permissions users.Permissions `json:"permissions"`
Commands []string `json:"commands,omitempty"`
ShowHidden bool `json:"showHidden"`
DateFormat bool `json:"dateFormat"`
ThemeColor string `json:"themeColor"`
QuickDownload bool `json:"quickDownload"`
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"`
}

View File

@ -1,7 +1,7 @@
package bolt
import (
"github.com/asdine/storm/v3"
storm "github.com/asdine/storm/v3"
"github.com/gtsteffaniak/filebrowser/backend/auth"
"github.com/gtsteffaniak/filebrowser/backend/errors"
)

View File

@ -1,7 +1,7 @@
package bolt
import (
"github.com/asdine/storm/v3"
storm "github.com/asdine/storm/v3"
"github.com/gtsteffaniak/filebrowser/backend/auth"
"github.com/gtsteffaniak/filebrowser/backend/settings"

View File

@ -1,7 +1,7 @@
package bolt
import (
"github.com/asdine/storm/v3"
storm "github.com/asdine/storm/v3"
"github.com/gtsteffaniak/filebrowser/backend/settings"
)

View File

@ -4,7 +4,7 @@ import (
"fmt"
"time"
"github.com/asdine/storm/v3"
storm "github.com/asdine/storm/v3"
"github.com/asdine/storm/v3/q"
"github.com/gtsteffaniak/filebrowser/backend/errors"

View File

@ -4,7 +4,7 @@ import (
"fmt"
"reflect"
"github.com/asdine/storm/v3"
storm "github.com/asdine/storm/v3"
"github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/backend/settings"

View File

@ -1,7 +1,7 @@
package bolt
import (
"github.com/asdine/storm/v3"
storm "github.com/asdine/storm/v3"
"github.com/gtsteffaniak/filebrowser/backend/errors"
)

View File

@ -5,7 +5,7 @@ import (
"os"
"path/filepath"
"github.com/asdine/storm/v3"
storm "github.com/asdine/storm/v3"
"github.com/gtsteffaniak/filebrowser/backend/auth"
"github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/backend/files"

View File

@ -1364,6 +1364,9 @@ const docTemplate = `{
"dateFormat": {
"type": "boolean"
},
"disableOnlyOfficeExt": {
"type": "string"
},
"disableSettings": {
"type": "boolean"
},
@ -1575,6 +1578,9 @@ const docTemplate = `{
"dateFormat": {
"type": "boolean"
},
"disableOnlyOfficeExt": {
"type": "string"
},
"disableSettings": {
"type": "boolean"
},

View File

@ -1353,6 +1353,9 @@
"dateFormat": {
"type": "boolean"
},
"disableOnlyOfficeExt": {
"type": "string"
},
"disableSettings": {
"type": "boolean"
},
@ -1564,6 +1567,9 @@
"dateFormat": {
"type": "boolean"
},
"disableOnlyOfficeExt": {
"type": "string"
},
"disableSettings": {
"type": "boolean"
},

View File

@ -132,6 +132,8 @@ definitions:
type: boolean
dateFormat:
type: boolean
disableOnlyOfficeExt:
type: string
disableSettings:
type: boolean
gallerySize:
@ -273,6 +275,8 @@ definitions:
type: boolean
dateFormat:
type: boolean
disableOnlyOfficeExt:
type: string
disableSettings:
type: boolean
gallerySize:

View File

@ -3,7 +3,7 @@ package users
import (
"regexp"
"github.com/golang-jwt/jwt/v4"
jwt "github.com/golang-jwt/jwt/v4"
)
type AuthToken struct {
@ -36,33 +36,34 @@ type Sorting struct {
// User describes a user.
type User struct {
StickySidebar bool `json:"stickySidebar"`
DarkMode bool `json:"darkMode"`
DisableSettings bool `json:"disableSettings"`
ID uint `storm:"id,increment" json:"id"`
Username string `storm:"unique" json:"username"`
Password string `json:"password,omitempty"`
Scope string `json:"scope"`
Locale string `json:"locale"`
LockPassword bool `json:"lockPassword"`
ViewMode string `json:"viewMode"`
SingleClick bool `json:"singleClick"`
Sorting Sorting `json:"sorting"`
Perm Permissions `json:"perm"`
Commands []string `json:"commands"`
Rules []Rule `json:"rules"`
ApiKeys map[string]AuthToken `json:"apiKeys,omitempty"`
ShowHidden bool `json:"showHidden"`
DateFormat bool `json:"dateFormat"`
GallerySize int `json:"gallerySize"`
ThemeColor string `json:"themeColor"`
QuickDownload bool `json:"quickDownload"`
StickySidebar bool `json:"stickySidebar"`
DarkMode bool `json:"darkMode"`
DisableSettings bool `json:"disableSettings"`
ID uint `storm:"id,increment" json:"id"`
Username string `storm:"unique" json:"username"`
Password string `json:"password,omitempty"`
Scope string `json:"scope"`
Locale string `json:"locale"`
LockPassword bool `json:"lockPassword"`
ViewMode string `json:"viewMode"`
SingleClick bool `json:"singleClick"`
Sorting Sorting `json:"sorting"`
Perm Permissions `json:"perm"`
Commands []string `json:"commands"`
Rules []Rule `json:"rules"`
ApiKeys map[string]AuthToken `json:"apiKeys,omitempty"`
ShowHidden bool `json:"showHidden"`
DateFormat bool `json:"dateFormat"`
GallerySize int `json:"gallerySize"`
ThemeColor string `json:"themeColor"`
QuickDownload bool `json:"quickDownload"`
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"`
}
var PublicUser = User{
Username: "publicUser", // temp user not registered
Password: "publicUser", // temp user not registered
Scope: "./",
Scope: "/does/not/exist",
ViewMode: "normal",
LockPassword: true,
Perm: Permissions{

View File

@ -22,6 +22,7 @@
"dependencies": {
"@onlyoffice/document-editor-vue": "^1.4.0",
"ace-builds": "^1.24.2",
"axios": "^1.7.9",
"clipboard": "^2.0.4",
"css-vars-ponyfill": "^2.4.3",
"dompurify": "^3.2.4",
@ -29,6 +30,7 @@
"marked": "^15.0.6",
"normalize.css": "^8.0.1",
"qrcode.vue": "^3.4.1",
"srt-support-for-html5-videos": "^2.6.11",
"vue": "^3.4.21",
"vue-i18n": "^9.10.2",
"vue-lazyload": "^3.0.0",

View File

@ -1,243 +1,244 @@
import { fetchURL, adjustedData } from "./utils";
import { removePrefix, getApiPath } from "@/utils/url.js";
import { state } from "@/store";
import { notify } from "@/notify";
import { externalUrl } from "@/utils/constants";
import { fetchURL, adjustedData } from './utils'
import { removePrefix, getApiPath } from '@/utils/url.js'
import { state } from '@/store'
import { notify } from '@/notify'
import { externalUrl } from '@/utils/constants'
// Notify if errors occur
export async function fetchFiles(url, content = false) {
export async function fetchFiles (url, content = false) {
try {
let path = encodeURIComponent(removePrefix(url, "files"));
const apiPath = getApiPath("api/resources",{path: path, content: content});
const res = await fetchURL(apiPath);
const data = await res.json();
const adjusted = adjustedData(data, url);
return adjusted;
let path = encodeURIComponent(removePrefix(url, 'files'))
const apiPath = getApiPath('api/resources', {
path: path,
content: content
})
const res = await fetchURL(apiPath)
const data = await res.json()
const adjusted = adjustedData(data, url)
return adjusted
} catch (err) {
notify.showError(err.message || "Error fetching data");
throw err;
notify.showError(err.message || 'Error fetching data')
throw err
}
}
async function resourceAction(url, method, content) {
async function resourceAction (url, method, content) {
try {
let opts = { method };
let opts = { method }
if (content) {
opts.body = content;
opts.body = content
}
let path = encodeURIComponent(removePrefix(url, "files"));
const apiPath = getApiPath("api/resources", { path: path });
const res = await fetchURL(apiPath, opts);
return res;
let path = encodeURIComponent(removePrefix(url, 'files'))
const apiPath = getApiPath('api/resources', { path: path })
const res = await fetchURL(apiPath, opts)
return res
} catch (err) {
notify.showError(err.message || "Error performing resource action");
throw err;
notify.showError(err.message || 'Error performing resource action')
throw err
}
}
export async function remove(url) {
export async function remove (url) {
try {
let path = encodeURIComponent(removePrefix(url, "files"));
return await resourceAction(path, "DELETE");
let path = encodeURIComponent(removePrefix(url, 'files'))
return await resourceAction(path, 'DELETE')
} catch (err) {
notify.showError(err.message || "Error deleting resource");
throw err;
notify.showError(err.message || 'Error deleting resource')
throw err
}
}
export async function put(url, content = "") {
export async function put (url, content = '') {
try {
let path = encodeURIComponent(removePrefix(url, "files"));
return await resourceAction(path, "PUT", content);
let path = encodeURIComponent(removePrefix(url, 'files'))
return await resourceAction(path, 'PUT', content)
} catch (err) {
notify.showError(err.message || "Error putting resource");
throw err;
notify.showError(err.message || 'Error putting resource')
throw err
}
}
export function download(format, files) {
if (format != "zip") {
format = "tar.gz"
export function download (format, files) {
if (format != 'zip') {
format = 'tar.gz'
}
try {
let fileargs = "";
let fileargs = ''
if (files.length === 1) {
fileargs = decodeURI(removePrefix(files[0], "files"))
fileargs = decodeURI(removePrefix(files[0], 'files'))
} else {
for (let file of files) {
fileargs += decodeURI(removePrefix(file,"files")) + ",";
fileargs += decodeURI(removePrefix(file, 'files')) + ',|'
}
fileargs = fileargs.substring(0, fileargs.length - 1);
fileargs = fileargs.substring(0, fileargs.length - 1)
}
const apiPath = getApiPath("api/raw", { files: encodeURIComponent(fileargs), algo: format });
const url = window.origin+apiPath
window.open(url);
const apiPath = getApiPath('api/raw', {
files: encodeURIComponent(fileargs),
algo: format
})
const url = window.origin + apiPath
window.open(url)
} catch (err) {
notify.showError(err.message || "Error downloading files");
notify.showError(err.message || 'Error downloading files')
}
}
export async function post(url, content = "", overwrite = false, onupload) {
export async function post (url, content = '', overwrite = false, onupload) {
try {
url = removePrefix(url, "files");
url = removePrefix(url, 'files')
let bufferContent;
let bufferContent
if (
content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)
!['http:', 'https:'].includes(window.location.protocol)
) {
bufferContent = await new Response(content).arrayBuffer();
bufferContent = await new Response(content).arrayBuffer()
}
const apiPath = getApiPath("api/resources", { path: url, override: overwrite });
const apiPath = getApiPath('api/resources', {
path: url,
override: overwrite
})
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest();
request.open(
"POST",
apiPath,
true
);
request.setRequestHeader("X-Auth", state.jwt);
let request = new XMLHttpRequest()
request.open('POST', apiPath, true)
request.setRequestHeader('X-Auth', state.jwt)
if (typeof onupload === "function") {
request.upload.onprogress = (event) => {
if (typeof onupload === 'function') {
request.upload.onprogress = event => {
if (event.lengthComputable) {
const percentComplete = Math.round((event.loaded / event.total) * 100);
onupload(percentComplete); // Pass the percentage to the callback
const percentComplete = Math.round(
(event.loaded / event.total) * 100
)
onupload(percentComplete) // Pass the percentage to the callback
}
};
}
}
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText);
resolve(request.responseText)
} else if (request.status === 409) {
reject(request.status);
reject(request.status)
} else {
reject(request.responseText);
reject(request.responseText)
}
};
request.onerror = () => {
reject(new Error("001 Connection aborted"));
};
request.send(bufferContent || content);
});
}
request.send(bufferContent || content)
})
} catch (err) {
notify.showError(err.message || "Error posting resource");
throw err;
notify.showError(err.message || 'Error posting resource')
throw err
}
}
export async function moveCopy(items, action = "copy", overwrite = false, rename = false) {
export async function moveCopy (
items,
action = 'copy',
overwrite = false,
rename = false
) {
let params = {
overwrite: overwrite,
action: action,
rename: rename,
};
rename: rename
}
try {
// Create an array of fetch calls
let promises = items.map((item) => {
let toPath = encodeURIComponent(removePrefix(decodeURI(item.to), "files"));
let fromPath = encodeURIComponent(removePrefix(decodeURI(item.from), "files"));
let localParams = { ...params, destination: toPath, from: fromPath };
const apiPath = getApiPath("api/resources", localParams);
return fetch(apiPath, { method: "PATCH" }).then((response) => {
let promises = items.map(item => {
let toPath = encodeURIComponent(removePrefix(decodeURI(item.to), 'files'))
let fromPath = encodeURIComponent(
removePrefix(decodeURI(item.from), 'files')
)
let localParams = { ...params, destination: toPath, from: fromPath }
const apiPath = getApiPath('api/resources', localParams)
return fetch(apiPath, { method: 'PATCH' }).then(response => {
if (!response.ok) {
// Throw an error if the fetch fails
return response.text().then((text) => {
throw new Error(`Failed to move/copy: ${text || response.statusText}`);
});
return response.text().then(text => {
throw new Error(
`Failed to move/copy: ${text || response.statusText}`
)
})
}
return response;
});
});
return response
})
})
// Await all promises and ensure errors propagate
await Promise.all(promises);
await Promise.all(promises)
} catch (err) {
notify.showError(err.message || "Error moving/copying resources");
throw err; // Re-throw the error to propagate it back to the caller
notify.showError(err.message || 'Error moving/copying resources')
throw err // Re-throw the error to propagate it back to the caller
}
}
export async function checksum(path, algo) {
export async function checksum (path, algo) {
try {
const params = {
path: encodeURIComponent(removePrefix(path, "files")),
checksum: algo,
};
const apiPath = getApiPath("api/resources", params);
const res = await fetchURL(apiPath);
const data = await res.json();
return data.checksums[algo];
} catch (err) {
notify.showError(err.message || "Error fetching checksum");
throw err;
}
}
export function getDownloadURL(path, inline, useExternal) {
try {
const params = {
files: encodeURIComponent(removePrefix(decodeURI(path),"files")),
...(inline && { inline: "true" }),
};
const apiPath = getApiPath("api/raw", params);
if (externalUrl && useExternal) {
return externalUrl+apiPath
path: encodeURIComponent(removePrefix(path, 'files')),
checksum: algo
}
return window.origin+apiPath
const apiPath = getApiPath('api/resources', params)
const res = await fetchURL(apiPath)
const data = await res.json()
return data.checksums[algo]
} catch (err) {
notify.showError(err.message || "Error getting download URL");
throw err;
notify.showError(err.message || 'Error fetching checksum')
throw err
}
}
export function getPreviewURL(path, size, modified) {
export function getDownloadURL (path, inline, useExternal) {
try {
const params = {
path: encodeURIComponent(removePrefix(decodeURI(path),"files")),
files: encodeURIComponent(removePrefix(decodeURI(path), 'files')),
...(inline && { inline: 'true' })
}
const apiPath = getApiPath('api/raw', params)
if (externalUrl && useExternal) {
return externalUrl + apiPath
}
return window.origin + apiPath
} catch (err) {
notify.showError(err.message || 'Error getting download URL')
throw err
}
}
export function getPreviewURL (path, size, modified) {
try {
const params = {
path: encodeURIComponent(removePrefix(decodeURI(path), 'files')),
size: size,
key: Date.parse(modified),
inline: "true",
};
const apiPath = getApiPath("api/preview", params);
return window.origin+apiPath
} catch (err) {
notify.showError(err.message || "Error getting preview URL");
throw err;
}
}
export function getSubtitlesURL(file) {
try {
const subtitles = [];
for (const sub of file.subtitles) {
const params = {
inline: "true",
path: encodeURIComponent(removePrefix(sub,"files"))
};
const apiPath = getApiPath("api/raw", params);
return window.origin+apiPath
inline: 'true'
}
return subtitles;
const apiPath = getApiPath('api/preview', params)
return window.origin + apiPath
} catch (err) {
notify.showError(err.message || "Error fetching subtitles URL");
throw err;
notify.showError(err.message || 'Error getting preview URL')
throw err
}
}
export async function usage(source) {
export function getSubtitlesURL (path) {
const params = {
inline: true,
files: path,
source: 'default'
}
const apiPath = getApiPath('api/raw', params)
return window.origin + apiPath
}
export async function usage (source) {
try {
const apiPath = getApiPath("api/usage", { source: source });
const res = await fetchURL(apiPath);
return await res.json();
const apiPath = getApiPath('api/usage', { source: source })
const res = await fetchURL(apiPath)
return await res.json()
} catch (err) {
notify.showError(err.message || "Error fetching usage data");
throw err;
notify.showError(err.message || 'Error fetching usage data')
throw err
}
}

View File

@ -68,7 +68,12 @@ export function adjustedData(data, url) {
data.items = [...(data.folders || []), ...(data.files || [])];
data.items = data.items.map((item) => {
item.url = `${data.url}${item.name}`;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (data.path == "/") {
item.path = `/${item.name}`
} else {
item.path = `${data.path}/${item.name}`
}
if (item.type === "directory") {
item.url += "/";
}

View File

@ -13,21 +13,23 @@ describe('adjustedData', () => {
{ name: "file1.txt", type: "file" },
{ name: "file2.txt", type: "file" },
],
path: "/root"
};
const url = "http://example.com/unit-testing/files/path/to/directory";
const url = "http://example.com/root/";
const expected = {
type: "directory",
url: "http://example.com/unit-testing/files/path/to/directory/",
url: "http://example.com/root/",
folders: [],
files: [],
items: [
{ name: "folder1", type: "directory", url: "http://example.com/unit-testing/files/path/to/directory/folder1/" },
{ name: "folder2", type: "directory", url: "http://example.com/unit-testing/files/path/to/directory/folder2/" },
{ name: "file1.txt", type: "file", url: "http://example.com/unit-testing/files/path/to/directory/file1.txt" },
{ name: "file2.txt", type: "file", url: "http://example.com/unit-testing/files/path/to/directory/file2.txt" },
{ name: "folder1", path: "/root/folder1", type: "directory", url: "http://example.com/root/folder1/" },
{ name: "folder2", path: "/root/folder2", type: "directory", url: "http://example.com/root/folder2/" },
{ name: "file1.txt", path: "/root/file1.txt", type: "file", url: "http://example.com/root/file1.txt" },
{ name: "file2.txt", path: "/root/file2.txt", type: "file", url: "http://example.com/root/file2.txt" },
],
path: "/root",
};
expect(adjustedData(input, url)).toEqual(expected);

View File

@ -13,27 +13,36 @@
<div v-if="selectedCount > 0" class="button selected-count-header">
<span>{{ selectedCount }} selected</span>
</div>
<action
v-if="!isSearchActive"
v-if="!showCreate"
icon="add"
label="New"
@action="startShowCreate"
/>
<action
v-if="showCreate && !isSearchActive"
icon="create_new_folder"
:label="$t('sidebar.newFolder')"
@action="showHover('newDir')"
/>
<action
v-if="!headerButtons.select && !isSearchActive"
v-if="showCreate && !isSearchActive"
icon="note_add"
:label="$t('sidebar.newFile')"
@action="showHover('newFile')"
/>
<action
v-if="!headerButtons.select && !isSearchActive"
v-if="showCreate && !isSearchActive"
icon="file_upload"
:label="$t('buttons.upload')"
@action="uploadFunc"
/>
<action
v-if="headerButtons.select"
v-if="!showCreate && headerButtons.select"
icon="info"
:label="$t('buttons.info')"
show="info"
@ -45,38 +54,38 @@
@action="toggleMultipleSelection"
/>
<action
v-if="headerButtons.download"
v-if="!showCreate && headerButtons.download"
icon="file_download"
:label="$t('buttons.download')"
@action="startDownload"
:counter="selectedCount"
/>
<action
v-if="headerButtons.share"
v-if="!showCreate && headerButtons.share"
icon="share"
:label="$t('buttons.share')"
show="share"
/>
<action
v-if="headerButtons.rename && !isSearchActive"
v-if="!showCreate && headerButtons.rename && !isSearchActive"
icon="mode_edit"
:label="$t('buttons.rename')"
show="rename"
/>
<action
v-if="headerButtons.copy"
v-if="!showCreate && headerButtons.copy"
icon="content_copy"
:label="$t('buttons.copyFile')"
show="copy"
/>
<action
v-if="headerButtons.move"
v-if="!showCreate && headerButtons.move"
icon="forward"
:label="$t('buttons.moveFile')"
show="move"
/>
<action
v-if="headerButtons.delete"
v-if="!showCreate && headerButtons.delete"
icon="delete"
:label="$t('buttons.delete')"
show="delete"
@ -88,6 +97,8 @@
import downloadFiles from "@/utils/download";
import { state, getters, mutations } from "@/store"; // Import your custom store
import Action from "@/components/Action.vue";
import { onlyOfficeUrl } from "@/utils/constants.js";
export default {
name: "ContextMenu",
@ -98,9 +109,20 @@ export default {
return {
posX: 0,
posY: 0,
showCreate: false,
};
},
computed: {
showContext() {
if (getters.currentPromptName() == "ContextMenu" && state.prompts != []) {
this.setPositions();
return true;
}
return false;
},
onlyofficeEnabled() {
return onlyOfficeUrl !== "";
},
isSearchActive() {
return state.isSearchActive;
},
@ -113,13 +135,6 @@ export default {
centered() {
return getters.isMobile() || !this.posX || !this.posY;
},
showContext() {
if (getters.currentPromptName() == "ContextMenu" && state.prompts != []) {
this.setPositions();
return true;
}
return false;
},
top() {
// Ensure the context menu stays within the viewport
return Math.min(
@ -153,6 +168,9 @@ export default {
},
},
methods: {
startShowCreate() {
this.showCreate = true;
},
uploadFunc() {
mutations.showHover("upload");
},
@ -160,6 +178,11 @@ export default {
return mutations.showHover(value);
},
setPositions() {
if (state.selected.length > 0) {
this.showCreate = false;
} else {
this.showCreate = true;
}
const contextProps = getters.currentPrompt().props;
let tempX = contextProps.posX;
let tempY = contextProps.posY;

View File

@ -35,7 +35,7 @@
<!-- Search results for desktop -->
<div v-show="active" id="results" ref="result">
<div class="searchContext">Search Context: {{ getContext }}</div>
<div aria-label="search-path" class="searchContext">Search Context: {{ getContext }}</div>
<div id="result-list">
<div>
<div v-if="active">

View File

@ -76,14 +76,14 @@ export default {
if (state.isSearchActive) {
this.items = [
{
from: "/files" + state.selected[0].url,
from: "/files" + state.selected[0].path,
name: state.selected[0].name,
},
];
} else {
for (let item of state.selected) {
this.items.push({
from: state.req.items[item].url,
from: state.req.items[item].path,
// add to: dest
name: state.req.items[item].name,
});

View File

@ -7,7 +7,7 @@
<p v-else>
{{ $t("prompts.deleteMessageMultiple", { count: selectedCount }) }}
</p>
<div style="display: grid" class="searchContext">
<div style="display: grid" aria-label="delete-path" class="searchContext">
<span v-for="(item, index) in nav" :key="index"> {{ item }} </span>
</div>
</div>
@ -23,7 +23,7 @@
<button
@click="submit"
class="button button--flat button--red"
:aria-label="$t('buttons.delete')"
aria-label="Confirm-Delete"
:title="$t('buttons.delete')"
>
{{ $t("buttons.delete") }}
@ -37,7 +37,6 @@ import { filesApi } from "@/api";
import buttons from "@/utils/buttons";
import { state, getters, mutations } from "@/store";
import { notify } from "@/notify";
import { removePrefix } from "@/utils/url";
export default {
name: "delete",
@ -57,7 +56,7 @@ export default {
}
let paths = [];
for (let index of state.selected) {
paths.push(removePrefix(state.req.items[index].url, "files"));
paths.push(state.req.items[index].path);
}
return paths;
},
@ -71,7 +70,7 @@ export default {
try {
if (state.isSearchActive) {
await filesApi.remove(state.selected[0].url);
await filesApi.remove(state.selected[0].path);
buttons.success("delete");
notify.showSuccess("Deleted item successfully");
mutations.closeHovers();
@ -95,14 +94,14 @@ export default {
let promises = [];
for (let index of state.selected) {
promises.push(filesApi.remove(state.req.items[index].url));
promises.push(filesApi.remove(state.req.items[index].path));
}
await Promise.all(promises);
buttons.success("delete");
notify.showSuccess("Deleted item successfully");
window.location.reload();
mutations.setReload(true); // Handle reload as needed
notify.showSuccess("Deleted item successfully! reloading...");
mutations.setReload(true); // Handle reload as neededs
} catch (e) {
buttons.done("delete");
notify.showError(e);

View File

@ -72,7 +72,7 @@ export default {
let promises = [];
for (let index of this.selected) {
promises.push(usersApi.remove(state.req.items[index].url));
promises.push(usersApi.remove(state.req.items[index].path));
}
await Promise.all(promises);

View File

@ -1,6 +1,6 @@
<template>
<div>
<div class="searchContext">Path: {{ nav }}</div>
<div aria-label="filelist-path" class="searchContext">Path: {{ nav }}</div>
<ul class="file-list">
<li
@click="itemClick"

View File

@ -165,12 +165,12 @@ export default {
event.preventDefault();
let link;
if (state.isSearchActive) {
const hash = await filesApi.checksum(state.selected[0].url, algo);
const hash = await filesApi.checksum(state.selected[0].path, algo);
event.target.innerHTML = hash;
return;
}
if (getters.selectedCount()) {
link = state.req.items[this.selected[0]].url;
link = state.req.items[this.selected[0]].path;
} else {
link = state.route.path;
}

View File

@ -75,14 +75,14 @@ export default {
if (state.isSearchActive) {
this.items = [
{
from: "/files" + state.selected[0].url,
from: "/files" + state.selected[0].path,
name: state.selected[0].name,
},
];
} else {
for (let item of state.selected) {
this.items.push({
from: state.req.items[item].url,
from: state.req.items[item].path,
// add to: dest
name: state.req.items[item].name,
});

View File

@ -106,12 +106,10 @@ export default {
return;
}
window.location.reload();
mutations.closeHovers();
mutations.setReload(true);
} catch (error) {
notify.showError(error);
}
},
},
};

View File

@ -3,7 +3,7 @@
<div class="card-title">
<h2>{{ $t("buttons.share") }}</h2>
</div>
<div class="searchContext">Path: {{ subpath }}</div>
<div aria-label="share-paths" class="searchContext">Path: {{ subpath }}</div>
<p>Note: anyone who has access to the link (and optional password) can access the shared files. There is no need to be logged in.</p>
<template v-if="listing">
<div class="card-content">

View File

@ -1,63 +0,0 @@
<template>
<form class="rules small">
<div v-for="(rule, index) in rules" :key="index">
<input type="checkbox" v-model="rule.regex" /><label>Regex</label>
<input type="checkbox" v-model="rule.allow" /><label>Allow</label>
<input
@keypress.enter.prevent
type="text"
v-if="rule.regex"
v-model="rule.regexp.raw"
:placeholder="$t('settings.insertRegex')"
/>
<input
@keypress.enter.prevent
type="text"
v-else
v-model="rule.path"
:placeholder="$t('settings.insertPath')"
/>
<button class="button button--red" @click="remove($event, index)">
-
</button>
</div>
<div>
<button class="button" @click="create" default="false">
{{ $t("buttons.new") }}
</button>
</div>
</form>
</template>
<script>
export default {
name: "rules-textarea",
props: ["rules"],
methods: {
remove(event, index) {
event.preventDefault();
let rules = [...this.rules];
rules.splice(index, 1);
this.$emit("update:rules", [...rules]);
},
create(event) {
event.preventDefault();
this.$emit("update:rules", [
...this.rules,
{
allow: true,
path: "",
regex: false,
regexp: {
raw: "",
},
},
]);
},
},
};
</script>

View File

@ -190,6 +190,7 @@ export default {
.user-card {
flex-direction: row !important;
justify-content: space-between !important;
color: var(--textPrimary);
}
.quick-toggles {
@ -197,6 +198,7 @@ export default {
justify-content: space-evenly;
width: 100%;
margin-top: 0.5em !important;
color: var(--textPrimary);
}
.quick-toggles button {

View File

@ -83,7 +83,8 @@ export default {
}
@supports (backdrop-filter: none) {
nav {
#sidebar {
background-color: rgba(237, 237, 237, 0.33) !important;
backdrop-filter: blur(16px) invert(0.1);
}
}
@ -126,7 +127,7 @@ body.rtl .action {
.credits {
font-size: 1em;
color: var(--textSecondary);
color: var(--textPrimary);
padding-left: 1em;
padding-bottom: 1em;
}
@ -139,7 +140,6 @@ body.rtl .action {
.credits a,
.credits a:hover {
color: inherit;
cursor: pointer;
}

View File

@ -4,7 +4,7 @@
--alt-background: #ddd;
--surfacePrimary: gray;
--surfaceSecondary: lightgray;
--textPrimary: white;
--textPrimary: #546e7a;
--textSecondary: gray;
}
@ -253,4 +253,4 @@ input.sizeInput:disabled {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
background-color: var(--surfaceSecondary);
}
}

View File

@ -15,7 +15,8 @@ header {
@supports (backdrop-filter: none) {
header {
backdrop-filter: blur(16px);
background-color: rgba(237, 237, 237, 0.33) !important;
backdrop-filter: blur(16px) invert(0.1);
}
}
@ -49,3 +50,11 @@ header img {
header .action span {
display: none;
}
/* Header with backdrop-filter support */
@supports (backdrop-filter: none) {
header {
background-color: rgba(237, 237, 237, 0.33) !important;
backdrop-filter: blur(16px) invert(0.1);
}
}

View File

@ -3,7 +3,6 @@ export function startLoading (from, to) {
return
}
console.log('startLoading', from, to)
// Get the spinner canvas element
let spinner = document.querySelector('.notification-spinner')
if (!spinner) {
@ -27,8 +26,8 @@ export function startLoading (from, to) {
let degrees = from * 3.6 // Convert percentage to degrees
let new_degrees = to * 3.6 // Convert percentage to degrees
let difference = new_degrees - degrees
let color = spinner.style.color || '#666'
let bgcolor = '#fff'
let color = spinner.style.color || '#fff'
let bgcolor = '#666'
let animation_loop
// Clear any existing animation loop

View File

@ -1,4 +1,5 @@
import { removePrefix } from "@/utils/url.js";
import { getFileExtension } from "@/utils/files.js";
import { state } from "./state.js";
import { mutations } from "./mutations.js";
import { noAuth } from "@/utils/constants.js";
@ -146,7 +147,7 @@ export const getters = {
if (state.req.type !== undefined) {
if (state.req.type == "directory") {
return "listingView";
} else if (state.req?.onlyOfficeId) {
} else if (getters.onlyOfficeEnabled(state.req.name)) {
return "onlyOfficeEditor";
} else if ("content" in state.req && state.req.type == "text/markdown" && window.location.hash != "#edit") {
return "markdownViewer";
@ -240,4 +241,19 @@ export const getters = {
return files.sort((a, b) => a.progress - b.progress);
},
onlyOfficeEnabled: (filename) => {
if (!state.req?.onlyOfficeId) {
return false
}
const ext = getFileExtension(filename);
if (state.user.disableOnlyOfficeExt) {
const disabledList = state.user.disableOnlyOfficeExt.split(" ");
for (const e of disabledList) {
if (e.trim().toLowerCase() === ext.toLowerCase()) {
return false;
}
}
}
return true;
},
};

View File

@ -2,6 +2,7 @@ import { reactive } from 'vue';
import { detectLocale } from "@/i18n";
export const state = reactive({
disableOnlyOfficeExt: "",
isSafari: /^((?!chrome|android).)*safari/i.test(navigator.userAgent),
activeSettingsView: "",
isMobile: window.innerWidth <= 800,

View File

@ -0,0 +1,7 @@
export function getFileExtension(filename) {
const firstDotIndex = filename.indexOf('.');
if (firstDotIndex === -1) {
return filename; // No dot found, return the original filename
}
return filename.substring(firstDotIndex);
}

View File

@ -0,0 +1,190 @@
export function convertToVTT (ext, text) {
if (ext == '.vtt') {
return text
}
let vttContent = 'WEBVTT\n\n'
let lrcLines = text.split('\n').filter(line => line.trim() !== '') // Filter out empty lines
switch (ext.toLowerCase()) {
case '.srt':
// Convert SRT to VTT
vttContent += text
.replace(/\r\n|\r/g, '\n') // Normalize newlines
.replace(
/\d+\n(\d{2}):(\d{2}):(\d{2}),(\d{3}) --> (\d{2}):(\d{2}):(\d{2}),(\d{3})/g,
(match, h1, m1, s1, ms1, h2, m2, s2, ms2) => {
return `${parseInt(h1)}:${m1}:${s1}.${ms1} --> ${parseInt(
h2
)}:${m2}:${s2}.${ms2}`
}
) // Fix timestamps (remove leading zeros)
.replace(/\n\n+/g, '\n\n') // Ensure proper spacing
.replace(/(\d+:\d{2}:\d{2}\.\d{3})\n([^\n])/g, '$1\n$2') // Prevent extra blank lines
.trim()
break
case '.sbv':
// Convert SBV to VTT
vttContent += text
.replace(
/(\d{1,2}:\d{2}:\d{2}\.\d{3}),(\d{1,2}:\d{2}:\d{2}\.\d{3})/g,
'$1 --> $2'
) // Convert comma to -->
.replace(/\n\n+/g, '\n\n')
.trim()
break
case '.lrc':
// Convert LRC to VTT
lrcLines = lrcLines
.map((line, index) => {
if (line.startsWith('[')) {
let [time, dialogue] = line.split(']') // Remove square brackets
if (!time || !dialogue) return '' // Skip invalid lines
time = time.slice(1) // Remove opening square bracket
// Format time to HH:MM:SS.MMM
let startTime = formatLrcTime(time)
let endTime =
index + 1 < lrcLines.length
? formatLrcTime(lrcLines[index + 1].split(']')[0].slice(1))
: startTime // Use next line's timestamp for end time
return `${startTime} --> ${endTime}\n ${dialogue.trim()}` // Add leading space before dialogue
} else {
return '' // Skip lines that don't start with '['
}
})
.filter(line => line !== '')
.join('\n\n')
.trim() // Remove empty lines
vttContent += lrcLines
break
case '.ass':
case '.ssa':
// Convert ASS/SSA to VTT
vttContent += text
.split('\n')
.filter(line => line.startsWith('Dialogue:')) // Keep only dialogue lines
.map(line => {
let parts = line.split(',')
let startTime = formatAssTime(parts[1].trim())
let endTime = formatAssTime(parts[2].trim())
let dialogue = parts.slice(9).join(',').trim()
return `${startTime} --> ${endTime}\n ${dialogue}` // Add leading space before dialogue
})
.join('\n\n')
.trim()
break
case '.smi':
// Convert SAMI to VTT
vttContent += text
.replace(/\r\n|\r/g, '\n') // Normalize newlines
.match(/<SYNC Start=\d+>.*?<\/SYNC>/gs) // Match all <SYNC> blocks
?.map(syncBlock => {
let startTime = syncBlock.match(/<SYNC Start=(\d+)>/)[1] // Extract start time in ms
let dialogueMatch = syncBlock.match(/<P[^>]*>(.*?)<\/P>/) // Extract text inside <P>
let dialogue = dialogueMatch
? dialogueMatch[1].replace(/<[^>]+>/g, '').trim()
: '' // Remove HTML tags
if (!dialogue) return '' // Skip empty captions
let startFormatted = formatMillisecondsToVTT(startTime)
// Use next start time as the end time if available
let nextSyncMatch = text.match(
new RegExp(`<SYNC Start=${parseInt(startTime) + 1}>`)
)
let endTime = nextSyncMatch
? nextSyncMatch[1]
: parseInt(startTime) + 3000 // Default to +3s
let endFormatted = formatMillisecondsToVTT(endTime)
return `${startFormatted} --> ${endFormatted}\n${dialogue}`
})
.filter(syncBlock => syncBlock !== '')
.join('\n\n')
.trim()
break
case '.sub':
// Convert SUB to VTT
vttContent += text
.replace(/\r\n|\r/g, '\n') // Normalize newlines
.split('\n\n') // Split by empty lines (each caption block)
.map(block => {
let lines = block.split('\n').filter(line => line.trim() !== ''); // Remove empty lines
if (lines.length < 2) return ''; // Ensure each block has a timestamp and text
let [time, ...dialogue] = lines; // First line is time, rest is dialogue
let [start, end] = time.split(','); // Split start and end time
if (!start || !end || dialogue.length === 0) return ''; // Skip invalid blocks
start = formatSubTime(start.trim());
end = formatSubTime(end.trim());
return `${start} --> ${end}\n${dialogue.join(' ')}`; // Join dialogue lines into one block
})
.filter(block => block !== '') // Remove empty blocks
.join('\n\n')
.trim();
break;
default:
throw new Error('Unsupported subtitle format.')
}
return vttContent
}
// Helper function to format LRC time (e.g., 00:00.000 -> 00:00:00.000)
function formatLrcTime (time) {
let [minutes, seconds] = time.split(':')
let [sec, ms] = seconds.split('.')
ms = ms ? ms.padEnd(3, '0') : '000' // Ensure milliseconds are 3 digits
// Return in the correct format
return `00:${minutes}:${sec}.${ms}`
}
// Helper function to fix ASS/SSA timestamps
function formatAssTime (time) {
let parts = time.split(':')
let hours = parts.length === 3 ? parseInt(parts[0]) : 0
let minutes = parts.length === 3 ? parts[1] : parts[0]
let seconds = parts[parts.length - 1].replace('.', ':')
// Ensure proper milliseconds formatting (000)
const [sec, ms = '000'] = seconds.split(':')
return `${hours}:${minutes}:${sec}.${ms.padEnd(3, '0')}`
}
// Helper function to ensure proper SUB timestamp format (with 3-digit milliseconds)
function formatSubTime(time) {
let parts = time.split(':');
if (parts.length < 3) return '00:00:00.000'; // Fallback for invalid input
let [hours, minutes, secMs] = parts;
let [seconds, ms = '000'] = secMs.split('.');
ms = ms.padEnd(3, '0'); // Ensure milliseconds are 3 digits
return `${hours}:${minutes}:${seconds}.${ms}`;
}
// Helper function to convert milliseconds to HH:MM:SS.MMM format
function formatMillisecondsToVTT (ms) {
let totalSeconds = Math.floor(ms / 1000)
let milliseconds = (ms % 1000).toString().padStart(3, '0')
let hours = Math.floor(totalSeconds / 3600)
let minutes = Math.floor((totalSeconds % 3600) / 60)
let seconds = totalSeconds % 60
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds
.toString()
.padStart(2, '0')}.${milliseconds}`
}

View File

@ -128,7 +128,7 @@ export async function handleFiles(files, base, overwrite = false) {
overwrite,
};
let last = 0;
notify.showPopup("success",`(${c} of ${count}) Uploading ${file.name}`,false);
notify.showPopup("success", `(${c} of ${count}) Uploading ${file.name}`, false);
await filesApi.post(
item.path,
item.file,
@ -143,23 +143,22 @@ export async function handleFiles(files, base, overwrite = false) {
last = percentComplete;
setTimeout(() => {
blockUpdates = false;
}, 200);
}, 250);
}
)
.then(response => {
let spinner = document.querySelector('.notification-spinner')
).then(response => {
let spinner = document.querySelector('.notification-spinner');
if (spinner) {
spinner.classList.add('hidden')
spinner.classList.add('hidden');
}
console.log("Upload successful!",response);
console.log("Upload successful!", response);
notify.showSuccess("Upload successful!");
})
.catch(error => {
let spinner = document.querySelector('.notification-spinner')
}).catch(error => {
let spinner = document.querySelector('.notification-spinner');
if (spinner) {
spinner.classList.add('hidden')
spinner.classList.add('hidden');
}
notify.showError("Error uploading file: "+error);
notify.showError("Error uploading file: " + error);
throw error;
});
}
}

View File

@ -54,15 +54,8 @@ export function removePrefix(path, prefix = "") {
if (prefix != "") {
prefix = "/" + trimSlashes(prefix)
}
const combined = trimSlashes(baseURL) + prefix
const combined2 = "/" + combined
// Remove combined (baseURL + prefix) from the start of the path if present
if (path.startsWith(combined)) {
path = path.slice(combined.length);
} else if (path.startsWith(combined2)) {
path = path.slice(combined2.length);
} else if (path.startsWith(prefix)) {
// Fallback: remove only the prefix if the combined string isn't present
if (path.startsWith(prefix)) {
path = path.slice(prefix.length);
}

View File

@ -70,6 +70,7 @@ export default {
$route: "fetchData",
reload(value) {
if (value) {
console.log("Reloading");
this.fetchData();
}
},
@ -100,15 +101,12 @@ export default {
}
},
async fetchData() {
if (state.route.path === this.lastPath) return;
this.lastHash = "";
// Set loading to true and reset the error.
mutations.setLoading("files", true);
this.error = null;
// Reset view information using mutations
mutations.setReload(false);
mutations.setMultiple(false);
mutations.closeHovers();
let data = {};
try {
@ -116,13 +114,7 @@ export default {
let res = await filesApi.fetchFiles(getters.routePath());
// If not a directory, fetch content
if (res.type != "directory") {
let content = false;
if (
!res.onlyOfficeId &&
(res.type.startsWith("application") || res.type.startsWith("text"))
) {
content = true;
}
const content = !getters.onlyOfficeEnabled()
res = await filesApi.fetchFiles(getters.routePath(), content);
}
data = res;

View File

@ -27,7 +27,7 @@
</style>
<script>
import { state, mutations } from "@/store";
import { state } from "@/store";
import { eventBus } from "@/store/eventBus";
import buttons from "@/utils/buttons";
import url from "@/utils/url.js";

View File

@ -80,7 +80,7 @@ export default {
},
methods: {
handleEditorValueRequest() {
filesApi.put(getters.routePath("files"), this.editor.getValue());
filesApi.put(state.req.path, this.editor.getValue());
},
back() {
let uri = url.removeLastDir(state.route.path) + "/";

View File

@ -176,6 +176,7 @@ export default {
this.columnWidth = 250 + state.user.gallerySize * 50;
this.colunmsResize();
},
},
computed: {
isStickySidebar() {
@ -296,6 +297,7 @@ export default {
window.addEventListener("resize", this.windowsResize);
window.addEventListener("click", this.clickClear);
window.addEventListener("keyup", this.clearCtrKey);
window.addEventListener("dragover", this.preventDefault);
// Adjust contextmenu listener based on browser
if (state.isSafari) {
@ -312,7 +314,6 @@ export default {
window.addEventListener("contextmenu", this.openContext);
}
// if safari , make sure click and hold opens context menu, but not for any other browser
if (!state.user.perm?.create) return;
this.$el.addEventListener("dragenter", this.dragEnter);
this.$el.addEventListener("dragleave", this.dragLeave);
@ -775,8 +776,6 @@ export default {
let el = event.target;
if (dt.files.length <= 0) {
mutations.setReload(true);
window.location.reload();
return;
}
@ -806,26 +805,29 @@ export default {
if (el !== null && el.classList.contains("item") && el.dataset.dir === "true") {
path = el.__vue__.url;
items = (await filesApi.fetchFiles(path)).items;
items = state.req.items
}
const conflict = upload.checkConflict(uploadFiles, items);
if (conflict) {
mutations.showHover({
name: "replace",
confirm: async (event) => {
event.preventDefault();
mutations.closeHovers();
await upload.handleFiles(uploadFiles, path, true);
},
});
} else {
await upload.handleFiles(uploadFiles, path);
try {
if (conflict) {
mutations.showHover({
name: "replace",
confirm: async (event) => {
event.preventDefault();
mutations.closeHovers();
await upload.handleFiles(uploadFiles, path, true);
},
});
} else {
await upload.handleFiles(uploadFiles, path);
}
mutations.setReload(true);
} catch {
console.log("failed to upload files")
}
mutations.setReload(true);
},
uploadInput(event) {
async uploadInput(event) {
mutations.closeHovers();
let files = event.currentTarget.files;
let folder_upload =
@ -843,16 +845,17 @@ export default {
if (conflict) {
mutations.showHover({
name: "replace",
confirm: (event) => {
confirm: async (event) => {
event.preventDefault();
mutations.closeHovers();
upload.handleFiles(files, path, true);
await upload.handleFiles(files, path, true);
},
});
return;
}
upload.handleFiles(files, path);
await upload.handleFiles(files, path);
mutations.setReload(true);
},
resetOpacity() {
let items = document.getElementsByClassName("item");

View File

@ -20,7 +20,9 @@ export default {
},
},
mounted() {
this.content = state.req.content
const fileContent =
state.req.content == "empty-file-x6OlSil" ? "" : state.req.content || "";
this.content = fileContent
},
};
</script>

View File

@ -1,19 +1,39 @@
<template>
<div id="previewer" @mousemove="toggleNavigation" @touchstart="toggleNavigation">
<div class="preview">
<ExtendedImage v-if="getSimpleType(currentItem.type) == 'image'" :src="raw">
<ExtendedImage v-if="getSimpleType(req.type) == 'image'" :src="raw">
</ExtendedImage>
<audio v-else-if="getSimpleType(currentItem.type) == 'audio'" ref="player" :src="raw" controls
:autoplay="autoPlay" @play="autoPlay = true"></audio>
<video v-else-if="getSimpleType(currentItem.type) == 'video'" ref="player" :src="raw" controls
:autoplay="autoPlay" @play="autoPlay = true">
<track kind="captions" v-for="(sub, index) in subtitles" :key="index" :src="sub" :label="'Subtitle ' + index"
:default="index === 0" />
Sorry, your browser doesn't support embedded videos, but don't worry, you can
<a :href="downloadUrl">download it</a>
and watch it with your favorite video player!
<audio
v-else-if="getSimpleType(req.type) == 'audio'"
ref="player"
:src="raw"
controls
:autoplay="autoPlay"
@play="autoPlay = true"
></audio>
<video
v-else-if="getSimpleType(req.type) == 'video'"
ref="player"
:src="raw"
controls
:autoplay="autoPlay"
@play="autoPlay = true"
>
<track
kind="captions"
v-for="(sub, index) in subtitlesList"
:key="index"
:src="sub.src"
:label="'Subtitle ' + sub.name"
:default="index === 0"
/>
</video>
<object v-else-if="getSimpleType(currentItem.type) == 'pdf'" class="pdf" :data="raw"></object>
<object
v-else-if="getSimpleType(req.type) == 'pdf'"
class="pdf"
:data="raw"
></object>
<div v-else class="info">
<div class="title">
<i class="material-icons">feedback</i>
@ -25,7 +45,12 @@
<i class="material-icons">file_download</i>{{ $t("buttons.download") }}
</div>
</a>
<a target="_blank" :href="raw" class="button button--flat" v-if="currentItem.type != 'directory'">
<a
target="_blank"
:href="raw"
class="button button--flat"
v-if="req.type != 'directory'"
>
<div>
<i class="material-icons">open_in_new</i>{{ $t("buttons.openFile") }}
</div>
@ -34,13 +59,24 @@
</div>
</div>
<button @click="prev" @mouseover="hoverNav = true" @mouseleave="hoverNav = false"
:class="{ hidden: !hasPrevious || !showNav }" :aria-label="$t('buttons.previous')"
:title="$t('buttons.previous')">
<button
@click="prev"
@mouseover="hoverNav = true"
@mouseleave="hoverNav = false"
:class="{ hidden: !hasPrevious || !showNav }"
:aria-label="$t('buttons.previous')"
:title="$t('buttons.previous')"
>
<i class="material-icons">chevron_left</i>
</button>
<button @click="next" @mouseover="hoverNav = true" @mouseleave="hoverNav = false"
:class="{ hidden: !hasNext || !showNav }" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
<button
@click="next"
@mouseover="hoverNav = true"
@mouseleave="hoverNav = false"
:class="{ hidden: !hasNext || !showNav }"
:aria-label="$t('buttons.next')"
:title="$t('buttons.next')"
>
<i class="material-icons">chevron_right</i>
</button>
<link rel="prefetch" :href="previousRaw" />
@ -55,6 +91,8 @@ import throttle from "@/utils/throttle";
import ExtendedImage from "@/components/files/ExtendedImage.vue";
import { state, getters, mutations } from "@/store";
import { getTypeInfo } from "@/utils/mimetype";
import { getFileExtension } from "@/utils/files";
import { convertToVTT } from "@/utils/subtitles";
const mediaTypes = ["image", "video", "audio", "blob"];
@ -78,16 +116,13 @@ export default {
nextRaw: "",
currentPrompt: null, // Replaces Vuex getter `currentPrompt`
oldReq: {}, // Replace with your actual initial state
currentItem: {
name: "",
path: "",
url: "",
modified: "",
type: "",
},
subtitlesList: [],
};
},
computed: {
raw() {
return filesApi.getDownloadURL(state.req.path);
},
isDarkMode() {
return getters.isDarkMode();
},
@ -98,20 +133,7 @@ export default {
return this.nextLink !== "";
},
downloadUrl() {
return filesApi.getDownloadURL(this.currentItem.url);
},
raw() {
if (this.currentItem.url == "" || this.currentItem.url == undefined) {
return;
}
const previewUrl = this.fullSize
? filesApi.getDownloadURL(this.currentItem.url, "large")
: filesApi.getPreviewURL(
this.currentItem.url,
"small",
this.currentItem.modified
);
return previewUrl;
return filesApi.getDownloadURL(state.req.path);
},
showMore() {
return getters.currentPromptName() === "more";
@ -119,11 +141,11 @@ export default {
isResizeEnabled() {
return resizePreview;
},
subtitles() {
if (this.currentItem.subtitles) {
return filesApi.getSubtitlesURL(this.currentItem);
}
return [];
getSubtitles() {
return this.subtitles();
},
req() {
return state.req;
},
},
watch: {
@ -138,12 +160,35 @@ export default {
async mounted() {
window.addEventListener("keydown", this.key);
this.listing = this.oldReq.items;
this.subtitlesList = await this.subtitles();
this.updatePreview();
},
beforeUnmount() {
window.removeEventListener("keydown", this.key);
},
methods: {
async subtitles() {
if (!state.req.subtitles || state.req.subtitles.length === 0) {
return [];
}
let subs = [];
for (const element of state.req.subtitles) {
const ext = getFileExtension(element);
const resp = await filesApi.fetchFiles(element, true); // Fetch .srt file
let vttContent = resp.content;
// Convert SRT to VTT (assuming srt2vtt() does this)
vttContent = convertToVTT(ext, resp.content);
// Create a virtual file (Blob) and get a URL for it
const blob = new Blob([vttContent], { type: "text/vtt" });
const vttURL = URL.createObjectURL(blob);
subs.push({
name: ext,
src: vttURL,
});
}
console.log(subs);
return subs;
},
getSimpleType(mimetype) {
return getTypeInfo(mimetype).simpleType;
},
@ -197,8 +242,7 @@ export default {
if (this.$refs.player && this.$refs.player.paused && !this.$refs.player.ended) {
this.autoPlay = false;
}
let parts = state.route.path.split("/");
this.name = decodeURI(parts.pop("/"));
this.name = state.req.name;
if (!this.listing) {
const path = url.removeLastDir(state.route.path);
const res = await filesApi.fetchFiles(path);
@ -216,7 +260,6 @@ export default {
if (this.listing[i].name !== this.name) {
continue;
}
this.currentItem = this.listing[i];
for (let j = i - 1; j >= 0; j--) {
let composedListing = this.listing[j];
composedListing.path = directoryPath + "/" + composedListing.name;

View File

@ -18,6 +18,21 @@
<input type="checkbox" v-model="quickDownload" />
Always show download icon for quick access
</p>
<div v-if="hasOnlyOfficeEnabled">
<h3>Disable onlyoffice viewer for certain file extentions</h3>
<p>A space separated list of file extensions to disable the only office viewer for. eg <code>.txt .html</code></p>
<input
class="input input--block"
:class="{'invalid-form':!formValidation()}"
type="text"
placeholder="enter file extentions"
id="onlyofficeExt"
v-model="disableOnlyOfficeExt"
/>
<button class="button" @click="submitOnlyOfficeChange" >save</button>
</div>
<h3>Theme Color</h3>
<ButtonGroup :buttons="colorChoices" @button-clicked="setColor" :initialActive="color" />
<h3>{{ $t("settings.language") }}</h3>
@ -30,6 +45,7 @@
<script>
import { notify } from "@/notify";
import { onlyOfficeUrl } from "@/utils/constants.js";
import { state, mutations } from "@/store";
import { usersApi } from "@/api";
import Languages from "@/components/settings/Languages.vue";
@ -50,6 +66,7 @@ export default {
color: "",
showHidden: false,
quickDownload: false,
disableOnlyOfficeExt: "",
colorChoices: [
{ label: "blue", value: "var(--blue)" },
{ label: "red", value: "var(--red)" },
@ -78,6 +95,9 @@ export default {
},
},
computed: {
hasOnlyOfficeEnabled() {
return onlyOfficeUrl != "";
},
settings() {
return state.settings;
},
@ -94,11 +114,23 @@ export default {
this.dateFormat = state.user.dateFormat;
this.color = state.user.themeColor;
this.quickDownload = state.user?.quickDownload;
this.disableOnlyOfficeExt = state.user.disableOnlyOfficeExt;
},
mounted() {
this.initialized = true;
},
methods: {
formValidation() {
let regex = /^\.\w+(?: \.\w+)*$/;
return regex.test(this.disableOnlyOfficeExt)
},
submitOnlyOfficeChange(event) {
if (!this.formValidation()) {
notify.showError("Invalid input, does not match requirement.")
return
}
this.updateSettings(event)
},
setColor(string) {
this.color = string
this.updateSettings()
@ -118,6 +150,7 @@ export default {
dateFormat: this.dateFormat,
themeColor: this.color,
quickDownload: this.quickDownload,
disableOnlyOfficeExt: this.disableOnlyOfficeExt,
};
const shouldReload =
rtlLanguages.includes(data.locale) !== rtlLanguages.includes(i18n.locale);
@ -127,6 +160,7 @@ export default {
"dateFormat",
"themeColor",
"quickDownload",
"disableOnlyOfficeExt"
]);
mutations.updateCurrentUser(data);
if (shouldReload) {
@ -144,3 +178,9 @@ export default {
},
};
</script>
<style>
.invalid-form {
border-color: red !important;
}
</style>

View File

@ -24,7 +24,7 @@ test("info from search", async ({ page, context }) => {
await expect(page.locator('.break-word')).toHaveText('Display Name: file.tar.gz');
})
test("copy from listing", async ({ page, context }) => {
test("copy from listing 2x", async ({ page, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="copyme.txt"]').waitFor({ state: 'visible' });
@ -32,8 +32,10 @@ test("copy from listing", async ({ page, 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('.searchContext')).toHaveText('Path: /');
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);
await page.locator('button[aria-label="Copy"]').click();
const popup = page.locator('#popup-notification-content');
await popup.waitFor({ state: 'visible' });
@ -46,12 +48,41 @@ test("copy from listing", async ({ page, 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('.searchContext')).toHaveText('Path: /');
await expect(page.locator('div[aria-label="filelist-path"]')).toHaveText('Path: /myfolder/');
await page.locator('li[aria-label="testdata"]').click();
await page.locator('button[aria-label="Copy"]').click();
const popup2 = page.locator('#popup-notification-content');
//await popup2.waitFor({ state: 'visible' });
//await expect(popup2).toHaveText("Successfully copied file/folder, redirecting...");
//await page.waitForURL('**/testdata/');
//await expect(page).toHaveTitle("Graham's Filebrowser - Files - testdata");
})
await popup2.waitFor({ state: 'visible' });
await expect(popup2).toHaveText("Successfully copied file/folder, redirecting...");
await page.waitForURL('**/testdata/');
await expect(page).toHaveTitle("Graham's Filebrowser - Files - testdata");
})
test("delete file", async ({ page, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="deleteme.txt"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="deleteme.txt"]').click({ button: "right" });
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="Delete"]').click();
await expect( page.locator('.card-content')).toHaveText('Are you sure you want to delete this file/folder?/deleteme.txt');
await expect(page.locator('div[aria-label="delete-path"]')).toHaveText('/deleteme.txt');
await page.locator('button[aria-label="Confirm-Delete"]').click();
const popup = page.locator('#popup-notification-content');
await popup.waitFor({ state: 'visible' });
await expect(popup).toHaveText("Deleted item successfully! reloading...");
})
test("delete nested file prompt", async ({ page, context }) => {
await page.goto("/files/folder%23hash/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - folder#hash");
await page.locator('a[aria-label="file#.sh"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="file#.sh"]').click({ button: "right" });
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="Delete"]').click();
await expect(page.locator('.card-content')).toHaveText('Are you sure you want to delete this file/folder?/folder#hash/file#.sh');
await expect(page.locator('div[aria-label="delete-path"]')).toHaveText('/folder#hash/file#.sh');
})

View File

@ -0,0 +1,14 @@
import { test, expect } from "@playwright/test";
test("navigate with hash in file name", async ({ page, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="folder#hash"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="folder#hash"]').dblclick();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - folder#hash");
await page.locator('a[aria-label="file#.sh"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="file#.sh"]').dblclick();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - file#.sh");
await expect(page.locator('.topTitle')).toHaveText('file#.sh');
})