updated with share refactor (#993)

This commit is contained in:
Graham Steffaniak authored and GitHub committed 2025-07-23 09:36:49 -05:00
1 parent e08690b66d
commit 080256bcff
44 files changed
+1357 -684

No files matched your search

+5
View File
@@ -2,6 +2,11 @@
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.8.0-beta
**Notes**:
- changed partition calculations on linux for total disk size (#982)
## v0.7.16-beta
**Notes**:
+1 -1
View File
@@ -50,7 +50,7 @@ func FileInfoFaster(opts iteminfo.FileOptions) (iteminfo.ExtendedFileInfo, error
if err != nil {
return response, err
}
} else {
} else if err == errors.ErrNotIndexed {
return response, fmt.Errorf("could not refresh file info: %v", err)
}
} else {
+2 -2
View File
@@ -10,8 +10,8 @@ import (
"github.com/gtsteffaniak/go-logger/logger"
)
const PermFile = 0640
const PermDir = 0750
const PermFile = 0644
const PermDir = 0755
// MoveFile moves a file from src to dst.
// By default, the rename system call is used. If src and dst point to different volumes,
+99 -15
View File
@@ -8,6 +8,7 @@ import (
"io/fs"
"net/http"
"os"
"strings"
"text/template"
"time"
@@ -64,6 +65,8 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
router := http.NewServeMux()
// API group routing
api := http.NewServeMux()
// Public group routing (new structure)
publicRoutes := http.NewServeMux()
// User routes
api.HandleFunc("GET /users", withUser(userGetHandler))
@@ -97,24 +100,23 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
api.HandleFunc("GET /inspectIndex", inspectIndex)
api.HandleFunc("GET /mockData", mockData)
}
// Access routes
api.HandleFunc("GET /access", withAdmin(accessGetHandler))
api.HandleFunc("POST /access", withAdmin(accessPostHandler))
api.HandleFunc("DELETE /access", withAdmin(accessDeleteHandler))
// Share routes
api.HandleFunc("GET /shares", withPermShare(shareListHandler))
api.HandleFunc("GET /share", withPermShare(shareGetHandler))
api.HandleFunc("POST /share", withPermShare(sharePostHandler))
api.HandleFunc("DELETE /share", withPermShare(shareDeleteHandler))
// Public routes
api.HandleFunc("GET /public/publicUser", withoutUser(publicUserGetHandler))
api.HandleFunc("GET /public/dl", withHashFile(publicRawHandler))
api.HandleFunc("GET /public/share", withHashFile(publicShareHandler))
api.HandleFunc("GET /public/preview", withHashFile(publicPreviewHandler))
// Create API sub-router for public API endpoints
publicAPI := http.NewServeMux()
// NEW PUBLIC ROUTES - All publicly accessible endpoints
// Share management routes (require permission but are publicly accessible)
publicRoutes.HandleFunc("GET /shares", withPermShare(shareListHandler))
publicRoutes.HandleFunc("GET /share", withPermShare(shareGetHandler))
publicRoutes.HandleFunc("POST /share", withPermShare(sharePostHandler))
publicRoutes.HandleFunc("DELETE /share", withPermShare(shareDeleteHandler))
// Public API routes (hash-based authentication)
publicAPI.HandleFunc("GET /user", withoutUser(publicUserGetHandler))
publicAPI.HandleFunc("GET /raw", withHashFile(publicRawHandler))
publicAPI.HandleFunc("GET /preview", withHashFile(publicPreviewHandler))
publicAPI.HandleFunc("GET /shared", withHashFile(publicShareHandler))
// Settings routes
api.HandleFunc("GET /settings", withAdmin(settingsGetHandler))
@@ -123,17 +125,99 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
d.ctx = ctx // Pass the parent context to ensure proper shutdown
return sseHandler(w, r, d)
}))
// Job routes
api.HandleFunc("GET /jobs/{action}/{target}", withUser(getJobsHandler))
api.HandleFunc("GET /onlyoffice/config", withUser(onlyofficeClientConfigGetHandler))
publicAPI.HandleFunc("GET /onlyoffice/config", withHashFile(onlyofficeClientConfigGetHandler))
api.HandleFunc("POST /onlyoffice/callback", withUser(onlyofficeCallbackHandler))
publicAPI.HandleFunc("POST /onlyoffice/callback", withHashFile(onlyofficeCallbackHandler))
api.HandleFunc("GET /onlyoffice/getToken", withUser(onlyofficeGetTokenHandler))
publicAPI.HandleFunc("GET /onlyoffice/getToken", withHashFile(onlyofficeGetTokenHandler))
api.HandleFunc("GET /search", withUser(searchHandler))
// Share routes (DEPRECATED - maintain for backwards compatibility)
// These will redirect to the new /public/shares endpoints
api.HandleFunc("GET /shares", func(w http.ResponseWriter, r *http.Request) {
newURL := config.Server.BaseURL + "public/shares"
if r.URL.RawQuery != "" {
newURL += "?" + r.URL.RawQuery
}
http.Redirect(w, r, newURL, http.StatusMovedPermanently)
})
api.HandleFunc("GET /share", func(w http.ResponseWriter, r *http.Request) {
newURL := config.Server.BaseURL + "public/share"
if r.URL.RawQuery != "" {
newURL += "?" + r.URL.RawQuery
}
http.Redirect(w, r, newURL, http.StatusMovedPermanently)
})
api.HandleFunc("POST /share", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, config.Server.BaseURL+"public/share", http.StatusPermanentRedirect)
})
api.HandleFunc("DELETE /share", func(w http.ResponseWriter, r *http.Request) {
newURL := config.Server.BaseURL + "public/share"
if r.URL.RawQuery != "" {
newURL += "?" + r.URL.RawQuery
}
http.Redirect(w, r, newURL, http.StatusPermanentRedirect)
})
// Legacy public routes (DEPRECATED - maintain for backwards compatibility)
api.HandleFunc("GET /public/publicUser", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, config.Server.BaseURL+"public/api/user", http.StatusMovedPermanently)
})
api.HandleFunc("GET /public/raw", func(w http.ResponseWriter, r *http.Request) {
newURL := config.Server.BaseURL + "public/api/raw"
if r.URL.RawQuery != "" {
newURL += "?" + r.URL.RawQuery
}
http.Redirect(w, r, newURL, http.StatusMovedPermanently)
})
api.HandleFunc("GET /public/share", func(w http.ResponseWriter, r *http.Request) {
newURL := config.Server.BaseURL + "public/api/shared"
if r.URL.RawQuery != "" {
newURL += "?" + r.URL.RawQuery
}
http.Redirect(w, r, newURL, http.StatusMovedPermanently)
})
api.HandleFunc("GET /public/preview", func(w http.ResponseWriter, r *http.Request) {
newURL := config.Server.BaseURL + "public/api/preview"
if r.URL.RawQuery != "" {
newURL += "?" + r.URL.RawQuery
}
http.Redirect(w, r, newURL, http.StatusMovedPermanently)
})
// Mount the public API sub-router
publicRoutes.Handle("/api/", http.StripPrefix("/api", publicAPI))
// Mount the route groups
apiPath := config.Server.BaseURL + "api"
publicPath := config.Server.BaseURL + "public"
router.Handle(apiPath+"/", http.StripPrefix(apiPath, api))
router.Handle(publicPath+"/", http.StripPrefix(publicPath, publicRoutes))
// Frontend share route redirect (DEPRECATED - maintain for backwards compatibility)
router.HandleFunc(fmt.Sprintf("GET %vshare/", config.Server.BaseURL), func(w http.ResponseWriter, r *http.Request) {
// Remove the base URL and "/share/" prefix to get the full path after share
sharePath := strings.TrimPrefix(r.URL.Path, config.Server.BaseURL+"share/")
newURL := config.Server.BaseURL + "public/share/" + sharePath
if r.URL.RawQuery != "" {
newURL += "?" + r.URL.RawQuery
}
http.Redirect(w, r, newURL, http.StatusMovedPermanently)
})
// New frontend share route handler - handle share page and any subpaths
publicRoutes.HandleFunc("GET /share/", func(w http.ResponseWriter, r *http.Request) {
// This serves the HTML page for shared content and any subpaths within shares
indexHandler(w, r)
})
// Public static assets (needed for shared pages to load CSS/JS/images)
publicRoutes.HandleFunc("GET /static/", staticFilesHandler)
// Static and index file handlers
router.HandleFunc(fmt.Sprintf("GET %vstatic/", config.Server.BaseURL), staticFilesHandler)
+3 -2
View File
@@ -199,9 +199,10 @@ func withUserHelper(fn handleFunc) handleFunc {
if err != nil {
return http.StatusInternalServerError, err
}
setUserInResponseWriter(w, data.user)
if data.user.Username == "" || data.user.Username == "publicUser" {
return http.StatusForbidden, fmt.Errorf("preview is disabled for public users")
}
// Call the handler function, passing in the context
return fn(w, r, data)
}
+310 -47
View File
@@ -34,70 +34,287 @@ type OnlyOfficeCallback struct {
UserData string `json:"userdata,omitempty"`
}
// ParsedURLInfo holds the parsed URL information
type ParsedURLInfo struct {
Source string
Path string
EncodedPath string
IsPublicShare bool
DecodedURL string
BaseURL string
ShareHash string
}
// parseOnlyOfficeURL parses and validates the OnlyOffice URL from the request
func parseOnlyOfficeURL(encodedURL string) (*ParsedURLInfo, error) {
decodedURL, err := url.QueryUnescape(encodedURL)
if err != nil {
return nil, fmt.Errorf("invalid URL encoding: %v", err)
}
info := &ParsedURLInfo{
DecodedURL: decodedURL,
}
if strings.Contains(decodedURL, "public/api/raw") {
return parsePublicShareURL(info, encodedURL)
}
return parseRegularResourceURL(info, encodedURL)
}
// parsePublicShareURL handles public share URL format: /public/api/raw?path=/file.doc&hash=...
func parsePublicShareURL(info *ParsedURLInfo, encodedURL string) (*ParsedURLInfo, error) {
info.IsPublicShare = true
// Parse the URL to extract query parameters
parsedURL, err := url.Parse(info.DecodedURL)
if err != nil {
return nil, fmt.Errorf("invalid URL format: %v", err)
}
info.Path = parsedURL.Query().Get("path")
info.ShareHash = parsedURL.Query().Get("hash")
if info.Path == "" || info.ShareHash == "" {
return nil, fmt.Errorf("missing path or hash parameter in public share URL")
}
// Extract base URL
parts := strings.Split(info.DecodedURL, "/public/api/raw")
if len(parts) < 2 {
return nil, fmt.Errorf("invalid public share URL format")
}
info.BaseURL = parts[0]
return info, nil
}
// parseRegularResourceURL handles regular resource URL format: /api/raw?files=source::path
func parseRegularResourceURL(info *ParsedURLInfo, encodedURL string) (*ParsedURLInfo, error) {
info.IsPublicShare = false
// Extract the files parameter
decodedParts := strings.Split(info.DecodedURL, "/api/raw?files=")
encodedParts := strings.Split(encodedURL, "/api/raw?files=")
if len(decodedParts) < 2 || len(encodedParts) < 2 {
return nil, fmt.Errorf("invalid resource URL format")
}
info.EncodedPath = encodedParts[1]
sourceFile := decodedParts[1]
// Parse source::path format
if err := parseSourcePath(sourceFile, info); err != nil {
return nil, err
}
// Extract base URL
baseURLParts := strings.Split(info.DecodedURL, "/api/raw")
if len(baseURLParts) < 1 {
return nil, fmt.Errorf("invalid resource URL format")
}
info.BaseURL = baseURLParts[0]
return info, nil
}
// parseSourcePath parses the source::path format and validates it
func parseSourcePath(sourceFile string, info *ParsedURLInfo) error {
const delimiter = "::"
parts := strings.Split(sourceFile, delimiter)
if len(parts) != 2 {
return fmt.Errorf("invalid source::path format in URL: %s", sourceFile)
}
info.Source = strings.TrimSpace(parts[0])
info.Path = strings.TrimSpace(parts[1])
if info.Source == "" || info.Path == "" {
return fmt.Errorf("source and path cannot be empty")
}
return nil
}
// processPublicShare handles the authentication and path construction for public shares
func processPublicShare(info *ParsedURLInfo, r *http.Request) error {
// Get the share link by hash
link, err := store.Share.GetByHash(info.ShareHash)
if err != nil {
return fmt.Errorf("share not found")
}
// Authenticate the share request
if link.Hash != "" {
status, err := authenticateShareRequest(r, link)
if err != nil || status != http.StatusOK {
return fmt.Errorf("could not authenticate share request")
}
}
// Use the actual source from the share link
info.Source = link.Source
// Construct the full path like in withHashFileHelper
fullPath := constructSharePath(link.Path, info.Path)
info.Path = fullPath
// Create encoded path for callback
info.EncodedPath = info.Source + "::" + url.QueryEscape(info.Path)
return nil
}
// constructSharePath builds the complete path for shared resources
func constructSharePath(linkPath, requestPath string) string {
if requestPath == "" || requestPath == "/" {
return linkPath
}
return strings.TrimSuffix(linkPath, "/") + "/" + strings.TrimPrefix(requestPath, "/")
}
// adjustURLForInternalConfig modifies URLs based on internal server configuration
func adjustURLForInternalConfig(info *ParsedURLInfo) {
if settings.Config.Server.InternalUrl == "" {
return
}
info.BaseURL = settings.Config.Server.InternalUrl
var replacement string
if info.IsPublicShare {
parts := strings.Split(info.DecodedURL, "/public/api/raw")
if len(parts) > 0 {
replacement = parts[0]
}
} else {
parts := strings.Split(info.DecodedURL, "/api/raw")
if len(parts) > 0 {
replacement = parts[0]
}
}
if replacement != "" {
info.DecodedURL = strings.Replace(info.DecodedURL, replacement, settings.Config.Server.InternalUrl, 1)
}
}
func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
if settings.Config.Integrations.OnlyOffice.Url == "" {
return http.StatusInternalServerError, errors.New("only-office integration must be configured in settings")
}
encodedUrl := r.URL.Query().Get("url")
// Decode the URL-encoded path
url, err := url.QueryUnescape(encodedUrl)
encodedURL := r.URL.Query().Get("url")
if encodedURL == "" {
return http.StatusBadRequest, errors.New("missing url parameter")
}
// Parse the URL into structured information
urlInfo, err := parseOnlyOfficeURL(encodedURL)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
return http.StatusBadRequest, err
}
// get path from url
pathParts := strings.Split(url, "/api/raw?files=")
origPathParts := strings.Split(encodedUrl, "/api/raw?files=")
encodedPath := origPathParts[len(origPathParts)-1]
sourceFile := pathParts[len(pathParts)-1]
sourceSplit := strings.Split(sourceFile, "::")
if len(sourceSplit) != 2 {
return http.StatusBadRequest, fmt.Errorf("invalid url path %v", url)
// Handle public share authentication and path construction
if urlInfo.IsPublicShare {
if err = processPublicShare(urlInfo, r); err != nil {
return http.StatusForbidden, err
}
}
source := sourceSplit[0]
path := sourceSplit[1]
urlFirst := pathParts[0]
if settings.Config.Server.InternalUrl != "" {
urlFirst = settings.Config.Server.InternalUrl
replacement := strings.Split(url, "/api/raw")[0]
url = strings.Replace(url, replacement, settings.Config.Server.InternalUrl, 1)
}
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
if err != nil {
return http.StatusForbidden, err
}
fileInfo, err := files.FileInfoFaster(iteminfo.FileOptions{
Access: store.Access,
Username: d.user.Username,
Path: utils.JoinPathAsUnix(userscope, path),
Modify: d.user.Permissions.Modify,
Source: source,
Expand: false,
})
// Adjust URLs based on internal server configuration
adjustURLForInternalConfig(urlInfo)
// Get file information based on share type
fileInfo, err := getFileInfo(urlInfo, d)
if err != nil {
return errToStatus(err), err
}
id, err := getOnlyOfficeId(source, fileInfo.Path)
// Generate OnlyOffice document ID
id, err := getOnlyOfficeId(urlInfo.Source, fileInfo.Path)
if err != nil {
return http.StatusNotFound, err
}
split := strings.Split(fileInfo.Name, ".")
fileType := split[len(split)-1]
// Build and return client configuration
clientConfig, err := buildClientConfig(urlInfo, fileInfo, id, d)
if err != nil {
return http.StatusInternalServerError, err
}
return renderJSON(w, r, clientConfig)
}
// getFileInfo retrieves file information based on whether it's a public share or regular resource
func getFileInfo(urlInfo *ParsedURLInfo, d *requestContext) (iteminfo.ExtendedFileInfo, error) {
if urlInfo.IsPublicShare {
// Get source configuration from the source map
sourceConfig, ok := config.Server.SourceMap[urlInfo.Source]
if !ok {
return iteminfo.ExtendedFileInfo{}, fmt.Errorf("source not found")
}
return files.FileInfoFaster(iteminfo.FileOptions{
Access: store.Access,
Username: "public", // Use public user for shares
Path: urlInfo.Path,
Modify: false, // Public shares don't allow modification
Source: sourceConfig.Name,
Expand: false,
})
}
// Handle regular authenticated resource
userScope, err := settings.GetScopeFromSourceName(d.user.Scopes, urlInfo.Source)
if err != nil {
return iteminfo.ExtendedFileInfo{}, err
}
return files.FileInfoFaster(iteminfo.FileOptions{
Access: store.Access,
Username: d.user.Username,
Path: utils.JoinPathAsUnix(userScope, urlInfo.Path),
Modify: d.user.Permissions.Modify,
Source: urlInfo.Source,
Expand: false,
})
}
// buildClientConfig constructs the OnlyOffice client configuration
func buildClientConfig(urlInfo *ParsedURLInfo, fileInfo iteminfo.ExtendedFileInfo, id string, d *requestContext) (map[string]interface{}, error) {
// Extract file type
fileType := extractFileType(fileInfo.Name)
// Determine theme
theme := "light"
if d.user.DarkMode {
theme = "dark"
}
canEdit := iteminfo.CanEditOnlyOffice(d.user.Permissions.Modify, fileType)
// Determine editing permissions
canEdit := determineEditPermissions(urlInfo.IsPublicShare, d.user.Permissions.Modify, fileType)
mode := "view"
if canEdit {
mode = "edit"
}
callbackURL := fmt.Sprintf("%v/api/onlyoffice/callback?path=%v&auth=%v", urlFirst, encodedPath, d.token)
// Build callback URL
callbackURL := fmt.Sprintf("%v/api/onlyoffice/callback?path=%v&auth=%v",
urlInfo.BaseURL, urlInfo.EncodedPath, d.token)
// Build client configuration
clientConfig := map[string]interface{}{
"document": map[string]interface{}{
"fileType": fileType,
"key": id,
"title": fileInfo.Name,
"url": url + "&auth=" + d.token,
"url": urlInfo.DecodedURL + "&auth=" + d.token,
"permissions": map[string]interface{}{
"edit": canEdit,
"download": true,
@@ -119,15 +336,57 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
"mode": mode,
},
}
// Add JWT token if secret is configured
if settings.Config.Integrations.OnlyOffice.Secret != "" {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(clientConfig))
signature, err := token.SignedString([]byte(settings.Config.Integrations.OnlyOffice.Secret))
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("failed to sign JWT")
return nil, fmt.Errorf("failed to sign JWT")
}
clientConfig["token"] = signature
}
return renderJSON(w, r, clientConfig)
return clientConfig, nil
}
// extractFileType safely extracts the file extension
func extractFileType(filename string) string {
parts := strings.Split(filename, ".")
if len(parts) < 2 {
return ""
}
return parts[len(parts)-1]
}
// determineEditPermissions determines if the user can edit the document
func determineEditPermissions(isPublicShare bool, userCanModify bool, fileType string) bool {
if isPublicShare {
return false // Public shares typically don't allow editing
}
return iteminfo.CanEditOnlyOffice(userCanModify, fileType)
}
// parseCallbackPath parses the encoded path from callback requests
func parseCallbackPath(encodedPath string) (source, path string, err error) {
const delimiter = "::"
parts := strings.Split(encodedPath, delimiter)
if len(parts) != 2 {
return "", "", fmt.Errorf("invalid callback path format: expected source::path")
}
source = strings.TrimSpace(parts[0])
if source == "" {
return "", "", fmt.Errorf("source cannot be empty")
}
path, err = url.QueryUnescape(parts[1])
if err != nil {
return "", "", fmt.Errorf("invalid path encoding: %v", err)
}
return source, path, nil
}
func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
@@ -142,15 +401,13 @@ func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *reques
}
encodedPath := r.URL.Query().Get("path")
pathParts := strings.Split(encodedPath, "::")
if len(pathParts) < 2 {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
if encodedPath == "" {
return http.StatusBadRequest, fmt.Errorf("missing path parameter")
}
source := pathParts[0]
// Decode the URL-encoded path
path, err := url.QueryUnescape(pathParts[1])
source, path, err := parseCallbackPath(encodedPath)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
return http.StatusBadRequest, err
}
if data.Status == onlyOfficeStatusDocumentClosedWithChanges ||
data.Status == onlyOfficeStatusDocumentClosedWithNoChanges {
@@ -165,6 +422,12 @@ func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *reques
if data.Status == onlyOfficeStatusDocumentClosedWithChanges ||
data.Status == onlyOfficeStatusForceSaveWhileDocumentStillOpen {
// Public shares typically don't allow modification
if source == "public" {
return http.StatusForbidden, fmt.Errorf("public shares do not allow modifications")
}
if !d.user.Permissions.Modify {
return http.StatusForbidden, nil
}
-1
View File
@@ -108,5 +108,4 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
return
}
handleWithStaticData(w, r, "index.html", "text/html")
}
+66 -1
View File
@@ -3,7 +3,14 @@
package indexing
import "syscall"
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"syscall"
)
func CheckWindowsHidden(realpath string) bool {
// Non-Windows platforms don't support hidden attributes in the same way
@@ -11,6 +18,64 @@ func CheckWindowsHidden(realpath string) bool {
}
func getPartitionSize(path string) (uint64, error) {
// Get the device containing the path
var stat syscall.Stat_t
err := syscall.Stat(path, &stat)
if err != nil {
return 0, err
}
// Get the device number
dev := stat.Dev
major := (dev >> 8) & 0xff
minor := dev & 0xff
// Read /proc/partitions to find the partition size
file, err := os.Open("/proc/partitions")
if err != nil {
// Fallback to filesystem size if we can't read /proc/partitions
return getFilesystemSize(path)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "major") {
continue
}
fields := strings.Fields(line)
if len(fields) < 4 {
continue
}
// Parse major and minor device numbers
majorNum, err1 := strconv.ParseUint(fields[0], 10, 32)
minorNum, err2 := strconv.ParseUint(fields[1], 10, 32)
blocks, err3 := strconv.ParseUint(fields[2], 10, 64)
if err1 != nil || err2 != nil || err3 != nil {
continue
}
// Check if this matches our device
if uint32(majorNum) == uint32(major) && uint32(minorNum) == uint32(minor) {
// blocks are in 1024-byte units in /proc/partitions
return blocks * 1024, nil
}
}
if err := scanner.Err(); err != nil {
return 0, fmt.Errorf("error reading /proc/partitions: %v", err)
}
// Fallback to filesystem size if partition not found
return getFilesystemSize(path)
}
// getFilesystemSize returns the filesystem size (fallback method)
func getFilesystemSize(path string) (uint64, error) {
var stat syscall.Statfs_t
err := syscall.Statfs(path, &stat)
if err != nil {
+94
View File
@@ -0,0 +1,94 @@
//go:build !windows
// +build !windows
package indexing
import (
"fmt"
"testing"
)
func TestPartitionSizeAccuracy(t *testing.T) {
// Test with root directory
partitionSize, err := getPartitionSize("/")
if err != nil {
t.Fatalf("getPartitionSize failed: %v", err)
}
// Get filesystem size for comparison
filesystemSize, err := getFilesystemSize("/")
if err != nil {
t.Fatalf("getFilesystemSize failed: %v", err)
}
t.Logf("Partition size: %d bytes (%.2f GB)", partitionSize, float64(partitionSize)/1e9)
t.Logf("Filesystem size: %d bytes (%.2f GB)", filesystemSize, float64(filesystemSize)/1e9)
// Partition size should be >= filesystem size
if partitionSize < filesystemSize {
t.Errorf("Partition size (%d) should be >= filesystem size (%d)", partitionSize, filesystemSize)
}
// Calculate the difference percentage
if partitionSize > 0 {
diff := float64(partitionSize-filesystemSize) / float64(partitionSize) * 100
t.Logf("Difference: %.2f%% (partition is larger)", diff)
// For most filesystems, partition should be at least 2% larger due to overhead
if diff < 1.0 {
t.Logf("Warning: Very small difference between partition and filesystem size")
}
}
}
func TestPartitionSizeConsistency(t *testing.T) {
// Test multiple paths on the same partition should return same size
paths := []string{"/", "/tmp", "/usr"}
var lastSize uint64
for i, path := range paths {
size, err := getPartitionSize(path)
if err != nil {
t.Logf("Path %s failed (might be on different partition): %v", path, err)
continue
}
if i == 0 {
lastSize = size
} else {
// Allow for small differences due to different partitions
if size != lastSize {
t.Logf("Path %s has different partition size: %d vs %d", path, size, lastSize)
}
}
}
}
// Benchmark to compare performance
func BenchmarkGetPartitionSize(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := getPartitionSize("/")
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkGetFilesystemSize(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := getFilesystemSize("/")
if err != nil {
b.Fatal(err)
}
}
}
func Example_getPartitionSize() {
size, err := getPartitionSize("/")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Partition size: %.2f GB\n", float64(size)/1e9)
}
+1 -1
View File
@@ -70,7 +70,7 @@ export function download(format, files, shareHash = "") {
}
}
fileargs = fileargs.slice(0, -2) // remove trailing "||"
const apiPath = getApiPath(shareHash == "" ? 'api/raw' : 'api/public/dl', {
const apiPath = getApiPath(shareHash == "" ? 'api/raw' : 'public/api/raw', {
files: fileargs,
algo: format,
hash: shareHash
+6 -3
View File
@@ -1,9 +1,12 @@
import * as filesApi from "./files";
import * as shareApi from "./share";
import * as publicApi from "./public";
import * as usersApi from "./users";
import * as settingsApi from "./settings";
import * as publicApi from "./public";
import * as accessApi from "./access";
import search from "./search";
export { filesApi, shareApi, usersApi, settingsApi, publicApi, search, accessApi };
// Note: shareApi has been consolidated into publicApi
export { filesApi, publicApi, usersApi, settingsApi, search, accessApi };
// For backward compatibility, export publicApi as shareApi as well
export { publicApi as shareApi };
+91 -10
View File
@@ -1,11 +1,20 @@
import { adjustedData } from "./utils";
import { getApiPath } from "@/utils/url.js";
import { fetchURL, fetchJSON, adjustedData } from "./utils";
import { notify } from "@/notify";
import { getApiPath, removePrefix } from "@/utils/url.js";
import { externalUrl, baseURL } from "@/utils/constants";
// ============================================================================
// PUBLIC API ENDPOINTS (hash-based authentication)
// ============================================================================
// Fetch public share data
export async function fetchPub(path, hash, password = "") {
const params = { path, hash }
const apiPath = getApiPath("api/public/share", params);
export async function fetchPub(path, hash, password = "", content = false) {
const params = {
path,
hash,
...(content && { content: 'true' })
}
const apiPath = getApiPath("public/api/shared", params);
const response = await fetch(apiPath, {
headers: {
"X-SHARE-PASSWORD": password ? encodeURIComponent(password) : "",
@@ -18,14 +27,14 @@ export async function fetchPub(path, hash, password = "") {
throw error;
}
let data = await response.json()
const adjusted = adjustedData(data, `/share/${hash}${path}`);
const adjusted = adjustedData(data, `/public/share/${hash}${path}`);
return adjusted
}
// Get the public user data
export async function getPublicUser() {
try {
const apiPath = getApiPath("api/public/publicUser");
const apiPath = getApiPath("public/api/user");
const response = await fetch(apiPath);
return response.json();
} catch (err) {
@@ -35,7 +44,7 @@ export async function getPublicUser() {
}
// Generate a download URL
export function getDownloadURL(share,files) {
export function getDownloadURL(share, files) {
const params = {
path: share.path,
files: files,
@@ -43,6 +52,78 @@ export function getDownloadURL(share,files) {
token: share.token,
...(share.inline && { inline: 'true' })
}
const apiPath = getApiPath("api/public/dl", params);
return window.origin+apiPath
const apiPath = getApiPath("public/api/raw", params);
return window.origin + apiPath
}
// Generate a preview URL for public shares
export function getPreviewURL(hash, path) {
try {
const params = {
path: encodeURIComponent(path),
size: 'small',
hash: hash,
inline: 'true'
}
const apiPath = getApiPath('public/api/preview', params)
return window.origin + apiPath
} catch (err) {
notify.showError(err.message || 'Error getting preview URL')
throw err
}
}
// ============================================================================
// SHARE MANAGEMENT API (permission-based authentication)
// ============================================================================
// List all shares
export async function list() {
const apiPath = getApiPath("public/shares");
return fetchJSON(apiPath);
}
// Get share information
export async function get(path, source) {
try {
const params = { path: encodeURIComponent(path), source: encodeURIComponent(source) };
const apiPath = getApiPath("public/share", params);
let data = fetchJSON(apiPath);
return adjustedData(data, path);
} catch (err) {
notify.showError(err.message || "Error fetching data");
throw err;
}
}
// Remove/delete a share
export async function remove(hash) {
const params = { hash };
const apiPath = getApiPath("public/share", params);
await fetchURL(apiPath, {
method: "DELETE",
});
}
// Create a new share
export async function create(path, source, password = "", expires = "", unit = "hours") {
const params = { path: encodeURIComponent(path), source: encodeURIComponent(source) };
const apiPath = getApiPath("public/share", params);
let body = "{}";
if (password != "" || expires !== "" || unit !== "hours") {
body = JSON.stringify({ password: password, expires: expires, unit: unit });
}
return fetchJSON(apiPath, {
method: "POST",
body: body,
});
}
// Get the shareable URL for a share
export function getShareURL(share) {
if (externalUrl) {
const apiPath = getApiPath(`public/share/${share.hash}`)
return externalUrl + removePrefix(apiPath, baseURL);
}
return window.origin + getApiPath(`public/share/${share.hash}`);
}
-66
View File
@@ -1,66 +0,0 @@
import { fetchURL, fetchJSON, adjustedData } from "./utils";
import { notify } from "@/notify";
import { getApiPath,removePrefix } from "@/utils/url.js";
import { externalUrl,baseURL } from "@/utils/constants";
export async function list() {
const apiPath = getApiPath("api/shares");
return fetchJSON(apiPath);
}
export async function get(path, source) {
try {
const params = { path: encodeURIComponent(path), source: encodeURIComponent(source) };
const apiPath = getApiPath("api/share",params);
let data = fetchJSON(apiPath);
return adjustedData(data, path);
} catch (err) {
notify.showError(err.message || "Error fetching data");
throw err;
}
}
export async function remove(hash) {
const params = { hash };
const apiPath = getApiPath("api/share",params);
await fetchURL(apiPath, {
method: "DELETE",
});
}
export async function create(path, source, password = "", expires = "", unit = "hours") {
const params = { path: encodeURIComponent(path), source: encodeURIComponent(source) };
const apiPath = getApiPath("api/share",params);
let body = "{}";
if (password != "" || expires !== "" || unit !== "hours") {
body = JSON.stringify({ password: password, expires: expires, unit: unit });
}
return fetchJSON(apiPath, {
method: "POST",
body: body,
});
}
export function getShareURL(share) {
if (externalUrl) {
const apiPath = getApiPath(`share/${share.hash}`)
return externalUrl + removePrefix(apiPath, baseURL);
}
return window.origin+getApiPath(`share/${share.hash}`);
}
export function getPreviewURL(hash, path) {
try {
const params = {
path: encodeURIComponent(path),
size: 'small',
hash: hash,
inline: 'true'
}
const apiPath = getApiPath('api/public/preview', params)
return window.origin + apiPath
} catch (err) {
notify.showError(err.message || 'Error getting preview URL')
throw err
}
}
+2 -2
View File
@@ -67,7 +67,7 @@ export default {
return state.req.hasUpdate;
},
addPadding() {
return getters.isStickySidebar() || getters.currentView() == "share";
return getters.isStickySidebar() || getters.isShare();
},
isCardView() {
return getters.isCardView();
@@ -112,7 +112,7 @@ export default {
},
methods: {
updatePaths() {
if (getters.currentView() == "share") {
if (getters.isShare()) {
this.base = getters.sharePathBase();
this.path = getters.routePath(this.base);
} else {
+1 -1
View File
@@ -207,7 +207,7 @@ export default {
state.user?.permissions &&
state.user?.permissions.share &&
state.user.username != "publicUser" &&
getters.currentView() != "share" &&
!getters.isShare() &&
!this.isSearchActive
);
},
+2 -1
View File
@@ -101,7 +101,8 @@ export default {
if (!this.isValNumeric) return 100;
if (this.max <= 0) return 0;
var pct = (this.val / this.max) * 100;
return Math.max(0, Math.min(pct.toFixed(2), 100));
var finalPct = Math.max(0, Math.min(pct.toFixed(2), 100));
return finalPct < 7 ? 0 : finalPct;
},
displayed_text() {
if (!this.isValNumeric) return this.val;
@@ -152,11 +152,9 @@ export default {
return "";
}
let path = url.removeTrailingSlash(state.req.path) + "/" + this.name;
if (getters.currentView() == "share") {
let urlPath = getters.routePath("share");
// Step 1: Split the path by '/'
const hash = urlPath.split("/")[1];
return shareApi.getPreviewURL(hash, path, state.req.modified);
if (getters.isShare()) {
let urlPath = getters.getSharePath() + "/" + this.name;
return shareApi.getPreviewURL(state.share.hash, urlPath, state.req.modified);
}
return filesApi.getPreviewURL(state.req.source, path, state.req.modified);
},
+167
View File
@@ -0,0 +1,167 @@
<template>
<div class="share card">
<div class="share__box">
<div class="share__box__element">
<strong>{{ $t("prompts.displayName") }}</strong> {{ req.name }}
</div>
<div class="share__box__element" :title="modTime">
<strong>{{ $t("prompts.lastModified") }}:</strong> {{ humanTime }} <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
</div>
<div class="share__box__element">
<strong>{{ $t("prompts.size") }}:</strong> {{ humanSize }} <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
</div>
<div class="share__box__element share__box__center">
<a target="_blank" :href="getLink(false)" class="button button--flat">
<div>
<i aria-label="share-download-all" class="material-icons">file_download</i>{{ $t("buttons.download") }}
</div>
</a>
<a target="_blank" :href="getLink(true)" class="button button--flat" v-if="req.type != 'directory'">
<div>
<i class="material-icons">open_in_new</i>{{ $t("buttons.openFile") }}
</div>
</a>
</div>
<div v-if="req.type" class="share__box__element share__box__center">
<qrcode-vue :value="getLink(false)" size="200" level="M"></qrcode-vue>
</div>
</div>
</div>
</template>
<script>
import { publicApi } from "@/api";
import { state, getters } from "@/store";
import { getHumanReadableFilesize } from "@/utils/filesizes";
import { getTypeInfo } from "@/utils/mimetype";
import QrcodeVue from "qrcode.vue";
export default {
name: "ShareInfo",
components: {
QrcodeVue,
},
props: {
hash: {
type: String,
required: true,
},
token: {
type: String,
required: true,
},
subPath: {
type: String,
default: "/",
},
},
computed: {
req() {
return state.req;
},
humanSize() {
if (!state.req.modified) return "";
if (state.req.type == "directory") {
return state.req.items.length + " items (" + getHumanReadableFilesize(state.req.size) + ")";
}
return getHumanReadableFilesize(state.req.size);
},
humanTime() {
if (!state.req.modified) return "";
return getters.getTime(state.req.modified);
},
modTime() {
return new Date(Date.parse(state.req.modified)).toLocaleString();
},
isImage() {
return state.req.type ? getTypeInfo(state.req.type).simpleType === "image" : false;
},
isMedia() {
if (!state.req.type) return false;
return (
getTypeInfo(state.req.type).simpleType === "video" ||
getTypeInfo(state.req.type).simpleType === "audio"
);
},
},
methods: {
getLink(inline = false) {
return publicApi.getDownloadURL({
path: "/",
hash: state.share.hash,
token: state.share.token,
inline: inline,
});
},
},
};
</script>
<style scoped>
.share {
display: flex;
}
.share__box {
width: 100%;
}
.share__box__header {
font-size: 1.2em;
font-weight: bold;
text-align: center;
padding-bottom: 1em;
border-bottom: 1px solid #eee;
margin-bottom: 1em;
}
.share__box__element {
margin: 0.5em 0;
}
.share__box__element canvas {
border-style: solid;
}
.share__box__center {
text-align: center;
}
.share__box__icon {
font-size: 4em;
color: #6c7b7f;
}
.share__box__icon i {
font-size: 4em;
}
.share__box__icon img,
.share__box__icon video {
max-width: 100%;
height: auto;
border-radius: 0.3em;
}
.button {
display: inline-flex;
align-items: center;
gap: 0.5em;
margin: 0.25em;
}
.button i {
font-size: 1.2em;
}
/* Dark mode support */
.dark-mode .share__box {
background: var(--surfacePrimary);
color: var(--textPrimary);
}
.dark-mode .share__box__header {
border-color: var(--divider);
}
</style>
+51
View File
@@ -0,0 +1,51 @@
<template>
<div class="share-sidebar">
<div class="share-sidebar__content">
<ShareInfo
:hash="hash"
:token="token"
:sub-path="subPath"
/>
</div>
</div>
</template>
<script>
import ShareInfo from "@/components/files/ShareInfo.vue";
export default {
name: "SidebarShare",
components: {
ShareInfo,
},
props: {
hash: {
type: String,
required: true,
},
token: {
type: String,
required: true,
},
subPath: {
type: String,
default: "/",
},
},
};
</script>
<style scoped>
.share-sidebar {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.share-sidebar__content {
flex: 1;
overflow: auto;
}
</style>
+5 -1
View File
@@ -8,7 +8,8 @@
<i @click="setSeenUpdate" aria-label="close-banner" class="material-icons">close</i>
</div>
<SidebarSettings v-if="isSettings"></SidebarSettings>
<SidebarGeneral v-else-if="isLoggedIn"></SidebarGeneral>
<SidebarGeneral v-if="!isSettings"></SidebarGeneral>
<SidebarShare v-if="isShare"></SidebarShare>
<div class="buffer"></div>
<div class="credits">
@@ -27,12 +28,14 @@ import { externalLinks, name, updateAvailable } from "@/utils/constants";
import { getters, mutations, state } from "@/store"; // Import your custom store
import SidebarGeneral from "./General.vue";
import SidebarSettings from "./Settings.vue";
import SidebarShare from "./Share.vue";
export default {
name: "sidebar",
components: {
SidebarGeneral,
SidebarSettings,
SidebarShare,
},
data() {
return {
@@ -45,6 +48,7 @@ export default {
mutations.setSeenUpdate(localStorage.getItem("seenUpdate"));
},
computed: {
isShare: () => getters.isShare(),
releaseUrl: () => updateAvailable,
isDarkMode: () => getters.isDarkMode(),
isLoggedIn: () => getters.isLoggedIn(),
-82
View File
@@ -1,82 +0,0 @@
.share {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: flex-start;
}
@media (max-width: 800px) {
.share {
display: block;
}
}
.share__box {
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
background: #fff;
border-radius: 1em;
margin: 5px;
overflow: hidden;
}
.share__box__header {
padding: 1em;
text-align: center;
}
.share__box__icon i {
font-size: 10em;
color: #40c4ff;
}
.share__box__center {
text-align: center;
}
.share__box__info {
flex: 1 1 18em;
}
.share__box__element {
padding: 1em;
border-top: 1px solid rgba(0, 0, 0, 0.1);
word-break: break-all;
}
.share__box__element .button {
display: inline-block;
}
.share__box__element .button i {
display: block;
margin-bottom: 4px;
}
.share__box__items {
text-align: left;
flex: 10 0 25em;
}
.share__box__items #listingView.list .item {
cursor: pointer;
border-left: 0;
border-right: 0;
border-bottom: 0;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.share__box__items #listingView.list .item .name {
width: 50%;
}
.share__box__items #listingView.list .item .modified {
width: 25%;
}
.share__wrong__password {
background: var(--red);
color: #fff;
padding: .5em;
text-align: center;
animation: .2s opac forwards;
}
-1
View File
@@ -2,7 +2,6 @@
@import "./_variables.css";
@import "./_buttons.css";
@import "./_inputs.css";
@import "./_share.css";
@import "./fonts.css";
@import "./base.css";
@import "./header.css";
+13 -2
View File
@@ -2,7 +2,6 @@ import { RouteLocation, createRouter, createWebHistory } from "vue-router";
import Login from "@/views/Login.vue";
import Layout from "@/views/Layout.vue";
import Files from "@/views/Files.vue";
import Share from "@/views/Share.vue";
import Settings from "@/views/Settings.vue";
import Errors from "@/views/Errors.vue";
import { baseURL, name, oidcAvailable, passwordAvailable } from "@/utils/constants";
@@ -15,6 +14,7 @@ import i18n from "@/i18n";
const titles = {
Login: "sidebar.login",
Share: "buttons.share",
PublicShare: "buttons.share",
Files: "general.files",
Settings: "sidebar.settings",
ProfileSettings: "settings.profileSettings",
@@ -40,7 +40,18 @@ const routes = [
{
path: ":path*",
name: "Share",
component: Share,
component: Files,
},
],
},
{
path: "/public/share",
component: Layout,
children: [
{
path: ":path*",
name: "PublicShare",
component: Files,
},
],
},
+24 -15
View File
@@ -1,4 +1,4 @@
import { removePrefix, buildItemUrl } from '@/utils/url.js'
import { removePrefix, buildItemUrl, removeLeadingSlash } from '@/utils/url.js'
import { getFileExtension } from '@/utils/files.js'
import { state, mutations } from '@/store'
import { noAuth } from '@/utils/constants.js'
@@ -40,7 +40,10 @@ export const getters = {
isMobile: () => state.isMobile,
isLoading: () => Object.keys(state.loading).length > 0,
isSettings: () => getters.currentView() === 'settings',
isShare: () => getters.currentView() === 'share',
isShare: () => {
const pathname = getters.routePath()
return pathname.startsWith(`/share`) || pathname.startsWith(`/public/share`)
},
isDarkMode: () => {
if (state.user == null) {
return true
@@ -145,10 +148,9 @@ export const getters = {
'editor'
]
let visible =
(state.showSidebar || getters.isStickySidebar()) &&
state.user.username != 'publicUser'
if (currentView == 'share') {
visible = false
(state.showSidebar || getters.isStickySidebar())
if (getters.isShare() && !state.isMobile) {
visible = true
}
if (
typeof getters.currentPromptName() === 'string' &&
@@ -167,7 +169,10 @@ export const getters = {
if (currentView == 'settings') {
sticky = true
}
if (currentView == null && !getters.isLoading() && getters.isShare()) {
if (currentView == null && !getters.isLoading()) {
sticky = true
}
if (getters.isShare()) {
sticky = true
}
if (getters.isMobile()) {
@@ -188,21 +193,25 @@ export const getters = {
routePath: (trimModifier = '') => {
return removePrefix(state.route.path, trimModifier)
},
sharePathBase: () => {
let urlPath = getters.routePath('share')
// Step 1: Split the path by '/'
shareHash: () => {
let urlPath = getters.routePath('public/share')
let parts = urlPath.split('/')
// Step 2: Assign hash to the second part (index 2) and join the rest for subPath
return '/share/' + parts[1] + '/'
return parts[1]
},
sharePathBase: () => {
return '/public/share/' + getters.shareHash() + '/'
},
getSharePath: () => {
let urlPath = getters.routePath('public/share')
let parts = urlPath.split('/')
return "/" + removeLeadingSlash(parts.slice(2).join('/'))
},
currentView: () => {
let listingView = null
const pathname = getters.routePath()
if (pathname.startsWith(`/settings`)) {
listingView = 'settings'
} else if (pathname.startsWith(`/share`)) {
listingView = 'share'
} else if (pathname.startsWith(`/files`)) {
} else {
if (state.req.type !== undefined) {
const ext = "." + state.req.name.split(".").pop().toLowerCase(); // Ensure lowercase and dot
if (state.user.disableViewingExt?.includes(ext)) {
+21 -5
View File
@@ -220,6 +220,7 @@ export const mutations = {
emitStateChanged();
},
setCurrentUser: (value) => {
console.log('setCurrentUser', value);
try {
// If value is null or undefined, emit state change and exit early
if (!value) {
@@ -248,6 +249,18 @@ export const mutations = {
state.jwt = value;
emitStateChanged();
},
setShareData: (shareData) => {
state.share = { ...state.share, ...shareData };
emitStateChanged();
},
clearShareData: () => {
state.share = {
hash: null,
token: "",
subPath: "",
};
emitStateChanged();
},
setSession: (value) => {
state.sessionId = value;
emitStateChanged();
@@ -352,11 +365,8 @@ export const mutations = {
}
let sortby = "name"
let asc = true
if (state.user.username && state.user?.username != "publicUser") {
sortby = state.user.sorting.by;
asc = state.user.sorting.asc;
}
sortby = state.user.sorting.by;
asc = state.user.sorting.asc;
// Separate directories and files
const dirs = value.items.filter((item) => item.type === 'directory');
const files = value.items.filter((item) => item.type !== 'directory');
@@ -379,6 +389,9 @@ export const mutations = {
emitStateChanged();
},
updateListingSortConfig: ({ field, asc }) => {
if (!state.user.sorting) {
state.user.sorting = {};
}
state.user.sorting.by = field;
state.user.sorting.asc = asc;
emitStateChanged();
@@ -418,6 +431,9 @@ export const mutations = {
emitStateChanged();
},
setMaxConcurrentUpload: (value) => {
if (!state.user.fileLoading) {
state.user.fileLoading = {};
}
state.user.fileLoading.maxConcurrentUpload = value;
emitStateChanged();
},
+5
View File
@@ -25,6 +25,11 @@ export const state = reactive({
realtimeActive: undefined,
realtimeDownCount: 0,
popupPreviewSource: "",
share: {
hash: null,
token: "",
subPath: "",
},
sources: {
current: "",
count: 1,
+2 -7
View File
@@ -11,14 +11,9 @@ export default function downloadFiles(items) {
// map the index to state.req.items
items = items.map(i => state.req.items[i]);
}
const currentView = getters.currentView();
if (currentView === "share") {
let urlPath = getters.routePath("share");
let parts = urlPath.split("/");
const hash = parts[1];
if (getters.isShare()) {
// Perform download without opening a new window
startDownload(null, items, hash);
startDownload(null, items, state.share.hash);
return;
}
+7
View File
@@ -1,5 +1,12 @@
export function getTypeInfo(mimeType) {
if (!mimeType) {
return {
classes: "material-icons",
materialIcon: "file",
simpleType: "file",
};
}
if (mimeType === "directory" || mimeType === "application/vnd.google-apps.folder") {
return {
classes: "primary-icons material-icons",
+1 -1
View File
@@ -156,7 +156,7 @@ export function goToItem(source, path, previousHash, shareHash) {
let newPath = encodedPath(path);
let fullPath;
if (shareHash) {
fullPath = `/share/${shareHash}${newPath}`;
fullPath = `/public/share/${shareHash}${newPath}`;
router.push({ path: fullPath });
return;
}
+197 -39
View File
@@ -1,7 +1,35 @@
<template>
<div>
<breadcrumbs v-if="showBreadCrumbs" />
<errors v-if="error" :errorCode="error.status" />
<breadcrumbs v-if="showBreadCrumbs" :base="isShare ? `/share/${shareHash}` : undefined" />
<!-- Share password prompt -->
<div v-if="isShare && error && error.status === 401" class="card floating" id="password">
<div v-if="attemptedPasswordLogin" class="share__wrong__password">
{{ $t("login.wrongCredentials") }}
</div>
<div class="card-title">
<h2>{{ $t("general.password") }}</h2>
</div>
<div class="card-content">
<input
v-focus
type="password"
:placeholder="$t('general.password')"
v-model="sharePassword"
@keyup.enter="fetchData"
/>
</div>
<div class="card-action">
<button
class="button button--flat"
@click="fetchData"
:aria-label="$t('buttons.submit')"
:title="$t('buttons.submit')"
>
{{ $t("buttons.submit") }}
</button>
</div>
</div>
<errors v-else-if="error" :errorCode="error.status" />
<component v-else-if="currentViewLoaded" :is="currentView"></component>
<div v-else>
<h2 class="message delayed">
@@ -18,7 +46,8 @@
</template>
<script>
import { filesApi } from "@/api";
import { filesApi, publicApi } from "@/api";
import { notify } from "@/notify";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import Errors from "@/views/Errors.vue";
import Preview from "@/views/files/Preview.vue";
@@ -56,9 +85,18 @@ export default {
lastPath: "",
lastHash: "",
popupSource: "",
// Share-specific data
sharePassword: "",
attemptedPasswordLogin: false,
shareHash: null,
shareSubPath: "",
shareToken: "",
};
},
computed: {
isShare() {
return getters.isShare();
},
popupEnabled() {
return state.user.preview.popup;
},
@@ -115,15 +153,132 @@ export default {
if (state.deletedItem) {
return
}
if (!state.user.sorting) {
mutations.updateListingSortConfig({
field: "name",
asc: true,
});
}
// Set loading and reset error
mutations.setLoading(this.isShare ? "share" : "files", true);
this.error = null;
mutations.setReload(false);
try {
if (this.isShare) {
await this.fetchShareData();
console.log('Share data after fetch:')
} else {
await this.fetchFilesData();
}
} catch (e) {
notify.showError(e.message);
this.error = e;
mutations.replaceRequest({});
if (e.status === 404) {
router.push({ name: "notFound" });
} else if (e.status === 403) {
router.push({ name: "forbidden" });
} else if (e.status === 401 && this.isShare) {
// Handle share password requirement
this.attemptedPasswordLogin = this.sharePassword !== "";
} else {
router.push({ name: "error" });
}
} finally {
mutations.setLoading(this.isShare ? "share" : "files", false);
}
setTimeout(() => {
this.scrollToHash();
}, 25);
this.lastPath = state.route.path;
},
async fetchShareData() {
// Parse share route
let urlPath = getters.routePath('public/share')
let parts = urlPath.split("/");
this.shareHash = parts[1]
this.shareSubPath = "/" + parts.slice(2).join("/");
console.log('Share data:', {
hash: this.shareHash,
subPath: this.shareSubPath,
password: this.sharePassword
});
// Store share data in state for use by components
mutations.setShareData({
hash: this.shareHash,
token: this.shareToken,
subPath: this.shareSubPath,
});
// Handle password
if (this.sharePassword === "" || this.sharePassword === null) {
this.sharePassword = localStorage.getItem("sharepass:" + this.shareHash);
} else {
localStorage.setItem("sharepass:" + this.shareHash, this.sharePassword);
}
// Get public user if not logged in
if (!getters.isLoggedIn()) {
let userData = await publicApi.getPublicUser();
mutations.setCurrentUser(userData);
}
mutations.resetSelected();
mutations.setMultiple(false);
mutations.closeHovers();
// Fetch share data
let file = await publicApi.fetchPub(this.shareSubPath, this.shareHash, this.sharePassword);
file.hash = this.shareHash;
this.shareToken = file.token;
// If not a directory, fetch content for preview components
if (file.type != "directory") {
const content = !getters.fileViewingDisabled(file.name);
console.log('Share file content fetch debug:', {
name: file.name,
type: file.type,
fileViewingDisabled: getters.fileViewingDisabled(file.name),
content: content,
disableViewingExt: state.user.disableViewingExt,
disableOfficePreviewExt: state.user.disableOfficePreviewExt
});
file = await publicApi.fetchPub(this.shareSubPath, this.shareHash, this.sharePassword, content);
file.hash = this.shareHash;
this.shareToken = file.token;
console.log('Share file after content fetch:', {
hasContent: 'content' in file,
contentLength: file.content ? file.content.length : 0,
contentPreview: file.content ? file.content.substring(0, 50) + '...' : null
});
}
mutations.replaceRequest(file);
document.title = `${document.title} - ${file.name}`;
},
async fetchFilesData() {
if (!getters.isLoggedIn()) {
return;
}
// Clear share data when accessing files
mutations.clearShareData();
const routePath = url.removeTrailingSlash(getters.routePath(`${baseURL}files`));
const rootRoute =
routePath == "/files" ||
routePath == "/files/" ||
routePath == "" ||
routePath == "/";
// lets redirect if multiple sources and user went to /files/
if (state.serverHasMultipleSources && rootRoute) {
const targetPath = `/files/${state.sources.current}`;
@@ -133,60 +288,63 @@ export default {
return;
}
}
const result = extractSourceFromPath(getters.routePath());
if (result.source === "") {
notify.showError($t("index.noSources"));
return;
}
this.lastHash = "";
// Set loading to true and reset the error.
mutations.setLoading("files", true);
this.error = null;
// Reset view information using mutations
mutations.setReload(false);
mutations.resetSelected();
let data = {};
try {
const fetchSource = decodeURIComponent(result.source);
const fetchPath = decodeURIComponent(result.path);
// Fetch initial data
let res = await filesApi.fetchFiles(fetchSource, fetchPath );
// If not a directory, fetch content
if (res.type != "directory") {
const content = !getters.fileViewingDisabled(res.name);
res = await filesApi.fetchFiles(res.source, res.path, content);
}
data = res;
if (state.sources.count > 1) {
mutations.setCurrentSource(data.source);
}
document.title = `${document.title} - ${res.name}`;
mutations.replaceRequest(data);
} catch (e) {
this.error = e;
mutations.replaceRequest({});
if (e.status === 404) {
router.push({ name: "notFound" });
} else if (e.status === 403) {
router.push({ name: "forbidden" });
} else {
router.push({ name: "error" });
}
} finally {
mutations.setLoading("files", false);
const fetchSource = decodeURIComponent(result.source);
const fetchPath = decodeURIComponent(result.path);
// Fetch initial data
let res = await filesApi.fetchFiles(fetchSource, fetchPath);
// If not a directory, fetch content
if (res.type != "directory") {
const content = !getters.fileViewingDisabled(res.name);
res = await filesApi.fetchFiles(res.source, res.path, content);
}
setTimeout(() => {
this.scrollToHash();
}, 25);
this.lastPath = state.route.path;
data = res;
if (state.sources.count > 1) {
mutations.setCurrentSource(data.source);
}
document.title = `${document.title} - ${res.name}`;
mutations.replaceRequest(data);
},
keyEvent(event) {
// F1!
if (event.keyCode === 112) {
event.preventDefault();
mutations.showHover("help"); // Use mutation
}
// Esc! - for shares, reset selection
if (this.isShare && event.keyCode === 27) {
if (getters.selectedCount() > 0) {
mutations.resetSelected();
}
}
},
},
};
</script>
<style scoped>
.share__wrong__password {
color: #ff4757;
text-align: center;
padding: 1em 0;
}
#password {
max-width: 400px;
margin: 2em auto;
}
</style>
+1 -1
View File
@@ -144,7 +144,7 @@ export default {
if (state.user.permissions.realtime) {
events.startSSE();
}
const maxUploads = state.user.fileLoading.maxConcurrentUpload;
const maxUploads = state.user.fileLoading?.maxConcurrentUpload || 0;
if (maxUploads > 10 || maxUploads < 1) {
mutations.setMaxConcurrentUpload(1);
}
-312
View File
@@ -1,312 +0,0 @@
<template>
<div>
<breadcrumbs :base="'/share/' + hash" />
<div v-if="loading">
<h2 class="message delayed">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
<span>{{ $t("files.loading") }}</span>
</h2>
</div>
<div v-else-if="error">
<div v-if="error.status === 401">
<div class="card floating" id="password">
<div v-if="attemptedPasswordLogin" class="share__wrong__password">
{{ $t("login.wrongCredentials") }}
</div>
<div class="card-title">
<h2>{{ $t("general.password") }}</h2>
</div>
<div class="card-content">
<input
v-focus
type="password"
:placeholder="$t('general.password')"
v-model="password"
@keyup.enter="fetchData"
/>
</div>
<div class="card-action">
<button
class="button button--flat"
@click="fetchData"
:aria-label="$t('buttons.submit')"
:title="$t('buttons.submit')"
>
{{ $t("buttons.submit") }}
</button>
</div>
</div>
</div>
<errors v-else :errorCode="error.status" />
</div>
<div v-else>
<div class="share">
<div class="share__box share__box__info">
<div class="share__box__header">
{{
req.type == "directory"
? $t("download.downloadFolder")
: $t("download.downloadFile")
}}
</div>
<div
v-if="isImage"
class="share__box__element share__box__center share__box__icon"
>
<img :src="getLink(true)" width="500px" />
</div>
<div
v-else-if="isMedia"
class="share__box__element share__box__center share__box__icon"
>
<video width="500" height="500" controls>
<source :src="getLink(true)" type="video/mp4" />
</video>
</div>
<div v-else class="share__box__element share__box__center share__box__icon">
<i class="material-icons">{{ icon }}</i>
</div>
<div class="share__box__element">
<strong>{{ $t("prompts.displayName") }}</strong> {{ req.name }}
</div>
<div class="share__box__element" :title="modTime">
<strong>{{ $t("prompts.lastModified") }}:</strong> {{ humanTime }} <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
</div>
<div class="share__box__element">
<strong>{{ $t("prompts.size") }}:</strong> {{ humanSize }} <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
</div>
<div class="share__box__element share__box__center">
<a target="_blank" :href="getLink(false)" class="button button--flat">
<div>
<i class="material-icons">file_download</i>{{ $t("buttons.download") }}
</div>
</a>
<a
target="_blank"
:href="getLink(true)"
class="button button--flat"
v-if="req.type != 'directory'"
>
<div>
<i class="material-icons">open_in_new</i>{{ $t("buttons.openFile") }}
</div>
</a>
</div>
<div class="share__box__element share__box__center">
<qrcode-vue :value="getLink(false)" size="200" level="M"></qrcode-vue>
</div>
</div>
<div
v-if="req.type == 'directory' && req.items.length > 0"
class="share__box share__box__items"
>
<div class="share__box__header" v-if="req.type == 'directory'">
{{ $t("general.files") }}
</div>
<div id="listingView" class="list file-icons">
<item
v-for="item in req.items"
:key="base64(item.name)"
v-bind:index="item.index"
v-bind:name="item.name"
v-bind:isDir="item.type == 'directory'"
v-bind:path="item.path"
v-bind:source="req.source"
v-bind:hash="hash"
v-bind:modified="item.modified"
v-bind:type="item.type"
v-bind:size="item.size"
readOnly
>
</item>
</div>
</div>
<div
v-else-if="req.type == 'directory' && req.items.length === 0"
class="share__box share__box__items"
>
<h2 class="message">
<i class="material-icons">sentiment_dissatisfied</i>
<span>{{ $t("files.lonely") }}</span>
</h2>
</div>
</div>
</div>
</div>
</template>
<script>
import { notify } from "@/notify";
import { getHumanReadableFilesize } from "@/utils/filesizes";
import { publicApi } from "@/api";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import Errors from "@/views/Errors.vue";
import QrcodeVue from "qrcode.vue";
import Item from "@/components/files/ListingItem.vue";
import Clipboard from "clipboard";
import { state, getters, mutations } from "@/store";
import { url } from "@/utils";
import { getTypeInfo } from "@/utils/mimetype";
export default {
name: "share",
components: {
Breadcrumbs,
Item,
QrcodeVue,
Errors,
},
data() {
return {
error: null,
password: "",
attemptedPasswordLogin: false,
hash: null,
subPath: "",
clip: null,
token: "",
};
},
watch: {
$route() {
this.fetchData();
},
},
created() {
this.fetchData();
},
mounted() {
if (state.locale == "") {
mutations.updateCurrentUser({
locale: this.$i18n.locale,
});
}
window.addEventListener("keydown", this.keyEvent);
this.clip = new Clipboard(".copy-clipboard");
this.clip.on("success", () => {
notify.showSuccess(this.$t("success.linkCopied"));
});
},
beforeUnmount() {
window.removeEventListener("keydown", this.keyEvent);
this.clip.destroy();
},
computed: {
setMultipleFalse() {
return mutations.setMultiple(false);
},
req() {
return state.req; // Access state directly from the store
},
loading() {
return getters.isLoading(); // Access state directly from the store
},
multiple() {
return state.multiple; // Access state directly from the store
},
selected() {
return state.selected; // Access state directly from the store
},
selectedCount() {
return getters.selectedCount(); // Access getter directly from the store
},
icon() {
if (state.req.type == "directory") return "folder";
if (getTypeInfo(state.req.type).simpleType == "image") return "insert_photo";
if (getTypeInfo(state.req.type).simpleType == "audio") return "volume_up";
if (getTypeInfo(state.req.type).simpleType == "video") return "movie";
return "insert_drive_file";
},
humanSize() {
if (state.req.type == "directory") {
return state.req.items.length;
}
return getHumanReadableFilesize(state.req.size);
},
humanTime() {
return getters.getTime(state.req.modified);
},
modTime() {
return new Date(Date.parse(state.req.modified)).toLocaleString();
},
isImage() {
return getTypeInfo(state.req.type).simpleType === "image";
},
isMedia() {
return (
getTypeInfo(state.req.type).simpleType === "video" ||
getTypeInfo(state.req.type).simpleType === "audio"
);
},
},
methods: {
getLink(inline = false) {
return publicApi.getDownloadURL({
path: this.subPath,
hash: this.hash,
token: this.token,
inline: inline,
});
},
base64(name) {
return url.base64Encode(name);
},
async fetchData() {
let urlPath = getters.routePath("share");
// Step 1: Split the path by '/'
let parts = urlPath.split("/");
// Step 2: Assign hash to the second part (index 2) and join the rest for subPath
this.hash = parts[1];
this.subPath = "/" + parts.slice(2).join("/");
// Set loading to true and reset the error.
mutations.setLoading("share", true);
this.error = null;
if (this.password == "" || this.password == null) {
this.password = localStorage.getItem("sharepass:" + this.hash);
} else {
localStorage.setItem("sharepass:" + this.hash, this.password);
}
// Reset view information.
if (!getters.isLoggedIn()) {
let userData = await publicApi.getPublicUser();
mutations.setCurrentUser(userData);
}
mutations.setReload(false);
mutations.resetSelected();
mutations.setMultiple(false);
mutations.closeHovers();
try {
let file = await publicApi.fetchPub(this.subPath, this.hash, this.password);
file.hash = this.hash;
this.token = file.token;
mutations.replaceRequest(file);
document.title = `${document.title} - ${file.name}`;
} catch (error) {
this.error = error;
}
mutations.setLoading("share", false);
},
keyEvent(event) {
// Esc!
if (event.keyCode === 27) {
// If we're on a listing, unselect all files and folders.
if (getters.selectedCount() > 0) {
mutations.resetSelected();
}
}
},
toggleMultipleSelection() {
mutations.setMultiple(!state.multiple);
},
download() {
downloadFiles(state.selected);
},
},
};
</script>
+3 -3
View File
@@ -1,7 +1,7 @@
<template>
<header v-if="!isOnlyOffice" :class="['flexbar', { 'dark-mode-header': isDarkMode }]">
<action
v-if="!isShare && !(disableNavButtons && isListingView)"
v-if="!(disableNavButtons && isListingView)"
icon="close_back"
:label="$t('buttons.close')"
:disabled="isDisabledMultiAction"
@@ -19,7 +19,7 @@
:disabled="isDisabled"
/>
<action
v-else-if="!isShare && !isListingView && !showQuickSave"
v-else-if="!isListingView && !showQuickSave"
:icon="iconName"
:disabled="noItems"
@click="toggleOverflow"
@@ -86,7 +86,7 @@ export default {
return icons[state.user.viewMode] || "grid_view";
},
isShare() {
return getters.currentView() == "share";
return getters.isShare();
},
noItems() {
return !state.contextMenuItems;
+15 -9
View File
@@ -9,8 +9,8 @@
<script>
import { defineComponent } from "vue";
import * as mammoth from "mammoth";
import { filesApi } from "@/api";
import { state,mutations } from "@/store";
import { filesApi, publicApi } from "@/api";
import { state, mutations, getters } from "@/store";
export default defineComponent({
name: "DocxViewer",
@@ -72,13 +72,19 @@ export default defineComponent({
this.error = "";
this.docxHtml = "";
console.log("[4] Getting download URL from filesApi...");
const downloadUrl = filesApi.getDownloadURL(
state.req.source,
state.req.path,
false,
true
);
console.log("[4] Getting download URL from API...");
const downloadUrl = getters.isShare()
? publicApi.getDownloadURL({
path: state.share.subPath,
hash: state.share.hash,
token: state.share.token,
})
: filesApi.getDownloadURL(
state.req.source,
state.req.path,
false,
true
);
if (!downloadUrl) {
throw new Error("Could not retrieve a valid download URL from the API.");
+9 -2
View File
@@ -104,7 +104,7 @@ export default {
notify.showError("Failed to initialize the editor. Please reload.");
}
},
handleEditorValueRequest() {
async handleEditorValueRequest() {
// Safety Check 2: Final verification before saving
if (state.req.name !== this.filename) {
// Corrected the error message to be more accurate
@@ -114,7 +114,14 @@ export default {
}
try {
if (this.editor) {
filesApi.put(state.req.source, state.req.path, this.editor.getValue());
if (getters.isShare()) {
// Shared files are read-only
notify.showError("Cannot save changes to shared files - they are read-only.");
return;
} else {
// Use regular files API for authenticated users
await filesApi.put(state.req.source, state.req.path, this.editor.getValue());
}
} else {
notify.showError("Editor instance is not initialized.");
return;
+14 -8
View File
@@ -16,8 +16,8 @@
<script lang="ts">
import { defineComponent } from "vue";
import ePub, { type Book, type Rendition } from "epubjs";
import { state,mutations } from "@/store"; // Assuming your store setup
import { filesApi } from "@/api"; // Assuming your api setup
import { state, mutations, getters } from "@/store"; // Assuming your store setup
import { filesApi, publicApi } from "@/api"; // Assuming your api setup
import { removeLastDir } from "@/utils/url"; // Assuming your utils setup
export default defineComponent({
@@ -41,12 +41,18 @@ export default defineComponent({
});
try {
// 1. Fetch the download URL for the EPUB file
const epubUrl = await filesApi.getDownloadURL(
state.req.source,
state.req.path,
false,
false
);
const epubUrl = getters.isShare()
? publicApi.getDownloadURL({
path: state.share.subPath,
hash: state.share.hash,
token: state.share.token,
})
: await filesApi.getDownloadURL(
state.req.source,
state.req.path,
false,
false
);
// 2. Initialize the EPUB book
this.book = ePub(epubUrl);
+33 -1
View File
@@ -1,5 +1,14 @@
<template>
<div class="no-select">
<!-- Share Info Component -->
<ShareInfo
v-if="showShareInfo"
:hash="state.share.hash"
:token="state.share.token"
:subPath="state.share.subPath"
/>
<div v-if="loading">
<h2 class="message delayed">
<div class="spinner">
@@ -112,6 +121,8 @@
v-bind:size="item.size"
v-bind:path="item.path"
v-bind:reducedOpacity="item.hidden || isDragging"
v-bind:hash="isShare ? state.share.hash : undefined"
:readOnly="isShare ? true : undefined"
/>
</div>
<div v-if="numFiles > 0">
@@ -132,6 +143,7 @@
v-bind:size="item.size"
v-bind:path="item.path"
v-bind:reducedOpacity="item.hidden || isDragging"
v-bind:hash="isShare ? state.share.hash : undefined"
/>
</div>
@@ -158,17 +170,22 @@
<script>
import downloadFiles from "@/utils/download";
import { filesApi } from "@/api";
import { notify } from "@/notify";
import { router } from "@/router";
import * as upload from "@/utils/upload";
import throttle from "@/utils/throttle";
import { state, mutations, getters } from "@/store";
import { url } from "@/utils";
import Item from "@/components/files/ListingItem.vue";
import ShareInfo from "@/components/files/ShareInfo.vue";
export default {
name: "listingView",
components: {
Item,
ShareInfo,
},
data() {
return {
@@ -225,6 +242,15 @@ export default {
},
},
computed: {
showShareInfo() {
return this.isShare && state.share.hash && state.isMobile;
},
isShare() {
return getters.isShare();
},
state() {
return state;
},
isDragging() {
return this.dragCounter > 0;
},
@@ -771,6 +797,13 @@ export default {
return;
}
mutations.setLoading("listing", true);
if (getters.isShare()) {
// Shared files don't support move/copy operations
mutations.setLoading("listing", false);
notify.showError("Move/copy operations are not supported for shared files.");
return;
}
let action = async (overwrite, rename) => {
await filesApi.moveCopy(items, "copy", overwrite, rename);
mutations.setLoading("listing", false);
@@ -779,7 +812,6 @@ export default {
if (this.clipboard.key === "x") {
action = async (overwrite, rename) => {
await filesApi.moveCopy(items, "move", overwrite, rename);
this.clipboard = {};
mutations.setLoading("listing", false);
};
+33 -21
View File
@@ -1,27 +1,21 @@
<template>
<!-- Conditionally render the DocumentEditor component -->
<DocumentEditor
v-if="ready"
id="docEditor"
:documentServerUrl="onlyOfficeUrl"
:config="clientConfig"
:onLoadComponentError="onLoadComponentError"
/>
<DocumentEditor v-if="ready" id="docEditor" :documentServerUrl="onlyOfficeUrl" :config="clientConfig"
:onLoadComponentError="onLoadComponentError" />
<div v-else>
<p>{{ $t("files.loading") }}</p>
</div>
<div @click="close" class="floating-close button" :class="{ 'float-in': floatIn }"
>
<i class="material-icons">close</i>
<div @click="close" class="floating-close button" :class="{ 'float-in': floatIn }">
<i class="material-icons">close</i>
</div>
</template>
<script lang="ts">
import { DocumentEditor } from "@onlyoffice/document-editor-vue";
import { onlyOfficeUrl } from "@/utils/constants";
import { state } from "@/store";
import { state, getters } from "@/store";
import { fetchJSON } from "@/api/utils";
import { filesApi } from "@/api";
import { filesApi, publicApi } from "@/api";
import { baseURL } from "@/utils/constants";
import { removeLastDir } from "@/utils/url";
@@ -48,13 +42,29 @@ export default {
async mounted() {
// Perform the setup and update the config
try {
const refUrl = await filesApi.getDownloadURL(
state.req.source,
state.req.path,
false,
true
);
let configData = await fetchJSON(baseURL + `api/onlyoffice/config?url=${refUrl}`);
if (getters.isShare()) {
console.log("state.share", state.share);
}
const refUrl = getters.isShare()
? publicApi.getDownloadURL({
path: state.share.subPath,
hash: state.share.hash,
token: state.share.token,
})
: await filesApi.getDownloadURL(
state.req.source,
state.req.path,
false,
true
);
console.log("refUrl", refUrl);
let configData;
let configUrl = `api/onlyoffice/config?url=${encodeURIComponent(refUrl)}`;
if (getters.isShare()) {
configUrl = configUrl + `&hash=${state.share.hash}`;
}
configData = await fetchJSON(baseURL + configUrl);
configData.type = state.isMobile ? "mobile" : "desktop";
this.clientConfig = configData;
console.log("Client config:", this.clientConfig);
@@ -82,7 +92,8 @@ export default {
.floating-close {
position: fixed;
left: 50%;
transform: translate(-50%, -5em); /* Start offscreen */
transform: translate(-50%, -5em);
/* Start offscreen */
transition: transform 0.4s ease;
background: var(--surfaceSecondary);
font-size: .5em;
@@ -90,7 +101,8 @@ export default {
}
.float-in {
transform: translate(-50%, 2.75em); /* Animate to final position */
transform: translate(-50%, 2.75em);
/* Animate to final position */
}
.floating-close i {
+47 -3
View File
@@ -54,7 +54,7 @@
</div>
</template>
<script>
import { filesApi } from "@/api";
import { filesApi, publicApi } from "@/api";
import { url } from "@/utils";
import throttle from "@/utils/throttle";
import ExtendedImage from "@/components/files/ExtendedImage.vue";
@@ -125,11 +125,22 @@ export default {
},
raw() {
if (this.pdfConvertable) {
if (getters.isShare()) {
return publicApi.getPreviewURL(state.share.hash, state.share.subPath) + "&size=original";
}
return (
filesApi.getPreviewURL(state.req.source, state.req.path, state.req.modified) +
"&size=original"
);
}
if (getters.isShare()) {
return publicApi.getDownloadURL({
path: state.share.subPath,
hash: state.share.hash,
token: state.share.token,
inline: true,
});
}
return filesApi.getDownloadURL(state.req.source, state.req.path, true);
},
isDarkMode() {
@@ -142,6 +153,13 @@ export default {
return this.nextLink !== "";
},
downloadUrl() {
if (getters.isShare()) {
return publicApi.getDownloadURL({
path: state.share.subPath,
hash: state.share.hash,
token: state.share.token,
});
}
return filesApi.getDownloadURL(state.req.source, state.req.path);
},
showMore() {
@@ -221,7 +239,16 @@ export default {
for (const subtitleFile of state.req.subtitles) {
const ext = getFileExtension(subtitleFile);
const path = url.removeLastDir(state.req.path) + "/" + subtitleFile;
const resp = await filesApi.fetchFiles(state.req.source, path, true); // Fetch .srt file
let resp;
if (getters.isShare()) {
// Use public API for shared files
resp = await publicApi.fetchPub(path, state.share.hash, "", true);
} else {
// Use regular files API for authenticated users
resp = await filesApi.fetchFiles(state.req.source, path, true);
}
let vttContent = resp.content;
// Convert SRT to VTT (assuming srt2vtt() does this)
vttContent = convertToVTT(ext, resp.content);
@@ -290,7 +317,14 @@ export default {
}
const directoryPath = url.removeLastDir(state.req.path);
if (!this.listing) {
const res = await filesApi.fetchFiles(state.req.source, directoryPath);
let res;
if (getters.isShare()) {
// Use public API for shared files
res = await publicApi.fetchPub(directoryPath, state.share.hash);
} else {
// Use regular files API for authenticated users
res = await filesApi.fetchFiles(state.req.source, directoryPath);
}
this.listing = res.items;
}
this.name = state.req.name;
@@ -323,6 +357,16 @@ export default {
}
},
prefetchUrl(item) {
if (getters.isShare()) {
return this.fullSize
? publicApi.getDownloadURL({
path: item.path,
hash: state.share.hash,
token: state.share.token,
inline: true,
})
: publicApi.getPreviewURL(state.share.hash, item.path);
}
return this.fullSize
? filesApi.getDownloadURL(state.req.source, item.path, true)
: filesApi.getPreviewURL(state.req.source, item.path, item.modified);
@@ -11,7 +11,20 @@ test("share download multiple files", async ({ page, checkForErrors, context })
throw new Error("Share hash not found in localStorage");
}
// Test explicit redirect behavior
const responsePromise = page.waitForResponse(response =>
response.url().includes("/share/" + shareHash + "/testdata/") &&
response.status() === 301
);
await page.goto("/share/" + shareHash + "/testdata/");
// Wait for and verify the redirect response
const response = await responsePromise;
expect(response.status()).toBe(301); // Moved Permanently
// Verify final URL and title after redirect
await expect(page).toHaveURL(new RegExp(`/public/share/${shareHash}/testdata/`));
await expect(page).toHaveTitle("Graham's Filebrowser - Share - testdata");
await page.locator('a[aria-label="gray-sample.jpg"]').click({ button: "right" });
await page.locator('button[aria-label="Select multiple"]').waitFor({ state: 'visible' });
@@ -33,5 +46,5 @@ test("share download multiple files", async ({ page, checkForErrors, context })
const popup2 = page.locator('#popup-notification-content');
await popup2.waitFor({ state: 'visible' });
await expect(popup2).toHaveText("Downloading...");
checkForErrors();
checkForErrors(0,1); // redirect errors are expected
});
@@ -18,7 +18,7 @@ test("breadcrumbs navigation checks", async ({ page, checkForErrors, context })
let spanChildrenCount = await page.locator('#breadcrumbs > ul > li.item').count();
expect(spanChildrenCount).toBe(1);
checkForErrors();
checkForErrors(0,1); // redirect errors are expected
});
test("root share path is valid", async ({ page, checkForErrors, openContextMenu, context }) => {
@@ -41,7 +41,7 @@ test("share file works", async ({ page, checkForErrors, context }) => {
await page.goto("/share/" + shareHashFile);
await expect(page).toHaveTitle("Graham's Filebrowser - Share - 1file1.txt");
checkForErrors();
checkForErrors(0,1); // redirect errors are expected
});
test("share download single file", async ({ page, checkForErrors, context }) => {
@@ -61,5 +61,5 @@ test("share download single file", async ({ page, checkForErrors, context }) =>
const popup = page.locator('#popup-notification-content');
await popup.waitFor({ state: 'visible' });
await expect(popup).toHaveText("Downloading...");
checkForErrors();
checkForErrors(0,1);
});
@@ -8,5 +8,5 @@ test("navigate folders", async ({ page, checkForErrors, context }) => {
await page.goto("/files/excluded");
const msg = "Error 500: could not refresh file info: directory or item excluded from indexing"
await expect(page.locator('#popup-notification-content')).toHaveText(msg);
checkForErrors(1,1);
checkForErrors(2,1); // expect error not indexed
});
@@ -7,11 +7,8 @@ test("share download multiple files", async ({ page, checkForErrors, context })
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
const shareHash = await page.evaluate(() => localStorage.getItem('shareHash'));
if (!shareHash) {
throw new Error("Share hash not found in localStorage");
}
await page.goto("/files/share/" + shareHash + "/testdata/");
await page.goto("/files/share/" + shareHash+"/testdata/");
await expect(page).toHaveTitle("Graham's Filebrowser - Share - testdata");
await page.locator('a[aria-label="gray-sample.jpg"]').click({ button: "right" });
await page.locator('button[aria-label="Select multiple"]').waitFor({ state: 'visible' });
@@ -33,5 +30,5 @@ test("share download multiple files", async ({ page, checkForErrors, context })
const popup2 = page.locator('#popup-notification-content');
await popup2.waitFor({ state: 'visible' });
await expect(popup2).toHaveText("Downloading...");
checkForErrors();
checkForErrors(0,1);
});
@@ -20,7 +20,7 @@ test("breadcrumbs navigation checks", async ({ page, checkForErrors, context })
let spanChildrenCount = await page.locator('#breadcrumbs > ul > li.item').count();
expect(spanChildrenCount).toBe(1);
checkForErrors();
checkForErrors(0,1); // redirect errors are expected
});
test("root share path is valid", async ({ page, checkForErrors, openContextMenu, context }) => {
@@ -42,7 +42,7 @@ test("share file works", async ({ page, checkForErrors, context }) => {
await page.goto("/files/share/" + shareHashFile);
await expect(page).toHaveTitle("Graham's Filebrowser - Share - 1file1.txt");
checkForErrors();
checkForErrors(0,1); // redirect errors are expected
});
test("share download single file", async ({ page, checkForErrors, context }) => {
@@ -62,5 +62,5 @@ test("share download single file", async ({ page, checkForErrors, context }) =>
const popup = page.locator('#popup-notification-content');
await popup.waitFor({ state: 'visible' });
await expect(popup).toHaveText("Downloading...");
checkForErrors();
checkForErrors(0,1); // redirect errors are expected
});