Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd40c82f83 | ||
|
|
c82463fd3e | ||
|
|
03783f547d | ||
|
|
56dab14e6a | ||
|
|
8ea33ee274 | ||
|
|
073395aa98 |
No files matched your search
@@ -1,4 +1,5 @@
|
||||
backend/**.db
|
||||
backend/tmp
|
||||
backend/vendor/**
|
||||
frontend/dist/**
|
||||
frontend/node_modules/**
|
||||
@@ -9,6 +9,7 @@ rice-box.go
|
||||
/backend/filebrowser
|
||||
/backend/filebrowser.exe
|
||||
/backend/backend.exe
|
||||
/backend/tmp
|
||||
/frontend/dist
|
||||
/frontend/pkg
|
||||
/frontend/test-results
|
||||
|
||||
+24
-5
@@ -2,14 +2,33 @@
|
||||
|
||||
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.6-beta
|
||||
|
||||
**New Feature**:
|
||||
- limit tar size creation to limit server burden. For example, don't let customers try to download the entire filesystem as a zip. see `server.maxArchiveSize` on config wiki.
|
||||
|
||||
**Notes**:
|
||||
- disableUsedPercentage also hides text and source bar.
|
||||
- share errors show up in logs in more verbose way.
|
||||
- archive creation occurs on disk rather than in memory, use `server.cacheDir` to determine where temp files are stored.
|
||||
- automatically ensures leading slash for scope
|
||||
- https://github.com/gtsteffaniak/filebrowser/issues/472
|
||||
- https://github.com/gtsteffaniak/filebrowser/issues/476
|
||||
|
||||
**BugFixes**:
|
||||
- fix proxy user creation issue https://github.com/gtsteffaniak/filebrowser/issues/478
|
||||
- externalUrl prefix issue fixed for shares. https://github.com/gtsteffaniak/filebrowser/issues/465
|
||||
- fix File Opens Instead of Just Downloading https://github.com/gtsteffaniak/filebrowser/issues/480
|
||||
- fix Download file name https://github.com/gtsteffaniak/filebrowser/issues/481
|
||||
|
||||
## v0.6.5-beta
|
||||
|
||||
**Notes**:
|
||||
- added more share and download tests
|
||||
**Notes**:
|
||||
- added more share and download tests
|
||||
|
||||
**BugFixes**:
|
||||
- fix share download issue https://github.com/gtsteffaniak/filebrowser/issues/465
|
||||
- fix content length size calculation issue when downloading multiple files.
|
||||
**BugFixes**:
|
||||
- fix share download issue https://github.com/gtsteffaniak/filebrowser/issues/465
|
||||
- fix content length size calculation issue when downloading multiple files.
|
||||
|
||||
## v0.6.4-beta
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||
ShowHidden: a.Settings.UserDefaults.ShowHidden,
|
||||
},
|
||||
Username: a.Cred.Username,
|
||||
Perm: a.Settings.UserDefaults.Perm,
|
||||
Perm: a.Settings.UserDefaults.Permissions,
|
||||
}
|
||||
u = a.GetUser(d)
|
||||
|
||||
|
||||
+12
-1
@@ -5,6 +5,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
// UpdateFileMetadata updates the FileInfo for the specified directory in the index.
|
||||
@@ -78,7 +79,17 @@ func GetIndex(name string) *Index {
|
||||
defer indexesMutex.Unlock()
|
||||
index, ok := indexes[name]
|
||||
if !ok {
|
||||
logger.Error(fmt.Sprintf("index %s not found", name))
|
||||
// try path if name fails
|
||||
// todo: update everywhere else so this isn't needed.
|
||||
source, ok := settings.Config.Server.SourceMap[name]
|
||||
if !ok {
|
||||
logger.Error(fmt.Sprintf("index %s not found", name))
|
||||
}
|
||||
index, ok = indexes[source.Name]
|
||||
if !ok {
|
||||
logger.Error(fmt.Sprintf("index %s not found", name))
|
||||
}
|
||||
|
||||
}
|
||||
return index
|
||||
}
|
||||
@@ -57,7 +57,9 @@ func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
}
|
||||
}
|
||||
data.path = strings.TrimSuffix(link.Path, "/") + "/" + strings.TrimPrefix(path, "/")
|
||||
|
||||
if path == "" || path == "/" {
|
||||
data.path = link.Path
|
||||
}
|
||||
// Get file information with options
|
||||
file, err := FileInfoFasterFunc(files.FileOptions{
|
||||
Path: data.path,
|
||||
@@ -67,7 +69,7 @@ func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
})
|
||||
file.Token = link.Token
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("error fetching file info for share %v %v %v", hash, path, err))
|
||||
logger.Error(fmt.Sprintf("error fetching file info for share. hash=%v path=%v error=%v", hash, data.path, err))
|
||||
return errToStatus(err), fmt.Errorf("error fetching share from server")
|
||||
}
|
||||
// Set the file info in the `data` object
|
||||
@@ -115,13 +117,16 @@ func withUserHelper(fn handleFunc) handleFunc {
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
newUser := storage.CreateUser(users.User{
|
||||
err = storage.CreateUser(users.User{
|
||||
Username: proxyUser,
|
||||
NonAdminEditable: users.NonAdminEditable{
|
||||
Password: hashpass, // hashed password that can't actually be used
|
||||
},
|
||||
}, false)
|
||||
data.user, err = store.Users.Get(newUser)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
data.user, err = store.Users.Get(proxyUser)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -140,7 +145,6 @@ func withUserHelper(fn handleFunc) handleFunc {
|
||||
return http.StatusUnauthorized, err
|
||||
}
|
||||
data.token = tokenString
|
||||
|
||||
var tk users.AuthToken
|
||||
token, err := jwt.ParseWithClaims(tokenString, &tk, keyFunc)
|
||||
if err != nil {
|
||||
|
||||
+11
-4
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
|
||||
_ "github.com/gtsteffaniak/filebrowser/backend/swagger/docs"
|
||||
@@ -19,7 +20,7 @@ import (
|
||||
// @Tags Resources
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param files query string true "a list of files in the following format 'source::filename' and separated by '||' with additional items in the list. (required)"
|
||||
// @Param files query string true "a list of files in the following format 'filename' and separated by '||' with additional items in the list. (required)"
|
||||
// @Param inline query bool false "If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults to 'attachment'."
|
||||
// @Param algo query string false "Compression algorithm for archiving multiple files or directories. Options: 'zip' and 'tar.gz'. Default is 'zip'."
|
||||
// @Success 200 {file} file "Raw file or directory content, or archive for multiple files"
|
||||
@@ -38,14 +39,20 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
|
||||
fileInfo, ok := d.raw.(files.ExtendedFileInfo)
|
||||
if !ok {
|
||||
logger.Error("public share handler: failed to assert type files.FileInfo")
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type files.FileInfo")
|
||||
}
|
||||
prefix := fileInfo.Path
|
||||
fileList := []string{}
|
||||
for _, file := range strings.Split(f, "||") {
|
||||
fileList = append(fileList, fileInfo.Source+"::"+prefix+file)
|
||||
fileList = append(fileList, fileInfo.Source+"::"+fileInfo.Path+file)
|
||||
}
|
||||
return rawFilesHandler(w, r, d, fileList)
|
||||
var status int
|
||||
status, err = rawFilesHandler(w, r, d, fileList)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("public share handler: error processing filelist: %v", err))
|
||||
return status, fmt.Errorf("error processing filelist: %v", f)
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func publicShareHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
|
||||
+62
-31
@@ -3,7 +3,6 @@ package http
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -17,11 +16,12 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
func setContentDisposition(w http.ResponseWriter, r *http.Request, fileName string) {
|
||||
if r.URL.Query().Get("inline") == "true" {
|
||||
w.Header().Set("Content-Disposition", "inline")
|
||||
w.Header().Set("Content-Disposition", "inline; filename*=utf-8''"+url.PathEscape(fileName))
|
||||
} else {
|
||||
// As per RFC6266 section 4.3
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(fileName))
|
||||
@@ -205,15 +205,13 @@ 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 {
|
||||
var estimatedSize int64
|
||||
// Compute estimated download size
|
||||
estimatedSize, err = computeArchiveSize(fileList, d)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
fd, err2 := os.Open(realPath)
|
||||
if err2 != nil {
|
||||
return http.StatusInternalServerError, err2
|
||||
@@ -244,6 +242,12 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
return 200, nil
|
||||
}
|
||||
|
||||
if config.Server.MaxArchiveSizeGB > 0 {
|
||||
maxSize := config.Server.MaxArchiveSizeGB * 1024 * 1024 * 1024
|
||||
if estimatedSize > maxSize {
|
||||
return http.StatusRequestEntityTooLarge, fmt.Errorf("pre-archive combined size of files exceeds maximum limit of %d GB", config.Server.MaxArchiveSizeGB)
|
||||
}
|
||||
}
|
||||
// ** Archive (ZIP/TAR.GZ) handling **
|
||||
algo := r.URL.Query().Get("algo")
|
||||
var extension string
|
||||
@@ -265,30 +269,47 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
}
|
||||
fileName = url.PathEscape(baseDirName + extension)
|
||||
|
||||
var archiveData []byte
|
||||
archiveData := filepath.Join(config.Server.CacheDir, utils.InsecureRandomIdentifier(10))
|
||||
if extension == ".zip" {
|
||||
archiveData, err = createZip(d, fileList...)
|
||||
archiveData = archiveData + ".zip"
|
||||
err = createZip(d, archiveData, fileList...)
|
||||
} else {
|
||||
archiveData, err = createTarGz(d, fileList...)
|
||||
archiveData = archiveData + ".tar.gz"
|
||||
err = createTarGz(d, archiveData, fileList...)
|
||||
}
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
calculatedSize := len(archiveData)
|
||||
sizeInMB := calculatedSize / 1024 / 1024
|
||||
// if larger than 100 megabytes, log it
|
||||
// stream archive to response
|
||||
fd, err := os.Open(archiveData)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
// Get file size
|
||||
fileInfo, err := fd.Stat()
|
||||
if err != nil {
|
||||
os.Remove(archiveData) // Remove the file if stat fails
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
sizeInMB := fileInfo.Size() / 1024 / 1024
|
||||
if sizeInMB > 100 {
|
||||
logger.Debug(fmt.Sprintf("User %v is downloading large (%d MB) file: %v", d.user.Username, sizeInMB, fileName))
|
||||
}
|
||||
|
||||
// Set headers AFTER computing actual archive size
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+fileName)
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", calculatedSize))
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
|
||||
// Write archive data to response
|
||||
_, err = w.Write(archiveData)
|
||||
// Stream the file
|
||||
_, err = io.Copy(w, fd)
|
||||
os.Remove(archiveData) // Remove the file after streaming
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to copy archive data to response: %v", err))
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
@@ -329,36 +350,46 @@ func computeArchiveSize(fileList []string, d *requestContext) (int64, error) {
|
||||
return estimatedSize, nil
|
||||
}
|
||||
|
||||
func createZip(d *requestContext, filenames ...string) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
zipWriter := zip.NewWriter(&buf)
|
||||
func createZip(d *requestContext, tmpDirPath string, filenames ...string) error {
|
||||
file, err := os.Create(tmpDirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
zipWriter := zip.NewWriter(file)
|
||||
defer zipWriter.Close()
|
||||
|
||||
for _, fname := range filenames {
|
||||
err := addFile(fname, d, nil, zipWriter, false)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to add %s to ZIP: %v", fname, err))
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
zipWriter.Close()
|
||||
|
||||
return buf.Bytes(), nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func createTarGz(d *requestContext, filenames ...string) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
gzWriter := gzip.NewWriter(&buf)
|
||||
func createTarGz(d *requestContext, tmpDirPath string, filenames ...string) error {
|
||||
file, err := os.Create(tmpDirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
gzWriter := gzip.NewWriter(file)
|
||||
defer gzWriter.Close()
|
||||
tarWriter := tar.NewWriter(gzWriter)
|
||||
defer tarWriter.Close()
|
||||
|
||||
for _, fname := range filenames {
|
||||
err := addFile(fname, d, tarWriter, nil, false)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to add %s to TAR.GZ: %v", fname, err))
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
tarWriter.Close()
|
||||
gzWriter.Close()
|
||||
|
||||
return buf.Bytes(), nil
|
||||
return nil
|
||||
}
|
||||
@@ -39,7 +39,6 @@ func shareListHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
if err == errors.ErrNotExist {
|
||||
return renderJSON(w, r, []*share.Link{})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -174,14 +173,16 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
stringHash = string(hash)
|
||||
}
|
||||
path := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = config.Server.DefaultSource.Name
|
||||
sourceName := r.URL.Query().Get("source")
|
||||
if sourceName == "" {
|
||||
sourceName = config.Server.DefaultSource.Name
|
||||
}
|
||||
source := config.Server.NameToSource[sourceName]
|
||||
|
||||
s = &share.Link{
|
||||
Path: path,
|
||||
Hash: secure_hash,
|
||||
Source: source,
|
||||
Source: source.Path,
|
||||
Expire: expire,
|
||||
UserID: d.user.ID,
|
||||
PasswordHash: stringHash,
|
||||
|
||||
@@ -126,7 +126,6 @@ func setupAuth() {
|
||||
if Config.Auth.Method != "" {
|
||||
logger.Warning("The `auth.method` setting is deprecated and will be removed in a future version. Please use `auth.methods` instead.")
|
||||
}
|
||||
Config.UserDefaults.Perm = Config.UserDefaults.Permissions
|
||||
if Config.Auth.Methods.PasswordAuth.Enabled {
|
||||
Config.Auth.AuthMethods = append(Config.Auth.AuthMethods, "Password")
|
||||
}
|
||||
@@ -206,6 +205,8 @@ func setDefaults() Settings {
|
||||
Database: "database.db",
|
||||
SourceMap: map[string]Source{},
|
||||
NameToSource: map[string]Source{},
|
||||
MaxArchiveSizeGB: 50,
|
||||
CacheDir: "tmp",
|
||||
},
|
||||
Auth: Auth{
|
||||
AdminUsername: "admin",
|
||||
@@ -234,6 +235,7 @@ func setDefaults() Settings {
|
||||
ViewMode: "normal",
|
||||
Locale: "en",
|
||||
GallerySize: 3,
|
||||
ThemeColor: "var(--blue)",
|
||||
Permissions: users.Permissions{
|
||||
Modify: false,
|
||||
Share: false,
|
||||
@@ -253,6 +255,9 @@ func ConvertToBackendScopes(scopes []users.SourceScope) ([]users.SourceScope, er
|
||||
if scope.Scope == "" {
|
||||
scope.Scope = "/"
|
||||
}
|
||||
if !strings.HasPrefix(scope.Scope, "/") {
|
||||
scope.Scope = "/" + scope.Scope
|
||||
}
|
||||
// first check if its already a path name and keep it
|
||||
source, ok := Config.Server.SourceMap[scope.Name]
|
||||
if ok {
|
||||
|
||||
@@ -44,9 +44,14 @@ func ApplyUserDefaults(u *users.User) {
|
||||
u.Locale = Config.UserDefaults.Locale
|
||||
u.ViewMode = Config.UserDefaults.ViewMode
|
||||
u.SingleClick = Config.UserDefaults.SingleClick
|
||||
u.Perm = Config.UserDefaults.Perm
|
||||
u.Perm = Config.UserDefaults.Permissions
|
||||
u.ShowHidden = Config.UserDefaults.ShowHidden
|
||||
u.DateFormat = Config.UserDefaults.DateFormat
|
||||
u.DisableOnlyOfficeExt = Config.UserDefaults.DisableOnlyOfficeExt
|
||||
u.ThemeColor = Config.UserDefaults.ThemeColor
|
||||
u.GallerySize = Config.UserDefaults.GallerySize
|
||||
u.QuickDownload = Config.UserDefaults.QuickDownload
|
||||
u.LockPassword = Config.UserDefaults.LockPassword
|
||||
if len(u.Scopes) == 0 {
|
||||
u.Scopes = []users.SourceScope{
|
||||
{
|
||||
@@ -55,5 +60,4 @@ func ApplyUserDefaults(u *users.User) {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -79,6 +79,7 @@ type Server struct {
|
||||
ExternalUrl string `json:"externalUrl"`
|
||||
InternalUrl string `json:"internalUrl"` // used by integrations
|
||||
CacheDir string `json:"cacheDir"`
|
||||
MaxArchiveSizeGB int64 `json:"maxArchiveSize"`
|
||||
// not exposed to config
|
||||
SourceMap map[string]Source // uses realpath as key
|
||||
NameToSource map[string]Source // uses name as key
|
||||
@@ -157,7 +158,6 @@ type UserDefaults struct {
|
||||
ViewMode string `json:"viewMode"`
|
||||
GallerySize int `json:"gallerySize"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Perm users.Permissions `json:"perm"`
|
||||
Permissions users.Permissions `json:"permissions"`
|
||||
ShowHidden bool `json:"showHidden"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
|
||||
@@ -1543,7 +1543,7 @@ const docTemplate = `{
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "a list of files in the following format 'source::filename' and separated by '||' with additional items in the list. (required)",
|
||||
"description": "a list of files in the following format 'filename' and separated by '||' with additional items in the list. (required)",
|
||||
"name": "files",
|
||||
"in": "query",
|
||||
"required": true
|
||||
@@ -1831,9 +1831,6 @@ const docTemplate = `{
|
||||
"lockPassword": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"perm": {
|
||||
"$ref": "#/definitions/users.Permissions"
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/definitions/users.Permissions"
|
||||
},
|
||||
|
||||
@@ -1532,7 +1532,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "a list of files in the following format 'source::filename' and separated by '||' with additional items in the list. (required)",
|
||||
"description": "a list of files in the following format 'filename' and separated by '||' with additional items in the list. (required)",
|
||||
"name": "files",
|
||||
"in": "query",
|
||||
"required": true
|
||||
@@ -1820,9 +1820,6 @@
|
||||
"lockPassword": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"perm": {
|
||||
"$ref": "#/definitions/users.Permissions"
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/definitions/users.Permissions"
|
||||
},
|
||||
|
||||
@@ -148,8 +148,6 @@ definitions:
|
||||
type: string
|
||||
lockPassword:
|
||||
type: boolean
|
||||
perm:
|
||||
$ref: '#/definitions/users.Permissions'
|
||||
permissions:
|
||||
$ref: '#/definitions/users.Permissions'
|
||||
quickDownload:
|
||||
@@ -1315,8 +1313,8 @@ paths:
|
||||
description: Returns the raw content of a file, multiple files, or a directory.
|
||||
Supports downloading files as archives in various formats.
|
||||
parameters:
|
||||
- description: a list of files in the following format 'source::filename' and
|
||||
separated by '||' with additional items in the list. (required)
|
||||
- description: a list of files in the following format 'filename' and separated
|
||||
by '||' with additional items in the list. (required)
|
||||
in: query
|
||||
name: files
|
||||
required: true
|
||||
|
||||
+28
-29
@@ -1,8 +1,8 @@
|
||||
import { fetchURL, adjustedData } from './utils'
|
||||
import { getApiPath, extractSourceFromPath } from '@/utils/url.js'
|
||||
import { getApiPath, extractSourceFromPath,removePrefix } from '@/utils/url.js'
|
||||
import { state } from '@/store'
|
||||
import { notify } from '@/notify'
|
||||
import { externalUrl } from '@/utils/constants'
|
||||
import { externalUrl,baseURL } from '@/utils/constants'
|
||||
|
||||
// Notify if errors occur
|
||||
export async function fetchFiles(url, content = false) {
|
||||
@@ -64,34 +64,32 @@ export function download(format, files) {
|
||||
if (format !== 'zip') {
|
||||
format = 'tar.gz'
|
||||
}
|
||||
try {
|
||||
let fileargs = ''
|
||||
if (files.length === 1) {
|
||||
const result = extractSourceFromPath(decodeURI(files[0]))
|
||||
fileargs = result.source + '::' + result.path + '||'
|
||||
} else {
|
||||
for (let file of files) {
|
||||
const result = extractSourceFromPath(decodeURI(file))
|
||||
fileargs += result.source + '::' + result.path + '||'
|
||||
}
|
||||
}
|
||||
fileargs = fileargs.slice(0, -2) // remove trailing "||"
|
||||
const apiPath = getApiPath('api/raw', {
|
||||
files: encodeURIComponent(fileargs),
|
||||
algo: format
|
||||
})
|
||||
const url = window.origin + apiPath
|
||||
|
||||
// Create a temporary <a> element to trigger the download
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', '') // Ensures it triggers a download
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link) // Clean up
|
||||
} catch (err) {
|
||||
notify.showError(err.message || 'Error downloading files')
|
||||
let fileargs = ''
|
||||
if (files.length === 1) {
|
||||
const result = extractSourceFromPath(decodeURI(files[0]))
|
||||
fileargs = result.source + '::' + result.path + '||'
|
||||
} else {
|
||||
for (let file of files) {
|
||||
const result = extractSourceFromPath(decodeURI(file))
|
||||
fileargs += result.source + '::' + result.path + '||'
|
||||
}
|
||||
}
|
||||
fileargs = fileargs.slice(0, -2) // remove trailing "||"
|
||||
const apiPath = getApiPath('api/raw', {
|
||||
files: encodeURIComponent(fileargs),
|
||||
algo: format
|
||||
})
|
||||
const url = window.origin + apiPath
|
||||
|
||||
// Create a temporary <a> element to trigger the download
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', '') // Ensures it triggers a download
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link) // Clean up
|
||||
|
||||
}
|
||||
|
||||
export async function post(url, content = '', overwrite = false, onupload) {
|
||||
@@ -212,7 +210,8 @@ export function getDownloadURL(source, path, inline, useExternal) {
|
||||
}
|
||||
const apiPath = getApiPath('api/raw', params)
|
||||
if (externalUrl && useExternal) {
|
||||
return externalUrl + apiPath
|
||||
|
||||
return externalUrl + removePrefix(apiPath,baseURL)
|
||||
}
|
||||
return window.origin + apiPath
|
||||
} catch (err) {
|
||||
|
||||
+23
-24
@@ -1,5 +1,5 @@
|
||||
import { adjustedData } from "./utils";
|
||||
import { getApiPath, extractSourceFromPath } from "@/utils/url.js";
|
||||
import { getApiPath } from "@/utils/url.js";
|
||||
import { notify } from "@/notify";
|
||||
|
||||
// Fetch public share data
|
||||
@@ -23,31 +23,30 @@ export async function fetchPub(path, hash, password = "") {
|
||||
}
|
||||
|
||||
// Download files with given parameters
|
||||
export function download(share, ...files) {
|
||||
try {
|
||||
let fileargs = ''
|
||||
if (files.length === 1) {
|
||||
const result = extractSourceFromPath(decodeURI(files[0]))
|
||||
fileargs = result.path + '||'
|
||||
} else {
|
||||
for (let file of files) {
|
||||
const result = extractSourceFromPath(decodeURI(file))
|
||||
fileargs += result.path + '||'
|
||||
}
|
||||
export function download(format, files) {
|
||||
console.log("Downloading files:",format, files)
|
||||
let fileargs = ''
|
||||
if (files.length === 1) {
|
||||
fileargs = decodeURI(files[0]) + '||'
|
||||
} else {
|
||||
for (let file of files) {
|
||||
fileargs += decodeURI(file) + '||'
|
||||
}
|
||||
fileargs = fileargs.slice(0, -2); // remove trailing "||"
|
||||
const params = {
|
||||
"files": fileargs,
|
||||
"hash": share.hash,
|
||||
"token": share.token,
|
||||
"inline": share.inline,
|
||||
};
|
||||
const apiPath = getApiPath("api/public/dl", params);
|
||||
window.open(window.origin+apiPath)
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error downloading files");
|
||||
throw err;
|
||||
}
|
||||
fileargs = fileargs.slice(0, -2) // remove trailing "||"
|
||||
const apiPath = getApiPath('api/public/dl', {
|
||||
files: encodeURIComponent(fileargs),
|
||||
hash: format.hash,
|
||||
})
|
||||
const url = window.origin + apiPath
|
||||
// Create a temporary <a> element to trigger the download
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', '') // Ensures it triggers a download
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link) // Clean up
|
||||
|
||||
}
|
||||
|
||||
// Get the public user data
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fetchURL, fetchJSON, adjustedData } from "./utils";
|
||||
import { notify } from "@/notify";
|
||||
import { getApiPath } from "@/utils/url.js";
|
||||
import { externalUrl } from "@/utils/constants";
|
||||
import { getApiPath,removePrefix } from "@/utils/url.js";
|
||||
import { externalUrl,baseURL } from "@/utils/constants";
|
||||
|
||||
export async function list() {
|
||||
const apiPath = getApiPath("api/shares");
|
||||
@@ -43,7 +43,8 @@ export async function create(path, source, password = "", expires = "", unit = "
|
||||
|
||||
export function getShareURL(share) {
|
||||
if (externalUrl) {
|
||||
return externalUrl+getApiPath(`share/${share.hash}`);
|
||||
const apiPath = getApiPath(`share/${share.hash}`)
|
||||
return externalUrl + removePrefix(apiPath, baseURL);
|
||||
}
|
||||
return window.origin+getApiPath(`share/${share.hash}`);
|
||||
}
|
||||
@@ -196,6 +196,7 @@ export default {
|
||||
methods: {
|
||||
downloadFile(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
mutations.resetSelected();
|
||||
mutations.addSelected(this.index);
|
||||
downloadFiles();
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
>
|
||||
<i class="material-icons source-icon">folder</i>
|
||||
<span>{{ name }}</span>
|
||||
<div>
|
||||
<div v-if="!disableUsedPercentage">
|
||||
<progress-bar
|
||||
:val="info.usedPercentage"
|
||||
text-position="inside"
|
||||
@@ -83,7 +83,13 @@
|
||||
|
||||
<script>
|
||||
import * as auth from "@/utils/auth";
|
||||
import { signup, disableExternal, noAuth, loginPage } from "@/utils/constants";
|
||||
import {
|
||||
signup,
|
||||
disableExternal,
|
||||
noAuth,
|
||||
loginPage,
|
||||
disableUsedPercentage,
|
||||
} from "@/utils/constants";
|
||||
import ProgressBar from "@/components/ProgressBar.vue";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ router.beforeResolve(async (to, from, next) => {
|
||||
// Handle auth requirements
|
||||
if (to.matched.some((record) => record.meta.requiresAuth)) {
|
||||
|
||||
if (state != null && state.user != null && !state.user.username){
|
||||
if (state != null && state.user != null && !state.user.username) {
|
||||
await validateLogin();
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function download() {
|
||||
});
|
||||
}
|
||||
|
||||
async function startDownload(config,files, isPublic) {
|
||||
async function startDownload(config, files, isPublic) {
|
||||
try {
|
||||
if (isPublic) {
|
||||
publicApi.download(config, files);
|
||||
@@ -58,6 +58,6 @@ async function startDownload(config,files, isPublic) {
|
||||
}
|
||||
notify.showSuccess("Downloading...");
|
||||
} catch (e) {
|
||||
notify.showError("Error downloading", e);
|
||||
notify.showError(`Error downloading: ${e}`);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ describe('testurl', () => {
|
||||
|
||||
it('url prefix', () => {
|
||||
let tests = [
|
||||
{input: "/files/share/hash", trimArg:"/files/",expected: "/share/hash",},
|
||||
{input: "/files/files", trimArg: "/files/",expected: "/files",},
|
||||
{input: "/files/share/something/", trimArg: "files", expected:"/share/something/"},
|
||||
{input: "test/iscool/", trimArg: "test",expected:"/iscool/"},
|
||||
@@ -25,6 +26,7 @@ describe('getapipath', () => {
|
||||
it('url prefix', () => {
|
||||
let tests = [
|
||||
{input: "/share/to/thing", expected: "/files/share/to/thing",},
|
||||
{input: "share/hash", expected: "/files/share/hash",},
|
||||
]
|
||||
for (let test of tests) {
|
||||
expect(getApiPath(test.input)).toEqual(test.expected);
|
||||
|
||||
@@ -326,7 +326,7 @@ export default {
|
||||
token: this.token,
|
||||
format: format,
|
||||
};
|
||||
publicApi.download(share, ...files);
|
||||
publicApi.download(share, files);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -33,6 +33,21 @@ test("root share path is valid", async ({ page, checkForErrors, context }) => {
|
||||
checkForErrors();
|
||||
});
|
||||
|
||||
|
||||
test("share file works", async ({ page, checkForErrors, context }) => {
|
||||
await page.goto("/files/");
|
||||
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||
const shareHashFile = await page.evaluate(() => localStorage.getItem('shareHashFile'));
|
||||
if (shareHashFile == "") {
|
||||
throw new Error("Share hash not found in localStorage");
|
||||
}
|
||||
|
||||
await page.goto("/share/" + shareHashFile);
|
||||
await expect(page).toHaveTitle("Graham's Filebrowser - Share - 1file1.txt");
|
||||
await expect(page.locator('a[aria-label="Home"]')).toHaveAttribute("href", `/share/${shareHashFile}/`);
|
||||
checkForErrors();
|
||||
});
|
||||
|
||||
test("share download single file", async ({ page, checkForErrors, context }) => {
|
||||
await page.goto("/files/");
|
||||
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||
|
||||
@@ -18,7 +18,7 @@ async function globalSetup() {
|
||||
|
||||
await page.waitForURL("**/files/playwright-files", { timeout: 250 });
|
||||
|
||||
// Create a share
|
||||
// Create a share of folder
|
||||
await page.locator('a[aria-label="myfolder"]').waitFor({ state: 'visible' });
|
||||
await page.locator('a[aria-label="myfolder"]').click({ button: "right" });
|
||||
await page.locator('.selected-count-header').waitFor({ state: 'visible' });
|
||||
@@ -36,6 +36,26 @@ async function globalSetup() {
|
||||
localStorage.setItem('shareHash', hash);
|
||||
}, shareHash);
|
||||
|
||||
await page.goto("http://127.0.0.1/files/playwright-files", { timeout: 250 });
|
||||
// Create a share of file
|
||||
await page.locator('a[aria-label="1file1.txt"]').waitFor({ state: 'visible' });
|
||||
await page.locator('a[aria-label="1file1.txt"]').click({ button: "right" });
|
||||
await page.locator('.selected-count-header').waitFor({ state: 'visible' });
|
||||
await expect(page.locator('.selected-count-header')).toHaveText('1 selected');
|
||||
await page.locator('button[aria-label="Share"]').click();
|
||||
await expect(page.locator('div[aria-label="share-path"]')).toHaveText('Path: /1file1.txt');
|
||||
await page.locator('button[aria-label="Share-Confirm"]').click();
|
||||
await expect(page.locator("#share .card-content table tbody tr:not(:has(th))")).toHaveCount(1);
|
||||
const shareHashFile = await page.locator("#share .card-content table tbody tr:not(:has(th)) td").first().textContent();
|
||||
if (!shareHashFile) {
|
||||
throw new Error("Failed to retrieve shareHash");
|
||||
}
|
||||
// Store shareHash in localStorage
|
||||
await page.evaluate((hash) => {
|
||||
localStorage.setItem('shareHashFile', hash);
|
||||
}, shareHashFile);
|
||||
|
||||
|
||||
await context.storageState({ path: "./loginAuth.json" });
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user