Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef0a2e30ed | ||
|
|
2544daf03c | ||
|
|
0c00d83ec9 | ||
|
|
7b4eedc403 | ||
|
|
c3cc5399ce | ||
|
|
4aa62f4f4d | ||
|
|
01ca8e473a | ||
|
|
9d118dab68 | ||
|
|
ae89f6d2b8 | ||
|
|
c8aae3358a | ||
|
|
e8406a9892 | ||
|
|
68e654f984 | ||
|
|
b2ad68378f | ||
|
|
d26b871949 |
No files matched your search
@@ -9,7 +9,7 @@ on:
|
||||
|
||||
jobs:
|
||||
test_playwright:
|
||||
name: Test Playwright - regular
|
||||
name: Test Playwright - general
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
|
||||
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.6.1-beta
|
||||
|
||||
**New Feature**:
|
||||
- download size information is added, including when downloding multiple files in zip/tar.gz. The browser will see the XMB of X GB and will show browser native progress.
|
||||
|
||||
**BugFixes**:
|
||||
- fixed onlyoffice bug https://github.com/gtsteffaniak/filebrowser/issues/418
|
||||
- fixed breadcrumbs bug https://github.com/gtsteffaniak/filebrowser/issues/419
|
||||
- fixed search context bug https://github.com/gtsteffaniak/filebrowser/issues/417
|
||||
- fixed sessionID for search
|
||||
|
||||
## v0.6.0-beta
|
||||
|
||||
> [!WARNING]
|
||||
|
||||
@@ -24,6 +24,7 @@ WORKDIR /home/filebrowser
|
||||
RUN adduser -D -s /bin/true -u 1000 filebrowser
|
||||
USER filebrowser
|
||||
COPY --from=base --chown=filebrowser:1000 /app/filebrowser* ./
|
||||
COPY --from=base --chown=filebrowser:1000 /app/config.yaml ./
|
||||
COPY --from=nbuild --chown=filebrowser:1000 /app/dist/ ./http/dist/
|
||||
USER root
|
||||
# exposing default port for auto discovery.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
server:
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
logging:
|
||||
- levels: "info|debug|warning|error"
|
||||
sources:
|
||||
- path: "/srv"
|
||||
auth:
|
||||
|
||||
+33
-60
@@ -17,7 +17,6 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -29,11 +28,6 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
pathMutexes = make(map[string]*sync.Mutex)
|
||||
pathMutexesMu sync.Mutex // Mutex to protect the pathMutexes map
|
||||
)
|
||||
|
||||
type ItemInfo struct {
|
||||
Name string `json:"name"` // name of the file
|
||||
Size int64 `json:"size"` // length in bytes for regular files
|
||||
@@ -88,11 +82,6 @@ func FileInfoFaster(opts FileOptions) (ExtendedFileInfo, error) {
|
||||
if index == nil {
|
||||
return response, fmt.Errorf("could not get index: %v ", opts.Source)
|
||||
}
|
||||
opts.Path = index.makeIndexPath(opts.Path)
|
||||
// Lock access for the specific path
|
||||
pathMutex := getMutex(opts.Path)
|
||||
pathMutex.Lock()
|
||||
defer pathMutex.Unlock()
|
||||
|
||||
realPath, isDir, err := index.GetRealPath(opts.Path)
|
||||
if err != nil {
|
||||
@@ -118,7 +107,6 @@ func FileInfoFaster(opts FileOptions) (ExtendedFileInfo, error) {
|
||||
// }
|
||||
// return info, nil
|
||||
//}
|
||||
|
||||
err = index.RefreshFileInfo(opts)
|
||||
if err != nil {
|
||||
return response, err
|
||||
@@ -187,7 +175,7 @@ func GetChecksum(fullPath, algo string) (map[string]string, error) {
|
||||
return subs, nil
|
||||
}
|
||||
|
||||
func DeleteFiles(source, absPath string, dirPath string) error {
|
||||
func DeleteFiles(source, absPath string, absDirPath string) error {
|
||||
err := os.RemoveAll(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -196,7 +184,7 @@ func DeleteFiles(source, absPath string, dirPath string) error {
|
||||
if index == nil {
|
||||
return fmt.Errorf("could not get index: %v ", source)
|
||||
}
|
||||
refreshConfig := FileOptions{Path: dirPath, IsDir: true}
|
||||
refreshConfig := FileOptions{Path: index.MakeIndexPath(absDirPath), IsDir: true}
|
||||
err = index.RefreshFileInfo(refreshConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -204,68 +192,66 @@ func DeleteFiles(source, absPath string, dirPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func MoveResource(sourceIndex, dstIndex, realsrc, realdst string, isSrcDir bool) error {
|
||||
func MoveResource(sourceIndex, destIndex, realsrc, realdst string) error {
|
||||
err := fileutils.MoveFile(realsrc, realdst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
index := GetIndex(sourceIndex)
|
||||
if index == nil {
|
||||
idxSrc := GetIndex(sourceIndex)
|
||||
if idxSrc == nil {
|
||||
return fmt.Errorf("could not get index: %v ", sourceIndex)
|
||||
}
|
||||
idxDst := GetIndex(destIndex)
|
||||
if idxDst == nil {
|
||||
return fmt.Errorf("could not get index: %v ", sourceIndex)
|
||||
}
|
||||
refreshSourceDir := idxSrc.MakeIndexPath(filepath.Dir(realsrc))
|
||||
refreshDestDir := idxDst.MakeIndexPath(filepath.Dir(realdst))
|
||||
// refresh info for source and dest
|
||||
err = index.RefreshFileInfo(FileOptions{
|
||||
Path: realsrc,
|
||||
IsDir: isSrcDir,
|
||||
err = idxSrc.RefreshFileInfo(FileOptions{
|
||||
Path: refreshSourceDir,
|
||||
IsDir: true,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not refresh index for source: %v", err)
|
||||
}
|
||||
refreshConfig := FileOptions{Path: realdst, IsDir: true}
|
||||
if !isSrcDir {
|
||||
refreshConfig.Path = filepath.Dir(realdst)
|
||||
if refreshSourceDir == refreshDestDir {
|
||||
return nil
|
||||
}
|
||||
index = GetIndex(dstIndex)
|
||||
if index == nil {
|
||||
return fmt.Errorf("could not get index: %v ", dstIndex)
|
||||
}
|
||||
err = index.RefreshFileInfo(refreshConfig)
|
||||
refreshConfig := FileOptions{Path: refreshDestDir, IsDir: true}
|
||||
err = idxDst.RefreshFileInfo(refreshConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not refresh index for dest: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CopyResource(sourceIndex, dstIndex, realsrc, realdst string, isSrcDir bool) error {
|
||||
func CopyResource(sourceIndex, destIndex, realsrc, realdst string) error {
|
||||
err := fileutils.CopyFile(realsrc, realdst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idxSrc := GetIndex(sourceIndex)
|
||||
if idxSrc == nil {
|
||||
return fmt.Errorf("could not get index: %v ", sourceIndex)
|
||||
}
|
||||
idxDst := GetIndex(destIndex)
|
||||
if idxDst == nil {
|
||||
return fmt.Errorf("could not get index: %v ", sourceIndex)
|
||||
}
|
||||
refreshSourceDir := idxSrc.MakeIndexPath(filepath.Dir(realsrc))
|
||||
refreshDestDir := idxDst.MakeIndexPath(filepath.Dir(realdst))
|
||||
index := GetIndex(sourceIndex)
|
||||
if index == nil {
|
||||
return fmt.Errorf("could not get index: %v ", sourceIndex)
|
||||
}
|
||||
refreshConfig := FileOptions{Path: refreshSourceDir, IsDir: true}
|
||||
// refresh info for source and dest
|
||||
err = index.RefreshFileInfo(FileOptions{
|
||||
Path: realsrc,
|
||||
IsDir: isSrcDir,
|
||||
})
|
||||
err = index.RefreshFileInfo(refreshConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not refresh index for source: %v", err)
|
||||
}
|
||||
refreshConfig := FileOptions{Path: realdst, IsDir: true}
|
||||
if !isSrcDir {
|
||||
refreshConfig.Path = filepath.Dir(realdst)
|
||||
}
|
||||
index = GetIndex(dstIndex)
|
||||
if index == nil {
|
||||
return fmt.Errorf("could not get index: %v ", dstIndex)
|
||||
}
|
||||
refreshConfig = FileOptions{Path: realdst, IsDir: true}
|
||||
if !isSrcDir {
|
||||
refreshConfig.Path = filepath.Dir(realdst)
|
||||
}
|
||||
refreshConfig.Path = refreshDestDir
|
||||
err = index.RefreshFileInfo(refreshConfig)
|
||||
if err != nil {
|
||||
return errors.ErrEmptyKey
|
||||
@@ -317,7 +303,7 @@ func WriteFile(opts FileOptions, in io.Reader) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Path = parentDir
|
||||
opts.Path = idx.MakeIndexPath(parentDir)
|
||||
opts.IsDir = true
|
||||
return idx.RefreshFileInfo(opts)
|
||||
}
|
||||
@@ -435,19 +421,6 @@ func IsSymlink(mode os.FileMode) bool {
|
||||
return mode&os.ModeSymlink != 0
|
||||
}
|
||||
|
||||
func getMutex(path string) *sync.Mutex {
|
||||
// Lock access to pathMutexes map
|
||||
pathMutexesMu.Lock()
|
||||
defer pathMutexesMu.Unlock()
|
||||
|
||||
// Create a mutex for the path if it doesn't exist
|
||||
if pathMutexes[path] == nil {
|
||||
pathMutexes[path] = &sync.Mutex{}
|
||||
}
|
||||
|
||||
return pathMutexes[path]
|
||||
}
|
||||
|
||||
func Exists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
|
||||
@@ -191,7 +191,8 @@ func (idx *Index) indexDirectory(adjustedPath string, quick, recursive bool) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (idx *Index) makeIndexPath(subPath string) string {
|
||||
// input should be non-index path.
|
||||
func (idx *Index) MakeIndexPath(subPath string) string {
|
||||
if strings.HasPrefix(subPath, "./") {
|
||||
subPath = strings.TrimPrefix(subPath, ".")
|
||||
}
|
||||
@@ -200,8 +201,8 @@ func (idx *Index) makeIndexPath(subPath string) string {
|
||||
}
|
||||
// clean path
|
||||
subPath = strings.TrimSuffix(subPath, "/")
|
||||
// remove index prefix
|
||||
adjustedPath := strings.TrimPrefix(subPath, idx.Source.Path)
|
||||
// remove index prefix
|
||||
adjustedPath = strings.ReplaceAll(adjustedPath, "\\", "/")
|
||||
// remove trailing slash
|
||||
adjustedPath = strings.TrimSuffix(adjustedPath, "/")
|
||||
@@ -251,10 +252,8 @@ func (idx *Index) RefreshFileInfo(opts FileOptions) error {
|
||||
IsDir: opts.IsDir,
|
||||
}
|
||||
if !refreshOptions.IsDir {
|
||||
refreshOptions.Path = idx.makeIndexPath(filepath.Dir(refreshOptions.Path))
|
||||
refreshOptions.Path = idx.MakeIndexPath(filepath.Dir(refreshOptions.Path))
|
||||
refreshOptions.IsDir = true
|
||||
} else {
|
||||
refreshOptions.Path = idx.makeIndexPath(refreshOptions.Path)
|
||||
}
|
||||
err := idx.indexDirectory(refreshOptions.Path, false, false)
|
||||
if err != nil {
|
||||
|
||||
@@ -94,14 +94,15 @@ func TestMakeIndexPath(t *testing.T) {
|
||||
{"Trailing slash removed", "/test/", "/test"},
|
||||
{"Subpath without root prefix", "/other/test", "/other/test"},
|
||||
{"Complex nested paths", "/nested/path", "/nested/path"},
|
||||
// TODO fix {"has source name as start", "/srv.tar.gz", "/srv.tar.gz"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
idx := &Index{Source: settings.Source{Path: "/"}}
|
||||
result := idx.makeIndexPath(tt.subPath)
|
||||
idx := &Index{Source: settings.Source{Path: "/srv"}}
|
||||
result := idx.MakeIndexPath(tt.subPath)
|
||||
if result != tt.expected {
|
||||
t.Errorf("makeIndexPath(%q)\ngot %q\nwant %q", tt.name, result, tt.expected)
|
||||
t.Errorf("MakeIndexPath(%q)\ngot %q\nwant %q", tt.name, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -120,9 +121,9 @@ func TestMakeIndexPath(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
idx := &Index{Source: settings.Source{Path: "C:\\Users"}}
|
||||
result := idx.makeIndexPath(tt.subPath)
|
||||
result := idx.MakeIndexPath(tt.subPath)
|
||||
if result != tt.expected {
|
||||
t.Errorf("makeIndexPath(%q)\ngot %q\nwant %q", tt.name, result, tt.expected)
|
||||
t.Errorf("MakeIndexPath(%q)\ngot %q\nwant %q", tt.name, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -141,9 +142,9 @@ func TestMakeIndexPathRoot(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
idx := &Index{Source: settings.Source{Path: "/rootpath", Name: "default"}}
|
||||
result := idx.makeIndexPath(tt.subPath)
|
||||
result := idx.MakeIndexPath(tt.subPath)
|
||||
if result != tt.expected {
|
||||
t.Errorf("makeIndexPath(%q)\ngot %q\nwant %q", tt.name, result, tt.expected)
|
||||
t.Errorf("MakeIndexPath(%q)\ngot %q\nwant %q", tt.name, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,8 +22,11 @@ type SearchResult struct {
|
||||
}
|
||||
|
||||
func (idx *Index) Search(search string, scope string, sourceSession string) []SearchResult {
|
||||
scope = strings.TrimSuffix(scope, "/")
|
||||
if search == "" {
|
||||
scope = ""
|
||||
}
|
||||
// Remove slashes
|
||||
scope = idx.makeIndexPath(scope)
|
||||
runningHash := utils.InsecureRandomIdentifier(4)
|
||||
sessionInProgress.Store(sourceSession, runningHash) // Store the value in the sync.Map
|
||||
searchOptions := ParseSearch(search)
|
||||
@@ -46,7 +49,6 @@ func (idx *Index) Search(search string, scope string, sourceSession string) []Se
|
||||
}
|
||||
idx.mu.Lock()
|
||||
for _, dirName := range directories {
|
||||
scopedPath := strings.TrimPrefix(strings.TrimPrefix(dirName, scope), "/") + "/"
|
||||
idx.mu.Unlock()
|
||||
dir, found := idx.GetReducedMetadata(dirName, true)
|
||||
idx.mu.Lock()
|
||||
@@ -63,16 +65,12 @@ func (idx *Index) Search(search string, scope string, sourceSession string) []Se
|
||||
}
|
||||
matches := reducedDir.containsSearchTerm(searchTerm, searchOptions)
|
||||
if matches {
|
||||
results[scopedPath] = SearchResult{Path: scopedPath, Type: "directory", Size: dir.Size}
|
||||
results[dirName+"/"] = SearchResult{Path: dirName + "/", Type: "directory", Size: dir.Size}
|
||||
count++
|
||||
}
|
||||
// search files first
|
||||
for _, item := range dir.Files {
|
||||
fullPath := dirName + "/" + item.Name
|
||||
scopedPath := strings.TrimPrefix(strings.TrimPrefix(fullPath, scope), "/")
|
||||
if item.Type == "directory" {
|
||||
scopedPath += "/"
|
||||
}
|
||||
value, found := sessionInProgress.Load(sourceSession)
|
||||
if !found || value != runningHash {
|
||||
idx.mu.Unlock()
|
||||
@@ -83,7 +81,7 @@ func (idx *Index) Search(search string, scope string, sourceSession string) []Se
|
||||
}
|
||||
matches := item.containsSearchTerm(searchTerm, searchOptions)
|
||||
if matches {
|
||||
results[scopedPath] = SearchResult{Path: scopedPath, Type: item.Type, Size: item.Size}
|
||||
results[fullPath] = SearchResult{Path: fullPath, Type: item.Type, Size: item.Size}
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,11 @@ func TestSearchIndexes(t *testing.T) {
|
||||
{Name: "video.mp4", Type: "video"},
|
||||
{Name: "video.MP4", Type: "video"},
|
||||
}},
|
||||
"/first Dir": {
|
||||
Files: []ItemInfo{
|
||||
{Name: "space jam.zip", Size: 100, Type: "archive"},
|
||||
},
|
||||
},
|
||||
"/new/test/path": {Files: []ItemInfo{{Name: "archive.zip", Type: "archive"}}},
|
||||
"/firstDir": {
|
||||
Files: []ItemInfo{
|
||||
@@ -127,7 +132,7 @@ func TestSearchIndexes(t *testing.T) {
|
||||
scope: "/new/",
|
||||
expectedResult: []SearchResult{
|
||||
{
|
||||
Path: "test/audio.wav",
|
||||
Path: "/new/test/audio.wav",
|
||||
Type: "audio",
|
||||
Size: 0,
|
||||
},
|
||||
@@ -138,12 +143,12 @@ func TestSearchIndexes(t *testing.T) {
|
||||
scope: "/",
|
||||
expectedResult: []SearchResult{
|
||||
{
|
||||
Path: "test/",
|
||||
Path: "/test/",
|
||||
Type: "directory",
|
||||
Size: 0,
|
||||
},
|
||||
{
|
||||
Path: "new/test/",
|
||||
Path: "/new/test/",
|
||||
Type: "directory",
|
||||
Size: 0,
|
||||
},
|
||||
@@ -154,12 +159,12 @@ func TestSearchIndexes(t *testing.T) {
|
||||
scope: "/",
|
||||
expectedResult: []SearchResult{
|
||||
{
|
||||
Path: "firstDir/archive.zip",
|
||||
Path: "/firstDir/archive.zip",
|
||||
Type: "archive",
|
||||
Size: 100,
|
||||
},
|
||||
{
|
||||
Path: "new/test/path/archive.zip",
|
||||
Path: "/new/test/path/archive.zip",
|
||||
Type: "archive",
|
||||
Size: 0,
|
||||
},
|
||||
@@ -170,7 +175,18 @@ func TestSearchIndexes(t *testing.T) {
|
||||
scope: "/firstDir",
|
||||
expectedResult: []SearchResult{
|
||||
{
|
||||
Path: "archive.zip",
|
||||
Path: "/firstDir/archive.zip",
|
||||
Type: "archive",
|
||||
Size: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
search: "space jam",
|
||||
scope: "/first Dir/",
|
||||
expectedResult: []SearchResult{
|
||||
{
|
||||
Path: "/first Dir/space jam.zip",
|
||||
Type: "archive",
|
||||
Size: 100,
|
||||
},
|
||||
@@ -181,7 +197,7 @@ func TestSearchIndexes(t *testing.T) {
|
||||
scope: "/",
|
||||
expectedResult: []SearchResult{
|
||||
{
|
||||
Path: "firstDir/thisIsDir/",
|
||||
Path: "/firstDir/thisIsDir/",
|
||||
Type: "directory",
|
||||
Size: 2097152,
|
||||
},
|
||||
@@ -192,7 +208,7 @@ func TestSearchIndexes(t *testing.T) {
|
||||
scope: "/",
|
||||
expectedResult: []SearchResult{
|
||||
{
|
||||
Path: "firstDir/thisIsDir/",
|
||||
Path: "/firstDir/thisIsDir/",
|
||||
Type: "directory",
|
||||
Size: 2097152,
|
||||
},
|
||||
@@ -203,12 +219,12 @@ func TestSearchIndexes(t *testing.T) {
|
||||
scope: "/",
|
||||
expectedResult: []SearchResult{
|
||||
{
|
||||
Path: "new/test/video.MP4",
|
||||
Path: "/new/test/video.MP4",
|
||||
Type: "video",
|
||||
Size: 0,
|
||||
},
|
||||
{
|
||||
Path: "new/test/video.mp4",
|
||||
Path: "/new/test/video.mp4",
|
||||
Type: "video",
|
||||
Size: 0,
|
||||
},
|
||||
|
||||
+12
-6
@@ -15,13 +15,16 @@ func (idx *Index) UpdateMetadata(info *FileInfo) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetMetadataInfo retrieves the FileInfo from the specified directory in the index.
|
||||
// GetMetadataInfo retrieves the FileInfo from the specified file or directory in the index.
|
||||
func (idx *Index) GetReducedMetadata(target string, isDir bool) (*FileInfo, bool) {
|
||||
idx.mu.Lock()
|
||||
defer idx.mu.Unlock()
|
||||
checkDir := idx.makeIndexPath(target)
|
||||
checkDir := idx.MakeIndexPath(target)
|
||||
if !isDir {
|
||||
checkDir = idx.makeIndexPath(filepath.Dir(target))
|
||||
checkDir = idx.MakeIndexPath(filepath.Dir(target))
|
||||
}
|
||||
if checkDir == "" {
|
||||
checkDir = "/"
|
||||
}
|
||||
dir, exists := idx.Directories[checkDir]
|
||||
if !exists {
|
||||
@@ -48,13 +51,16 @@ func (idx *Index) GetReducedMetadata(target string, isDir bool) (*FileInfo, bool
|
||||
|
||||
}
|
||||
|
||||
// GetMetadataInfo retrieves the FileInfo from the specified directory in the index.
|
||||
// raw directory info retrieval -- does not work on files, only returns a directory
|
||||
func (idx *Index) GetMetadataInfo(target string, isDir bool) (*FileInfo, bool) {
|
||||
idx.mu.RLock()
|
||||
defer idx.mu.RUnlock()
|
||||
checkDir := idx.makeIndexPath(target)
|
||||
checkDir := idx.MakeIndexPath(target)
|
||||
if !isDir {
|
||||
checkDir = idx.makeIndexPath(filepath.Dir(target))
|
||||
checkDir = idx.MakeIndexPath(filepath.Dir(target))
|
||||
}
|
||||
if checkDir == "" {
|
||||
checkDir = "/"
|
||||
}
|
||||
dir, exists := idx.Directories[checkDir]
|
||||
return dir, exists
|
||||
|
||||
@@ -140,7 +140,7 @@ func TestUpdateFileMetadata(t *testing.T) {
|
||||
|
||||
fileInfo, exists := index.GetReducedMetadata("/testpath/testfile.txt", false)
|
||||
if !exists || fileInfo.Name != "testfile.txt" {
|
||||
t.Fatalf("expected testfile.txt to be updated in the directory metadata:%v %v", exists, info.Name)
|
||||
t.Fatalf("expected '%v' to exist in the directory metadata. %v ", info.Name, exists)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,17 +22,12 @@ func MoveFile(src, dst string) error {
|
||||
// fallback
|
||||
err = CopyFile(src, dst)
|
||||
if err != nil {
|
||||
go func() {
|
||||
err = os.Remove(src)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("os.Remove failed %v %v ", src, err))
|
||||
}
|
||||
}()
|
||||
logger.Error(fmt.Sprintf("CopyFile failed %v %v %v ", src, dst, err))
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
err = os.Remove(src)
|
||||
err = os.RemoveAll(src)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("os.Remove failed %v %v ", src, err))
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ func wrapHandler(fn handleFunc) http.HandlerFunc {
|
||||
|
||||
// Write the JSON error response
|
||||
if _, writeErr := w.Write(errorBytes); writeErr != nil {
|
||||
logger.Error(fmt.Sprintf("Error writing error response: %v", writeErr))
|
||||
logger.Debug(fmt.Sprintf("Error writing error response: %v", writeErr))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
+11
-13
@@ -37,24 +37,21 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
if settings.Config.Integrations.OnlyOffice.Url == "" {
|
||||
return http.StatusInternalServerError, errors.New("only-office integration must be configured in settings")
|
||||
}
|
||||
|
||||
if !d.user.Perm.Modify {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
encodedUrl := r.URL.Query().Get("url")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = settings.Config.Server.DefaultSource.Name
|
||||
}
|
||||
// Decode the URL-encoded path
|
||||
url, err := url.QueryUnescape(encodedUrl)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
|
||||
// get path from url
|
||||
pathParts := strings.Split(url, "/api/raw?files=/")
|
||||
path := pathParts[len(pathParts)-1]
|
||||
pathParts := strings.Split(url, "/api/raw?files=")
|
||||
sourceFile := pathParts[len(pathParts)-1]
|
||||
sourceSplit := strings.Split(sourceFile, "::")
|
||||
if len(sourceSplit) != 2 {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid url path %v", url)
|
||||
}
|
||||
source := sourceSplit[0]
|
||||
path := sourceSplit[1]
|
||||
urlFirst := pathParts[0]
|
||||
if settings.Config.Server.InternalUrl != "" {
|
||||
urlFirst = strings.TrimSuffix(settings.Config.Server.InternalUrl, "/")
|
||||
@@ -87,7 +84,7 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
if d.user.DarkMode {
|
||||
theme = "dark"
|
||||
}
|
||||
|
||||
callbackURL := fmt.Sprintf("%v/api/onlyoffice/callback?path=%v&auth=%v", urlFirst, path, d.token)
|
||||
clientConfig := map[string]interface{}{
|
||||
"document": map[string]interface{}{
|
||||
"fileType": fileType,
|
||||
@@ -101,7 +98,7 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
},
|
||||
},
|
||||
"editorConfig": map[string]interface{}{
|
||||
"callbackUrl": fmt.Sprintf("%v/api/onlyoffice/callback?path=%v&auth=%v", urlFirst, path, d.token),
|
||||
"callbackUrl": callbackURL,
|
||||
"user": map[string]interface{}{
|
||||
"id": strconv.FormatUint(uint64(d.user.ID), 10),
|
||||
"name": d.user.Username,
|
||||
@@ -201,6 +198,7 @@ func getOnlyOfficeId(source, path string) (string, error) {
|
||||
}
|
||||
return "", fmt.Errorf("document key not found")
|
||||
}
|
||||
|
||||
func deleteOfficeId(source, path string) {
|
||||
idx := files.GetIndex(source)
|
||||
if idx == nil {
|
||||
|
||||
+83
-16
@@ -148,7 +148,7 @@ func addSingleFile(realPath, archivePath string, zipWriter *zip.Writer, tarWrite
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
info, err := os.Stat(realPath)
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -158,7 +158,6 @@ func addSingleFile(realPath, archivePath string, zipWriter *zip.Writer, tarWrite
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Ensure correct relative path
|
||||
header.Name = filepath.ToSlash(archivePath)
|
||||
if err = tarWriter.WriteHeader(header); err != nil {
|
||||
return err
|
||||
@@ -206,29 +205,45 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Compute estimated download size
|
||||
estimatedSize, err := computeArchiveSize(fileList, d)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// ** Single file download with Content-Length **
|
||||
if len(fileList) == 1 && !isDir {
|
||||
fd, err2 := os.Open(realPath)
|
||||
if err2 != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
return http.StatusInternalServerError, err2
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
// Get file information
|
||||
fileInfo, err3 := fd.Stat()
|
||||
if err3 != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
// Get file size
|
||||
fileInfo, err2 := fd.Stat()
|
||||
if err2 != nil {
|
||||
return http.StatusInternalServerError, err2
|
||||
}
|
||||
|
||||
// Set headers and serve the file
|
||||
// Set headers
|
||||
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)
|
||||
return 0, nil
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))
|
||||
// if larger than 100 megabytes, log it
|
||||
if estimatedSize > 100*1024/1024 {
|
||||
logger.Debug(fmt.Sprintf("User %v is downloading large (%d MB) file: %v", d.user.Username, estimatedSize/1024/1024, fileName))
|
||||
}
|
||||
// Stream file to response
|
||||
_, err2 = io.Copy(w, fd)
|
||||
if err2 != nil {
|
||||
return http.StatusInternalServerError, err2
|
||||
}
|
||||
return 200, nil
|
||||
}
|
||||
|
||||
// ** Archive (ZIP/TAR.GZ) handling **
|
||||
algo := r.URL.Query().Get("algo")
|
||||
var extension string
|
||||
switch algo {
|
||||
@@ -239,6 +254,7 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
default:
|
||||
return http.StatusInternalServerError, errors.New("format not implemented")
|
||||
}
|
||||
|
||||
baseDirName := filepath.Base(filepath.Dir(realPath))
|
||||
if baseDirName == "" || baseDirName == "/" {
|
||||
baseDirName = "download"
|
||||
@@ -246,15 +262,21 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
if len(fileList) == 1 && isDir {
|
||||
baseDirName = filepath.Base(realPath)
|
||||
}
|
||||
downloadFileName := url.PathEscape(baseDirName + extension)
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+downloadFileName)
|
||||
// Create the archive and stream it directly to the response
|
||||
fileName = url.PathEscape(baseDirName + extension)
|
||||
|
||||
// Set headers for archive
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+fileName)
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", estimatedSize))
|
||||
// if larger than 100 megabytes, log it
|
||||
if estimatedSize > 100*1024/1024 {
|
||||
logger.Debug(fmt.Sprintf("User %v is downloading large (%d MB) file: %v", d.user.Username, estimatedSize/1024/1024, fileName))
|
||||
}
|
||||
// Create archive and stream it
|
||||
if extension == ".zip" {
|
||||
err = createZip(w, d, fileList...)
|
||||
} else {
|
||||
err = createTarGz(w, d, fileList...)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -262,9 +284,43 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func computeArchiveSize(fileList []string, d *requestContext) (int64, error) {
|
||||
var estimatedSize int64
|
||||
for _, fname := range fileList {
|
||||
splitFile := strings.Split(fname, "::")
|
||||
if len(splitFile) != 2 {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid file in files request: %v", fileList[0])
|
||||
}
|
||||
source := splitFile[0]
|
||||
path := splitFile[1]
|
||||
idx := files.GetIndex(source)
|
||||
if idx == nil {
|
||||
return 0, fmt.Errorf("source %s is not available", source)
|
||||
}
|
||||
userScope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
|
||||
if d.share == nil && err != nil {
|
||||
return 0, fmt.Errorf("source %s is not available for user %s", source, d.user.Username)
|
||||
}
|
||||
_, isDir, err := idx.GetRealPath(userScope, path)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
info, ok := idx.GetReducedMetadata(path, isDir)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("failed to get metadata info for %s", path)
|
||||
}
|
||||
estimatedSize += info.Size
|
||||
}
|
||||
return estimatedSize, nil
|
||||
}
|
||||
|
||||
func createZip(w io.Writer, d *requestContext, filenames ...string) error {
|
||||
zipWriter := zip.NewWriter(w)
|
||||
defer zipWriter.Close()
|
||||
// Stream the response to avoid high memory usage
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
|
||||
// Check if we have exactly one directory
|
||||
//flatten := len(filenames) == 1
|
||||
@@ -274,6 +330,10 @@ func createZip(w io.Writer, d *requestContext, filenames ...string) error {
|
||||
logger.Error(fmt.Sprintf("Failed to add %s to ZIP: %v", fname, err))
|
||||
return err
|
||||
}
|
||||
// Flush after adding each file
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -285,6 +345,10 @@ func createTarGz(w io.Writer, d *requestContext, filenames ...string) error {
|
||||
tarWriter := tar.NewWriter(gzWriter)
|
||||
defer tarWriter.Close()
|
||||
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
|
||||
// Check if we have exactly one directory
|
||||
//flatten := len(filenames) == 1
|
||||
for _, fname := range filenames {
|
||||
@@ -293,6 +357,9 @@ func createTarGz(w io.Writer, d *requestContext, filenames ...string) error {
|
||||
logger.Error(fmt.Sprintf("Failed to add %s to TAR.GZ: %v", fname, err))
|
||||
return err
|
||||
}
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -114,13 +114,12 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
fileOpts := files.FileOptions{
|
||||
fileInfo, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: filepath.Join(userscope, path),
|
||||
Source: source,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(fileOpts)
|
||||
})
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
@@ -277,7 +276,6 @@ func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
// TODO source := r.URL.Query().Get("source")
|
||||
action := r.URL.Query().Get("action")
|
||||
|
||||
if !d.user.Perm.Modify {
|
||||
return http.StatusForbidden, fmt.Errorf("user is not allowed to create or modify")
|
||||
}
|
||||
@@ -384,17 +382,14 @@ func delThumbs(ctx context.Context, fileCache FileCache, file files.ExtendedFile
|
||||
func patchAction(ctx context.Context, action, src, dst string, d *requestContext, fileCache FileCache, isSrcDir bool, srcIndex, destIndex string) error {
|
||||
switch action {
|
||||
case "copy":
|
||||
if !d.user.Perm.Modify {
|
||||
return errors.ErrPermissionDenied
|
||||
}
|
||||
err := files.CopyResource(srcIndex, destIndex, src, dst, isSrcDir)
|
||||
err := files.CopyResource(srcIndex, destIndex, src, dst)
|
||||
return err
|
||||
case "rename", "move":
|
||||
if !d.user.Perm.Modify {
|
||||
return errors.ErrPermissionDenied
|
||||
}
|
||||
idx := files.GetIndex(srcIndex)
|
||||
srcPath := idx.MakeIndexPath(src)
|
||||
fmt.Println("srcPath", srcPath)
|
||||
fileInfo, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: src,
|
||||
Path: srcPath,
|
||||
Source: srcIndex,
|
||||
IsDir: isSrcDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
@@ -408,7 +403,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *requestContext
|
||||
|
||||
// delete thumbnails
|
||||
delThumbs(ctx, fileCache, fileInfo)
|
||||
return files.MoveResource(srcIndex, destIndex, src, dst, isSrcDir)
|
||||
return files.MoveResource(srcIndex, destIndex, src, dst)
|
||||
default:
|
||||
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
|
||||
}
|
||||
|
||||
+10
-6
@@ -3,6 +3,8 @@ package http
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
@@ -59,8 +61,12 @@ func searchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (i
|
||||
if source == "" {
|
||||
source = config.Server.DefaultSource.Name
|
||||
}
|
||||
searchScope := strings.TrimPrefix(r.URL.Query().Get("scope"), ".")
|
||||
searchScope = strings.TrimPrefix(searchScope, "/")
|
||||
scope := r.URL.Query().Get("scope")
|
||||
unencodedScope, err := url.QueryUnescape(scope)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
searchScope := strings.TrimPrefix(unencodedScope, ".")
|
||||
// Retrieve the User-Agent and X-Auth headers from the request
|
||||
sessionId := r.Header.Get("SessionId")
|
||||
index := files.GetIndex(source)
|
||||
@@ -71,11 +77,9 @@ func searchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (i
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
userScope := strings.TrimPrefix(userscope, ".")
|
||||
combinedScope := strings.TrimPrefix(userScope+"/"+searchScope, "/")
|
||||
|
||||
combinedPath := index.MakeIndexPath(filepath.Join(userscope, searchScope))
|
||||
// Perform the search using the provided query and user scope
|
||||
response := index.Search(query, combinedScope, sessionId)
|
||||
response := index.Search(query, combinedPath, sessionId)
|
||||
// Set the Content-Type header to application/json
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
return renderJSON(w, r, response)
|
||||
|
||||
@@ -65,6 +65,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
|
||||
"ExternalLinks": config.Frontend.ExternalLinks,
|
||||
"ExternalUrl": strings.TrimSuffix(config.Server.ExternalUrl, "/"),
|
||||
"OnlyOfficeUrl": settings.Config.Integrations.OnlyOffice.Url,
|
||||
"SourceCount": len(config.Server.Sources),
|
||||
}
|
||||
|
||||
if config.Frontend.Files != "" {
|
||||
|
||||
@@ -47,10 +47,13 @@ func ApplyUserDefaults(u *users.User) {
|
||||
u.Perm = Config.UserDefaults.Perm
|
||||
u.ShowHidden = Config.UserDefaults.ShowHidden
|
||||
u.DateFormat = Config.UserDefaults.DateFormat
|
||||
u.Scopes = []users.SourceScope{
|
||||
{
|
||||
Scope: Config.Server.DefaultSource.Config.DefaultUserScope,
|
||||
Name: Config.Server.DefaultSource.Path,
|
||||
},
|
||||
if len(u.Scopes) == 0 {
|
||||
u.Scopes = []users.SourceScope{
|
||||
{
|
||||
Scope: Config.Server.DefaultSource.Config.DefaultUserScope,
|
||||
Name: Config.Server.DefaultSource.Path,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -96,11 +96,9 @@ func quickSetup(store *Storage) {
|
||||
Scope: "",
|
||||
})
|
||||
}
|
||||
user.DarkMode = true
|
||||
user.ViewMode = "normal"
|
||||
user.LockPassword = false
|
||||
user.Perm = settings.AdminPerms()
|
||||
logger.Debug(fmt.Sprintf("Creating user as admin: %v %v\n", user.Username, user.Password))
|
||||
logger.Debug(fmt.Sprintf("Creating user as admin: %v %v", user.Username, user.Password))
|
||||
err = store.Users.Save(user, true)
|
||||
utils.CheckErr("store.Users.Save", err)
|
||||
}
|
||||
@@ -112,6 +110,7 @@ func CreateUser(userInfo users.User, asAdmin bool) error {
|
||||
if userInfo.Username == "" || userInfo.Password == "" {
|
||||
return errors.ErrInvalidRequestParams
|
||||
}
|
||||
logger.Debug(fmt.Sprintf("Creating user: %v %v\n", userInfo.Username, userInfo.Scopes))
|
||||
settings.ApplyUserDefaults(newUser)
|
||||
if asAdmin {
|
||||
userInfo.Perm = settings.AdminPerms()
|
||||
|
||||
@@ -11,7 +11,6 @@ export default {
|
||||
computed: {},
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
console.log("static vars", window.FileBrowser);
|
||||
mutations.setLoading("main-app", false);
|
||||
// Query the loading element and remove it from the DOM
|
||||
const loadingDiv = document.getElementById("loading");
|
||||
|
||||
+11
-11
@@ -5,7 +5,7 @@ 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 {
|
||||
const result = extractSourceFromPath(url)
|
||||
const apiPath = getApiPath('api/resources', {
|
||||
@@ -23,7 +23,7 @@ export async function fetchFiles (url, content = false) {
|
||||
}
|
||||
}
|
||||
|
||||
async function resourceAction (url, method, content) {
|
||||
async function resourceAction(url, method, content) {
|
||||
try {
|
||||
const result = extractSourceFromPath(url)
|
||||
let source = result.source
|
||||
@@ -42,7 +42,7 @@ async function resourceAction (url, method, content) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove (url) {
|
||||
export async function remove(url) {
|
||||
try {
|
||||
return await resourceAction(url, 'DELETE')
|
||||
} catch (err) {
|
||||
@@ -51,7 +51,7 @@ export async function remove (url) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function put (url, content = '') {
|
||||
export async function put(url, content = '') {
|
||||
try {
|
||||
return await resourceAction(url, 'PUT', content)
|
||||
} catch (err) {
|
||||
@@ -60,7 +60,7 @@ export async function put (url, content = '') {
|
||||
}
|
||||
}
|
||||
|
||||
export function download (format, files) {
|
||||
export function download(format, files) {
|
||||
if (format !== 'zip') {
|
||||
format = 'tar.gz'
|
||||
}
|
||||
@@ -94,7 +94,7 @@ export function download (format, files) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function post (url, content = '', overwrite = false, onupload) {
|
||||
export async function post(url, content = '', overwrite = false, onupload) {
|
||||
try {
|
||||
const result = extractSourceFromPath(url)
|
||||
let bufferContent
|
||||
@@ -143,7 +143,7 @@ export async function post (url, content = '', overwrite = false, onupload) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function moveCopy (
|
||||
export async function moveCopy(
|
||||
items,
|
||||
action = 'copy',
|
||||
overwrite = false,
|
||||
@@ -186,7 +186,7 @@ export async function moveCopy (
|
||||
}
|
||||
}
|
||||
|
||||
export async function checksum (url, algo) {
|
||||
export async function checksum(url, algo) {
|
||||
try {
|
||||
const result = extractSourceFromPath(url)
|
||||
const params = {
|
||||
@@ -204,7 +204,7 @@ export async function checksum (url, algo) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getDownloadURL (source, path, inline, useExternal) {
|
||||
export function getDownloadURL(source, path, inline, useExternal) {
|
||||
try {
|
||||
const params = {
|
||||
files: source + '::' + encodeURIComponent(path),
|
||||
@@ -221,7 +221,7 @@ export function getDownloadURL (source, path, inline, useExternal) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getPreviewURL (source, path, size, modified) {
|
||||
export function getPreviewURL(source, path, size, modified) {
|
||||
try {
|
||||
const params = {
|
||||
path: encodeURIComponent(path),
|
||||
@@ -238,7 +238,7 @@ export function getPreviewURL (source, path, size, modified) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function usage (source) {
|
||||
export async function usage(source) {
|
||||
try {
|
||||
const apiPath = getApiPath('api/usage', { source: source })
|
||||
const res = await fetchURL(apiPath)
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { fetchURL } from "./utils";
|
||||
import { notify } from "@/notify"; // Import notify for error handling
|
||||
import { removePrefix, getApiPath } from "@/utils/url.js";
|
||||
import { getApiPath } from "@/utils/url.js";
|
||||
|
||||
export default async function search(base, source, query) {
|
||||
try {
|
||||
base = removePrefix(base,"files");
|
||||
query = encodeURIComponent(query);
|
||||
|
||||
if (!base.endsWith("/")) {
|
||||
base += "/";
|
||||
}
|
||||
|
||||
const apiPath = getApiPath("api/search", { scope: base, query: query, source: source });
|
||||
const res = await fetchURL(apiPath);
|
||||
let data = await res.json();
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
<template>
|
||||
<div class="breadcrumbs">
|
||||
<component
|
||||
:is="element"
|
||||
:to="homePath"
|
||||
:aria-label="$t('files.home')"
|
||||
:title="$t('files.home')"
|
||||
>
|
||||
<router-link :to="base" :aria-label="$t('files.home')" :title="$t('files.home')">
|
||||
<i class="material-icons">home</i>
|
||||
</component>
|
||||
</router-link>
|
||||
|
||||
<span v-for="(link, index) in items" :key="index">
|
||||
<span
|
||||
:aria-label="'breadcrumb-link-' + link.name"
|
||||
v-for="(link, index) in items"
|
||||
:key="index"
|
||||
>
|
||||
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
|
||||
<component :is="element" :to="link.url">{{ link.name }}</component>
|
||||
<router-link :to="link.url">{{ link.name }}</router-link>
|
||||
</span>
|
||||
<action style="display: contents" v-if="showShare" icon="share" show="share" />
|
||||
<div v-if="isCardView">
|
||||
@@ -32,6 +31,7 @@
|
||||
|
||||
<script>
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import { baseURL } from "@/utils/constants.js";
|
||||
import { extractSourceFromPath, removePrefix, removeLeadingSlash } from "@/utils/url.js";
|
||||
import Action from "@/components/Action.vue";
|
||||
|
||||
@@ -43,11 +43,11 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
gallerySize: state.user.gallerySize,
|
||||
homePath: "/files/",
|
||||
base: "/files/",
|
||||
path: "",
|
||||
};
|
||||
},
|
||||
props: ["base", "noLink"],
|
||||
props: ["noLink"],
|
||||
mounted() {
|
||||
this.updatePaths();
|
||||
},
|
||||
@@ -67,38 +67,24 @@ export default {
|
||||
}
|
||||
|
||||
let breadcrumbs = [];
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i === 0) {
|
||||
breadcrumbs.push({
|
||||
name: decodeURIComponent(parts[i]),
|
||||
url: this.base + "/" + parts[i] + "/",
|
||||
});
|
||||
} else {
|
||||
breadcrumbs.push({
|
||||
name: decodeURIComponent(parts[i]),
|
||||
url: breadcrumbs[i - 1].url + parts[i] + "/",
|
||||
});
|
||||
}
|
||||
}
|
||||
let buildRef = this.base;
|
||||
parts.forEach((element) => {
|
||||
buildRef = buildRef + encodeURIComponent(element) + "/";
|
||||
breadcrumbs.push({
|
||||
name: decodeURIComponent(element),
|
||||
url: buildRef,
|
||||
});
|
||||
});
|
||||
|
||||
if (breadcrumbs.length > 3) {
|
||||
while (breadcrumbs.length !== 4) {
|
||||
breadcrumbs.shift();
|
||||
}
|
||||
|
||||
breadcrumbs[0].name = "...";
|
||||
}
|
||||
|
||||
return breadcrumbs;
|
||||
},
|
||||
element() {
|
||||
if (this.noLink !== undefined) {
|
||||
return "span";
|
||||
}
|
||||
|
||||
return "router-link";
|
||||
},
|
||||
showShare() {
|
||||
return (
|
||||
state.user?.perm &&
|
||||
@@ -111,13 +97,15 @@ export default {
|
||||
methods: {
|
||||
updatePaths() {
|
||||
const result = extractSourceFromPath(getters.routePath());
|
||||
this.path = removePrefix(result.path, "files");
|
||||
if (getters.currentView() == "share") {
|
||||
this.homePath = getters.sharePathBase();
|
||||
this.path = removePrefix(getters.routePath(), this.homePath + "/");
|
||||
this.base = getters.sharePathBase();
|
||||
this.path = removePrefix(getters.routePath(), this.base + "/");
|
||||
} else {
|
||||
this.path = decodeURIComponent(result.path);
|
||||
}
|
||||
if (state.sources.count > 1) {
|
||||
this.homePath = "/files/" + result.source;
|
||||
this.base = baseURL;
|
||||
if (state.serverHasMultipleSources) {
|
||||
this.base = `${this.base}${result.source}/`;
|
||||
}
|
||||
},
|
||||
updateGallerySize(event) {
|
||||
|
||||
@@ -232,7 +232,7 @@ export default {
|
||||
mutations.closeHovers();
|
||||
return;
|
||||
}
|
||||
if (state.sources.count > 1) {
|
||||
if (state.serverHasMultipleSources) {
|
||||
this.selectedSource = state.sources.current;
|
||||
}
|
||||
setTimeout(() => {
|
||||
@@ -248,7 +248,7 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (state.sources.count > 1) {
|
||||
if (state.serverHasMultipleSources) {
|
||||
this.selectedSource = state.sources.current;
|
||||
}
|
||||
},
|
||||
@@ -320,11 +320,13 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
updateSource(event) {
|
||||
this.selectedSource = event.target.value; // Correct way to get the new value
|
||||
this.selectedSource = event.target.value;
|
||||
this.submit();
|
||||
},
|
||||
getRelative(path) {
|
||||
return "/files/" + this.selectedSource + "/" + path;
|
||||
path = path.slice(1); // remove leading slash
|
||||
const fullpath = "/files/" + this.selectedSource + "/" + encodeURIComponent(path);
|
||||
return fullpath;
|
||||
},
|
||||
getIcon(mimetype) {
|
||||
return getMaterialIconForType(mimetype);
|
||||
@@ -433,7 +435,11 @@ export default {
|
||||
if (source == "") {
|
||||
source = state.sources.current;
|
||||
}
|
||||
this.results = await search(this.getContext, source, searchTypesFull + this.value);
|
||||
this.results = await search(
|
||||
encodeURIComponent(this.getContext),
|
||||
source,
|
||||
searchTypesFull + this.value
|
||||
);
|
||||
|
||||
this.ongoing = false;
|
||||
if (this.results.length == 0) {
|
||||
|
||||
@@ -158,7 +158,7 @@ export default {
|
||||
return this.selected.indexOf(this.index) !== -1;
|
||||
},
|
||||
isDraggable() {
|
||||
return this.readOnly == undefined && state.user.perm?.rename;
|
||||
return this.readOnly == undefined && state.user.perm?.modify;
|
||||
},
|
||||
canDrop() {
|
||||
if (!this.isDir || this.readOnly !== undefined) return false;
|
||||
@@ -326,7 +326,7 @@ export default {
|
||||
name: state.req.items[i].name,
|
||||
});
|
||||
}
|
||||
let response = await filesApi.fetchFiles(el.__vue__.url);
|
||||
let response = await filesApi.fetchFiles(decodeURIComponent(el.__vue__.url));
|
||||
|
||||
let action = async (overwrite, rename) => {
|
||||
await filesApi.moveCopy(items, "move", overwrite, rename);
|
||||
@@ -334,7 +334,6 @@ export default {
|
||||
mutations.setReload(true);
|
||||
}, 50);
|
||||
};
|
||||
|
||||
let conflict = upload.checkConflict(items, response.items);
|
||||
|
||||
let overwrite = false;
|
||||
|
||||
@@ -227,7 +227,9 @@ export default {
|
||||
if (this.availableSources.length > 0) {
|
||||
const newSource = this.availableSources.pop();
|
||||
if (newSource) {
|
||||
this.selectedSources.push({ name: newSource.name, scope: "" });
|
||||
const scope = { name: newSource.name, scope: "" };
|
||||
this.selectedSources.push(scope);
|
||||
this.updateParent({ source: scope, input: { target: { value: "" } } });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import { emitStateChanged } from './eventBus'; // Import the function from event
|
||||
import { usersApi } from "@/api";
|
||||
import { notify } from "@/notify";
|
||||
import { sortedItems } from "@/utils/sort.js";
|
||||
import { serverHasMultipleSources } from "@/utils/constants.js";
|
||||
|
||||
export const mutations = {
|
||||
setCurrentSource: (value) => {
|
||||
@@ -17,6 +18,7 @@ export const mutations = {
|
||||
emitStateChanged();
|
||||
},
|
||||
setSources: (user) => {
|
||||
state.serverHasMultipleSources = serverHasMultipleSources;
|
||||
const currentSource = user.scopes.length > 0 ? user.scopes[0].name : "";
|
||||
let sources = {info: {}, current: currentSource, count: user.scopes.length};
|
||||
for (const source of user.scopes) {
|
||||
@@ -155,6 +157,7 @@ export const mutations = {
|
||||
},
|
||||
setSession: (value) => {
|
||||
state.sessionId = value;
|
||||
localStorage.setItem("sessionId", value);
|
||||
emitStateChanged();
|
||||
},
|
||||
setMultiple: (value) => {
|
||||
|
||||
@@ -10,6 +10,7 @@ export const state = reactive({
|
||||
showSidebar: false,
|
||||
usages: {},
|
||||
editor: null,
|
||||
serverHasMultipleSources: false,
|
||||
sources: {
|
||||
current: "",
|
||||
count: 1,
|
||||
|
||||
@@ -18,6 +18,7 @@ const resizePreview = window.FileBrowser.ResizePreview;
|
||||
const enableExec = window.FileBrowser.EnableExec;
|
||||
const externalUrl = window.FileBrowser.ExternalUrl
|
||||
const onlyOfficeUrl = window.FileBrowser.OnlyOfficeUrl
|
||||
const serverHasMultipleSources = window.FileBrowser.SourceCount > 1;
|
||||
const origin = window.location.origin;
|
||||
|
||||
const settings = [
|
||||
@@ -29,6 +30,7 @@ const settings = [
|
||||
]
|
||||
|
||||
export {
|
||||
serverHasMultipleSources,
|
||||
name,
|
||||
externalUrl,
|
||||
disableExternal,
|
||||
|
||||
@@ -63,6 +63,7 @@ export function getTypeInfo(mimeType) {
|
||||
mimeType === "application/gzip" ||
|
||||
mimeType === "application/x-xz" ||
|
||||
mimeType === "application/x-zip-compressed" ||
|
||||
mimeType === "application/x-compressed" ||
|
||||
mimeType === "application/x-gzip"
|
||||
) {
|
||||
return {
|
||||
|
||||
@@ -118,11 +118,11 @@ export function base64Encode(str) {
|
||||
export function extractSourceFromPath(url) {
|
||||
let source;
|
||||
let path = removePrefix(url, 'files');
|
||||
if (state.sources.count <= 1) {
|
||||
source = state.sources.current;
|
||||
} else {
|
||||
if (state.serverHasMultipleSources) {
|
||||
source = path.split('/')[1];
|
||||
path = removePrefix(path, source);
|
||||
} else {
|
||||
source = state.sources.current;
|
||||
}
|
||||
return { source, path };
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { removePrefix,extractSourceFromPath } from './url.js';
|
||||
|
||||
import { removePrefix, extractSourceFromPath } from './url.js';
|
||||
|
||||
describe('testurl', () => {
|
||||
|
||||
@@ -25,11 +24,10 @@ describe('extractSourceFromPath', () => {
|
||||
vi.doMock("@/store", () => {
|
||||
return {
|
||||
state: {
|
||||
count: 1,
|
||||
current: "default",
|
||||
serverHasMultipleSources: false,
|
||||
sources: {
|
||||
first: { pathPrefix: "", used: "0 B", total: "0 B", usedPercentage: 0 },
|
||||
},
|
||||
current: "default",
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -54,11 +52,13 @@ describe('extractSourceFromPath2', () => {
|
||||
vi.doMock("@/store", () => {
|
||||
return {
|
||||
state: {
|
||||
count: 2,
|
||||
current: "first",
|
||||
serverHasMultipleSources: true,
|
||||
sources: {
|
||||
first: { pathPrefix: "first", used: "0 B", total: "0 B", usedPercentage: 0 },
|
||||
second: { pathPrefix: "second", used: "0 B", total: "0 B", usedPercentage: 0 },
|
||||
current: "first",
|
||||
list: [
|
||||
{ pathPrefix: "first", used: "0 B", total: "0 B", usedPercentage: 0 },
|
||||
{ pathPrefix: "second", used: "0 B", total: "0 B", usedPercentage: 0 }
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -68,13 +68,11 @@ describe('extractSourceFromPath2', () => {
|
||||
{ url: "/files/first/root/file1.txt", expected: { source: "first", path: "/root/file1.txt" } },
|
||||
{ url: "/files/second/root/folder1/file1.txt", expected: { source: "second", path: "/root/folder1/file1.txt" } },
|
||||
];
|
||||
|
||||
for (const test of tests) {
|
||||
const result = extractSourceFromPath(test.url);
|
||||
expect(result.source).toEqual(test.expected.source);
|
||||
expect(result.path).toEqual(test.expected.path);
|
||||
}
|
||||
|
||||
vi.resetModules(); // Reset modules between tests
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<breadcrumbs v-if="showBreadCrumbs" base="/files" />
|
||||
<breadcrumbs v-if="showBreadCrumbs" />
|
||||
<errors v-if="error" :errorCode="error.status" />
|
||||
<component v-else-if="currentViewLoaded" :is="currentView"></component>
|
||||
<div v-else>
|
||||
@@ -103,7 +103,7 @@ export default {
|
||||
async fetchData() {
|
||||
// lets redirect if multiple sources and user went to /files/
|
||||
if (
|
||||
state.sources.count > 1 &&
|
||||
state.serverHasMultipleSources &&
|
||||
(getters.routePath() === "/files/" || getters.routePath() === "/files")
|
||||
) {
|
||||
router.push(`/files/${state.sources.current}`);
|
||||
|
||||
@@ -69,6 +69,9 @@ export default {
|
||||
if (state.user.themeColor) {
|
||||
document.documentElement.style.setProperty("--primaryColor", state.user.themeColor);
|
||||
}
|
||||
if (!state.sessionId) {
|
||||
mutations.setSession(localStorage.getItem("sessionId"));
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showPadding() {
|
||||
|
||||
@@ -517,7 +517,7 @@ export default {
|
||||
}
|
||||
},
|
||||
keyEvent(event) {
|
||||
if (state.isSearchActive) {
|
||||
if (state.isSearchActive || getters.currentView() !== "files") {
|
||||
return;
|
||||
}
|
||||
const { key, ctrlKey, metaKey, which } = event;
|
||||
@@ -571,12 +571,12 @@ export default {
|
||||
break;
|
||||
|
||||
case "Delete":
|
||||
if (!state.user.perm.delete || state.selected.length === 0) return;
|
||||
if (!state.user.perm.modify || state.selected.length === 0) return;
|
||||
mutations.showHover("delete");
|
||||
break;
|
||||
|
||||
case "F2":
|
||||
if (!state.user.perm.rename || state.selected.length !== 1) return;
|
||||
if (!state.user.perm.modify || state.selected.length !== 1) return;
|
||||
mutations.showHover("rename");
|
||||
break;
|
||||
|
||||
@@ -812,7 +812,6 @@ export default {
|
||||
path = el.__vue__.url;
|
||||
items = state.req.items;
|
||||
}
|
||||
|
||||
const conflict = upload.checkConflict(uploadFiles, items);
|
||||
try {
|
||||
if (conflict) {
|
||||
|
||||
@@ -51,6 +51,7 @@ export default {
|
||||
let configData = await fetchJSON(baseURL + `api/onlyoffice/config?url=${refUrl}`);
|
||||
configData.type = state.isMobile ? "mobile" : "desktop";
|
||||
this.clientConfig = configData;
|
||||
console.log("Client config:", this.clientConfig);
|
||||
this.ready = true;
|
||||
} catch (error) {
|
||||
console.error("Error during setup:", error);
|
||||
|
||||
@@ -274,8 +274,8 @@ export default {
|
||||
},
|
||||
prefetchUrl(item) {
|
||||
return this.fullSize
|
||||
? filesApi.getDownloadURL(item.source, item.path, true)
|
||||
: filesApi.getPreviewURL(item.source, item.path, "large", item.modified);
|
||||
? filesApi.getDownloadURL(state.req.source, item.path, true)
|
||||
: filesApi.getPreviewURL(state.req.source, item.path, "large", item.modified);
|
||||
},
|
||||
openMore() {
|
||||
this.currentPrompt = "more";
|
||||
|
||||
@@ -11,4 +11,48 @@ test("navigate with hash in file name", async ({ page, context }) => {
|
||||
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');
|
||||
})
|
||||
})
|
||||
|
||||
test("breadcrumbs navigation checks", async ({ page }) => {
|
||||
await page.goto("/files/");
|
||||
await expect(page.locator('a[aria-label="Home"]')).toHaveAttribute("href", "/playwright-files/");
|
||||
|
||||
// Ensure no <span> children exist directly under .breadcrumbs (ie no breadcrumbs paths)
|
||||
let spanChildrenCount = await page.locator('.breadcrumbs > span').count();
|
||||
await page.waitForSelector('.breadcrumbs');
|
||||
expect(spanChildrenCount).toBe(0);
|
||||
|
||||
await page.goto("/files/playwright-files/myfolder");
|
||||
await page.waitForSelector('.breadcrumbs');
|
||||
spanChildrenCount = await page.locator('.breadcrumbs > span').count();
|
||||
expect(spanChildrenCount).toBe(1);
|
||||
let breadCrumbLink = page.locator('span[aria-label="breadcrumb-link-myfolder"] a')
|
||||
await expect(breadCrumbLink).toHaveText("myfolder");
|
||||
|
||||
await page.goto("/files/playwright-files/myfolder/testdata");
|
||||
await page.waitForSelector('.breadcrumbs');
|
||||
spanChildrenCount = await page.locator('.breadcrumbs > span').count();
|
||||
expect(spanChildrenCount).toBe(2);
|
||||
breadCrumbLink = page.locator('span[aria-label="breadcrumb-link-testdata"] a')
|
||||
await expect(breadCrumbLink).toHaveText("testdata");
|
||||
|
||||
await page.goto("/files/playwright-files/files");
|
||||
await page.waitForSelector('.breadcrumbs');
|
||||
spanChildrenCount = await page.locator('.breadcrumbs > span').count();
|
||||
expect(spanChildrenCount).toBe(1);
|
||||
breadCrumbLink = page.locator('span[aria-label="breadcrumb-link-files"] a')
|
||||
await expect(breadCrumbLink).toHaveText("files");
|
||||
|
||||
});
|
||||
|
||||
|
||||
test("navigate from search item", async ({ page }) => {
|
||||
await page.goto("/files/");
|
||||
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||
await page.locator('#search').click()
|
||||
await page.locator('#main-input').fill('for testing');
|
||||
await expect(page.locator('#result-list')).toHaveCount(1);
|
||||
await page.locator('li[aria-label="for testing.md"]').click();
|
||||
await expect(page).toHaveTitle("Graham's Filebrowser - Files - for testing.md");
|
||||
await expect(page.locator('.topTitle')).toHaveText('for testing.md');
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
# this is a test
|
||||
@@ -9,7 +9,6 @@ setup:
|
||||
echo "installing npm requirements for frontend..." && \
|
||||
cd frontend && npm i
|
||||
|
||||
|
||||
update:
|
||||
cd backend && go get -u ./... && go mod tidy && cd ../frontend && npm update
|
||||
|
||||
@@ -19,7 +18,7 @@ build:
|
||||
build-backend:
|
||||
cd backend && go build -o filebrowser --ldflags="-w -s -X 'github.com/gtsteffaniak/filebrowser/backend/version.CommitSHA=testingCommit' -X 'github.com/gtsteffaniak/filebrowser/backend/version.Version=testing'"
|
||||
|
||||
run: run-frontend
|
||||
run: build-frontend
|
||||
cd backend && swag init --output swagger/docs && \
|
||||
if [ "$(shell uname)" = "Darwin" ]; then \
|
||||
sed -i '' '/func init/,+3d' ./swagger/docs/docs.go; \
|
||||
@@ -29,7 +28,7 @@ run: run-frontend
|
||||
FILEBROWSER_NO_EMBEDED=true go run \
|
||||
--ldflags="-w -s -X 'github.com/gtsteffaniak/filebrowser/backend/version.CommitSHA=testingCommit' -X 'github.com/gtsteffaniak/filebrowser/backend/version.Version=testing'" . -c test_config.yaml
|
||||
|
||||
run-frontend:
|
||||
build-frontend:
|
||||
cd backend/http && rm -rf dist && rm -rf embed/* && ln -s ../../frontend/dist && \
|
||||
cd ../../frontend && npm run build
|
||||
|
||||
@@ -51,7 +50,7 @@ test-backend:
|
||||
test-frontend:
|
||||
cd frontend && npm run test
|
||||
|
||||
test-playwright: run-frontend
|
||||
test-playwright: build-frontend
|
||||
cd backend && GOOS=linux go build -o filebrowser . && cd .. && \
|
||||
docker build -t filebrowser-playwright-tests -f _docker/Dockerfile.playwright-general . && \
|
||||
docker run --rm --name filebrowser-playwright-tests filebrowser-playwright-tests && \
|
||||
|
||||
Reference in new issue
Block a user