updated with share refactor (#993)
This commit is contained in:
1 parent
e08690b66d
commit
080256bcff
44 files changed
+1357
-684
No files matched your search
@@ -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**:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -108,5 +108,4 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
handleWithStaticData(w, r, "index.html", "text/html")
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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}`);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -207,7 +207,7 @@ export default {
|
||||
state.user?.permissions &&
|
||||
state.user?.permissions.share &&
|
||||
state.user.username != "publicUser" &&
|
||||
getters.currentView() != "share" &&
|
||||
!getters.isShare() &&
|
||||
!this.isSearchActive
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -25,6 +25,11 @@ export const state = reactive({
|
||||
realtimeActive: undefined,
|
||||
realtimeDownCount: 0,
|
||||
popupPreviewSource: "",
|
||||
share: {
|
||||
hash: null,
|
||||
token: "",
|
||||
subPath: "",
|
||||
},
|
||||
sources: {
|
||||
current: "",
|
||||
count: 1,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
Reference in new issue
Block a user