You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
190 lines
5.8 KiB
190 lines
5.8 KiB
package sharepreview
|
|
|
|
import (
|
|
"context"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
|
"github.com/cloudreve/Cloudreve/v4/ent"
|
|
"github.com/cloudreve/Cloudreve/v4/inventory"
|
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
|
sharepkg "github.com/cloudreve/Cloudreve/v4/pkg/share"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const (
|
|
ogStatusInvalidLink = "Invalid Link"
|
|
ogStatusShareExpired = "Share Expired"
|
|
ogStatusPasswordRequired = "Password Required"
|
|
ogDefaultFileName = "Shared File"
|
|
ogDefaultFolderName = "Shared Folder"
|
|
)
|
|
|
|
type Options struct {
|
|
ID string
|
|
Password string
|
|
SharePath string
|
|
}
|
|
|
|
// RenderOGPage renders an Open Graph HTML page for social media previews.
|
|
func RenderOGPage(c *gin.Context, opts Options) (string, error) {
|
|
dep := dependency.FromContext(c)
|
|
shareClient := dep.ShareClient()
|
|
settings := dep.SettingProvider()
|
|
u := inventory.UserFromContext(c)
|
|
|
|
// Check if anonymous users have permission to access share links.
|
|
anonymousGroup, err := dep.GroupClient().AnonymousGroup(c)
|
|
needLogin := err != nil || anonymousGroup.Permissions == nil || !anonymousGroup.Permissions.Enabled(int(types.GroupPermissionShareDownload))
|
|
|
|
// Get site settings.
|
|
siteBasic := settings.SiteBasic(c)
|
|
pwa := settings.PWA(c)
|
|
base := settings.SiteURL(c)
|
|
|
|
sharePath := sharepkg.SanitizeSharePath(opts.SharePath)
|
|
|
|
// Build share URL.
|
|
shareURL := routes.MasterShareUrl(base, opts.ID, "")
|
|
if sharePath != "" {
|
|
shareURLQuery := shareURL.Query()
|
|
shareURLQuery.Set("path", sharePath)
|
|
shareURL.RawQuery = shareURLQuery.Encode()
|
|
}
|
|
|
|
// Build thumbnail URL - use PWA large icon (PNG), fallback to medium icon.
|
|
thumbnailURL := pwa.LargeIcon
|
|
if thumbnailURL == "" {
|
|
thumbnailURL = pwa.MediumIcon
|
|
}
|
|
if thumbnailURL != "" && !isAbsoluteURL(thumbnailURL) {
|
|
thumbnailURL = base.ResolveReference(&url.URL{Path: thumbnailURL}).String()
|
|
}
|
|
|
|
// Prepare OG data.
|
|
ogData := &ShareOGData{
|
|
SiteName: siteBasic.Name,
|
|
SiteDescription: siteBasic.Description,
|
|
SiteURL: base.String(),
|
|
ShareURL: shareURL.String(),
|
|
ShareID: opts.ID,
|
|
ThumbnailURL: thumbnailURL,
|
|
RedirectURL: sharepkg.BuildRedirectURL(opts.ID, opts.Password, sharePath, c.Request.URL.Query()),
|
|
}
|
|
|
|
// Get file info (hide for invalid/expired/password-protected/login-required shares).
|
|
shareID, err := dep.HashIDEncoder().Decode(opts.ID, hashid.ShareID)
|
|
if err != nil {
|
|
return renderStatusOG(ogData, siteBasic.Name, ogStatusInvalidLink)
|
|
}
|
|
|
|
share, unlocked, status, _ := sharepkg.LoadShareForInfo(c, shareClient, shareID, u, opts.Password)
|
|
switch status {
|
|
case sharepkg.LoadNotFound:
|
|
return renderStatusOG(ogData, siteBasic.Name, ogStatusInvalidLink)
|
|
case sharepkg.LoadExpired:
|
|
return renderStatusOG(ogData, siteBasic.Name, ogStatusShareExpired)
|
|
case sharepkg.LoadError:
|
|
return renderStatusOG(ogData, siteBasic.Name, ogStatusInvalidLink)
|
|
}
|
|
|
|
if needLogin {
|
|
return renderStatusOG(ogData, siteBasic.Name, ogStatusShareExpired)
|
|
}
|
|
|
|
if share.Password != "" && !unlocked {
|
|
// Password required but not provided or incorrect.
|
|
return renderStatusOG(ogData, siteBasic.Name, ogStatusPasswordRequired)
|
|
}
|
|
|
|
if share.Edges.File == nil {
|
|
return renderStatusOG(ogData, siteBasic.Name, ogStatusInvalidLink)
|
|
}
|
|
|
|
// Get owner name (don't expose email for privacy).
|
|
if share.Edges.User != nil {
|
|
ogData.OwnerName = share.Edges.User.Nick
|
|
}
|
|
|
|
fileType := types.FileType(share.Edges.File.Type)
|
|
fileName := share.Edges.File.Name
|
|
fileSize := share.Edges.File.Size
|
|
if sharePath != "" {
|
|
if fileType != types.FileTypeFolder {
|
|
return renderStatusOG(ogData, siteBasic.Name, ogStatusInvalidLink)
|
|
}
|
|
|
|
resolvedFile, err := resolveSharePathFile(c, dep, u, opts.ID, opts.Password, sharePath)
|
|
if err != nil {
|
|
return renderStatusOG(ogData, siteBasic.Name, ogStatusInvalidLink)
|
|
}
|
|
|
|
fileType = resolvedFile.Type()
|
|
fileName = resolvedFile.Name()
|
|
if displayName := resolvedFile.DisplayName(); displayName != "" {
|
|
fileName = displayName
|
|
}
|
|
fileSize = resolvedFile.Size()
|
|
}
|
|
|
|
if fileType == types.FileTypeFolder {
|
|
ogData.FolderName = defaultIfEmpty(fileName, ogDefaultFolderName)
|
|
ogData.DisplayName = ogData.FolderName
|
|
return RenderFolderOGHTML(ogData, ogFolderTitleTemplate, ogFolderDescTemplate)
|
|
}
|
|
|
|
ogData.FileName = defaultIfEmpty(fileName, ogDefaultFileName)
|
|
ogData.FileSize = FormatFileSize(fileSize)
|
|
ogData.FileExt = strings.TrimPrefix(path.Ext(ogData.FileName), ".")
|
|
ogData.DisplayName = ogData.FileName
|
|
|
|
return RenderOGHTML(ogData, ogFileTitleTemplate, ogFileDescTemplate)
|
|
}
|
|
|
|
func renderStatusOG(data *ShareOGData, displayName, status string) (string, error) {
|
|
data.Status = status
|
|
data.DisplayName = displayName
|
|
return RenderStatusOGHTML(data, ogStatusTitleTemplate, ogStatusDescTemplate)
|
|
}
|
|
|
|
func defaultIfEmpty(value, fallback string) string {
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
return value
|
|
}
|
|
|
|
func resolveSharePathFile(ctx context.Context, dep dependency.Dep, viewer *ent.User, id, password, sharePath string) (fs.File, error) {
|
|
if viewer == nil {
|
|
anonymous, err := dep.UserClient().AnonymousUser(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
viewer = anonymous
|
|
}
|
|
|
|
fm := manager.NewFileManager(dep, viewer)
|
|
defer fm.Recycle()
|
|
|
|
shareURI, err := fs.NewUriFromString(fs.NewShareUri(id, password))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if sharePath != "" {
|
|
shareURI = shareURI.JoinRaw(sharePath)
|
|
}
|
|
|
|
return fm.Get(ctx, shareURI)
|
|
}
|
|
|
|
// isAbsoluteURL checks if the URL is absolute (starts with http:// or https://).
|
|
func isAbsoluteURL(u string) bool {
|
|
return strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://")
|
|
}
|