Beta/v0.5.3 (#374)
This commit is contained in:
parent
c84a3b0d41
commit
c4ccfd48f5
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -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**:
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
[](https://hub.docker.com/r/gtstef/filebrowser)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
[](https://github.com/gtsteffaniak/filebrowser/discussions/368)
|
||||
[](https://www.paypal.com/donate/?business=W5XKNXHJM2WPE&no_recurring=0¤cy_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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.23-alpine AS base
|
||||
FROM golang:1.24-alpine AS base
|
||||
ARG VERSION
|
||||
ARG REVISION
|
||||
WORKDIR /app
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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=
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -19,7 +19,7 @@ import (
|
|||
"runtime"
|
||||
"unicode"
|
||||
|
||||
"github.com/flynn/go-shlex"
|
||||
shlex "github.com/flynn/go-shlex"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,6 +155,7 @@ func setDefaults() Settings {
|
|||
Name: "FileBrowser Quantum",
|
||||
},
|
||||
UserDefaults: UserDefaults{
|
||||
DisableOnlyOfficeExt: ".txt .csv .html",
|
||||
StickySidebar: true,
|
||||
Scope: ".",
|
||||
LockPassword: false,
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -163,4 +163,5 @@ type UserDefaults struct {
|
|||
DateFormat bool `json:"dateFormat"`
|
||||
ThemeColor string `json:"themeColor"`
|
||||
QuickDownload bool `json:"quickDownload"`
|
||||
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"`
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/asdine/storm/v3"
|
||||
storm "github.com/asdine/storm/v3"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/asdine/storm/v3"
|
||||
storm "github.com/asdine/storm/v3"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -3,7 +3,7 @@ package users
|
|||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
jwt "github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
type AuthToken struct {
|
||||
|
@ -57,12 +57,13 @@ type User struct {
|
|||
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{
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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];
|
||||
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;
|
||||
notify.showError(err.message || 'Error fetching checksum')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export function getDownloadURL(path, inline, useExternal) {
|
||||
export function getDownloadURL (path, inline, useExternal) {
|
||||
try {
|
||||
const params = {
|
||||
files: encodeURIComponent(removePrefix(decodeURI(path),"files")),
|
||||
...(inline && { inline: "true" }),
|
||||
};
|
||||
const apiPath = getApiPath("api/raw", params);
|
||||
files: encodeURIComponent(removePrefix(decodeURI(path), 'files')),
|
||||
...(inline && { inline: 'true' })
|
||||
}
|
||||
const apiPath = getApiPath('api/raw', params)
|
||||
if (externalUrl && useExternal) {
|
||||
return externalUrl+apiPath
|
||||
return externalUrl + apiPath
|
||||
}
|
||||
return window.origin+apiPath
|
||||
return window.origin + apiPath
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error getting download URL");
|
||||
throw err;
|
||||
notify.showError(err.message || 'Error getting download URL')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export function getPreviewURL(path, size, modified) {
|
||||
export function getPreviewURL (path, size, modified) {
|
||||
try {
|
||||
const params = {
|
||||
path: encodeURIComponent(removePrefix(decodeURI(path),"files")),
|
||||
path: encodeURIComponent(removePrefix(decodeURI(path), 'files')),
|
||||
size: size,
|
||||
key: Date.parse(modified),
|
||||
inline: "true",
|
||||
};
|
||||
const apiPath = getApiPath("api/preview", params);
|
||||
return window.origin+apiPath
|
||||
inline: 'true'
|
||||
}
|
||||
const apiPath = getApiPath('api/preview', params)
|
||||
return window.origin + apiPath
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error getting preview URL");
|
||||
throw err;
|
||||
notify.showError(err.message || 'Error getting preview URL')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export function getSubtitlesURL(file) {
|
||||
try {
|
||||
const subtitles = [];
|
||||
for (const sub of file.subtitles) {
|
||||
export function getSubtitlesURL (path) {
|
||||
const params = {
|
||||
inline: "true",
|
||||
path: encodeURIComponent(removePrefix(sub,"files"))
|
||||
};
|
||||
const apiPath = getApiPath("api/raw", params);
|
||||
return window.origin+apiPath
|
||||
}
|
||||
|
||||
return subtitles;
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error fetching subtitles URL");
|
||||
throw err;
|
||||
inline: true,
|
||||
files: path,
|
||||
source: 'default'
|
||||
}
|
||||
const apiPath = getApiPath('api/raw', params)
|
||||
return window.origin + apiPath
|
||||
}
|
||||
|
||||
export async function usage(source) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 += "/";
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -106,12 +106,10 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
mutations.closeHovers();
|
||||
mutations.setReload(true);
|
||||
} catch (error) {
|
||||
notify.showError(error);
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
--alt-background: #ddd;
|
||||
--surfacePrimary: gray;
|
||||
--surfaceSecondary: lightgray;
|
||||
--textPrimary: white;
|
||||
--textPrimary: #546e7a;
|
||||
--textSecondary: gray;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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}`
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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) + "/";
|
||||
|
|
|
@ -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,11 +805,11 @@ 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);
|
||||
|
||||
try {
|
||||
if (conflict) {
|
||||
mutations.showHover({
|
||||
name: "replace",
|
||||
|
@ -824,8 +823,11 @@ export default {
|
|||
await upload.handleFiles(uploadFiles, path);
|
||||
}
|
||||
mutations.setReload(true);
|
||||
} catch {
|
||||
console.log("failed to upload files")
|
||||
}
|
||||
},
|
||||
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");
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -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');
|
||||
|
||||
})
|
|
@ -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');
|
||||
})
|
Loading…
Reference in New Issue