Compare commits

...
14 Commits
41 changed files with 365 additions and 252 deletions

No files matched your search

+1 -1
View File
@@ -9,7 +9,7 @@ on:
jobs:
test_playwright:
name: Test Playwright - regular
name: Test Playwright - general
runs-on: ubuntu-latest
steps:
- name: Checkout
+11
View File
@@ -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]
+1
View File
@@ -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.
+2
View File
@@ -1,6 +1,8 @@
server:
port: 80
baseURL: "/"
logging:
- levels: "info|debug|warning|error"
sources:
- path: "/srv"
auth:
+33 -60
View File
@@ -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 {
+4 -5
View File
@@ -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 {
+8 -7
View File
@@ -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)
}
})
}
+6 -8
View File
@@ -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++
}
}
+26 -10
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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)
}
}
+2 -7
View File
@@ -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))
}
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+8 -13
View File
@@ -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
View File
@@ -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)
+1
View File
@@ -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 != "" {
+8 -5
View File
@@ -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,
},
}
}
}
+2 -3
View File
@@ -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()
-1
View File
@@ -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
View File
@@ -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 -3
View File
@@ -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();
+26 -38
View File
@@ -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) {
+11 -5
View File
@@ -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: "" } } });
}
}
},
+3
View File
@@ -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) => {
+1
View File
@@ -10,6 +10,7 @@ export const state = reactive({
showSidebar: false,
usages: {},
editor: null,
serverHasMultipleSources: false,
sources: {
current: "",
count: 1,
+2
View File
@@ -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,
+1
View File
@@ -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 {
+3 -3
View File
@@ -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 };
}
+10 -12
View File
@@ -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
});
});
+2 -2
View File
@@ -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}`);
+3
View File
@@ -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() {
+3 -4
View File
@@ -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);
+2 -2
View File
@@ -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";
+45 -1
View File
@@ -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
+3 -4
View File
@@ -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 && \