refactor(share): restructure OG rendering with template system

- Extend ShareOGData with additional fields for richer previews
- Add magic variable replacement for File/Folder/Status scenarios
- Refactor status handling with switch/case pattern
- Separate rendering paths for files, folders, and status messages
pull/3227/head
WittF 8 months ago
parent 5e02fbcd53
commit 017e52dd63
No known key found for this signature in database

@ -4,54 +4,165 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"html/template" "html/template"
"regexp"
"strings"
) )
// ShareOGData contains data for rendering Open Graph HTML page // ShareOGData contains data for rendering Open Graph HTML page.
type ShareOGData struct { type ShareOGData struct {
SiteName string SiteName string
FileName string SiteDescription string
FileSize string SiteURL string
OwnerName string ShareURL string
ShareURL string ShareID string
ThumbnailURL string FileName string
RedirectURL string FileSize string
FileExt string
FolderName string
OwnerName string
Status string
Title string
Description string
DisplayName string
ThumbnailURL string
RedirectURL string
} }
const ogHTMLTemplate = `<!DOCTYPE html> const ogHTMLTemplate = `<!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta property="og:title" content="{{.FileName}}"> <meta property="og:title" content="{{.Title}}">
<meta property="og:description" content="{{.FileSize}}{{if .OwnerName}} · {{.OwnerName}}{{end}}"> <meta property="og:description" content="{{.Description}}">
<meta property="og:image" content="{{.ThumbnailURL}}"> <meta property="og:image" content="{{.ThumbnailURL}}">
<meta property="og:url" content="{{.ShareURL}}"> <meta property="og:url" content="{{.ShareURL}}">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:site_name" content="{{.SiteName}}"> <meta property="og:site_name" content="{{.SiteName}}">
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{{.FileName}}"> <meta name="twitter:title" content="{{.Title}}">
<meta name="twitter:description" content="{{.FileSize}}{{if .OwnerName}} · {{.OwnerName}}{{end}}"> <meta name="twitter:description" content="{{.Description}}">
<meta name="twitter:image" content="{{.ThumbnailURL}}"> <meta name="twitter:image" content="{{.ThumbnailURL}}">
<title>{{.FileName}} - {{.SiteName}}</title> <title>{{.Title}} - {{.SiteName}}</title>
</head> </head>
<body> <body>
<script>window.location.href = "{{.RedirectURL}}";</script> <script>window.location.href = "{{.RedirectURL}}";</script>
<noscript> <noscript>
<p><a href="{{.RedirectURL}}">{{.FileName}}</a></p> <p><a href="{{.RedirectURL}}">{{.DisplayName}}</a></p>
</noscript> </noscript>
</body> </body>
</html>` </html>`
var ogTemplate = template.Must(template.New("og").Parse(ogHTMLTemplate)) var ogTemplate = template.Must(template.New("og").Parse(ogHTMLTemplate))
var ogMagicVarRe = regexp.MustCompile(`\{[^{}]+\}`)
var ogOwnerSeparatorRe = regexp.MustCompile(`\s*·\s*·+\s*`)
const (
ogFileTitleTemplate = "{file_name}"
ogFileDescTemplate = "{file_size} · {owner_name}"
ogFolderTitleTemplate = "{folder_name}"
ogFolderDescTemplate = "Folder · {owner_name}"
ogStatusTitleTemplate = "{site_name}"
ogStatusDescTemplate = "{status}"
)
// RenderOGHTML renders the Open Graph HTML page for file shares.
func RenderOGHTML(data *ShareOGData, titleTemplate, descTemplate string) (string, error) {
title := replaceOGFileMagicVars(titleTemplate, data)
description := replaceOGFileMagicVars(descTemplate, data)
return renderOGHTML(data, title, description)
}
// RenderFolderOGHTML renders the Open Graph HTML page for folder shares.
func RenderFolderOGHTML(data *ShareOGData, titleTemplate, descTemplate string) (string, error) {
title := replaceOGFolderMagicVars(titleTemplate, data)
description := replaceOGFolderMagicVars(descTemplate, data)
return renderOGHTML(data, title, description)
}
// RenderStatusOGHTML renders the Open Graph HTML page for share status scenarios.
func RenderStatusOGHTML(data *ShareOGData, titleTemplate, descTemplate string) (string, error) {
title := replaceOGStatusMagicVars(titleTemplate, data)
description := replaceOGStatusMagicVars(descTemplate, data)
return renderOGHTML(data, title, description)
}
func renderOGHTML(data *ShareOGData, title, description string) (string, error) {
renderData := *data
renderData.Title = title
renderData.Description = description
// RenderOGHTML renders the Open Graph HTML page
func RenderOGHTML(data *ShareOGData) (string, error) {
var buf bytes.Buffer var buf bytes.Buffer
if err := ogTemplate.Execute(&buf, data); err != nil { if err := ogTemplate.Execute(&buf, &renderData); err != nil {
return "", fmt.Errorf("failed to render OG template: %w", err) return "", fmt.Errorf("failed to render OG template: %w", err)
} }
return buf.String(), nil return buf.String(), nil
} }
func replaceOGFileMagicVars(tmpl string, data *ShareOGData) string {
vars := map[string]string{
"{site_name}": data.SiteName,
"{site_description}": data.SiteDescription,
"{site_url}": data.SiteURL,
"{file_name}": data.FileName,
"{file_size}": data.FileSize,
"{file_ext}": data.FileExt,
"{owner_name}": data.OwnerName,
"{share_url}": data.ShareURL,
"{share_id}": data.ShareID,
}
return replaceOGMagicVars(tmpl, vars, data.OwnerName, true)
}
func replaceOGFolderMagicVars(tmpl string, data *ShareOGData) string {
vars := map[string]string{
"{site_name}": data.SiteName,
"{site_description}": data.SiteDescription,
"{site_url}": data.SiteURL,
"{folder_name}": data.FolderName,
"{owner_name}": data.OwnerName,
"{share_url}": data.ShareURL,
"{share_id}": data.ShareID,
}
return replaceOGMagicVars(tmpl, vars, data.OwnerName, true)
}
func replaceOGStatusMagicVars(tmpl string, data *ShareOGData) string {
vars := map[string]string{
"{site_name}": data.SiteName,
"{site_description}": data.SiteDescription,
"{site_url}": data.SiteURL,
"{share_url}": data.ShareURL,
"{share_id}": data.ShareID,
"{status}": data.Status,
}
return replaceOGMagicVars(tmpl, vars, "", false)
}
func replaceOGMagicVars(tmpl string, vars map[string]string, ownerName string, trimOwner bool) string {
rendered := ogMagicVarRe.ReplaceAllStringFunc(tmpl, func(match string) string {
if value, ok := vars[match]; ok {
return value
}
return match
})
if trimOwner {
return trimEmptyOwnerSeparator(tmpl, rendered, ownerName)
}
return rendered
}
func trimEmptyOwnerSeparator(tmpl, rendered, ownerName string) string {
if ownerName != "" || !strings.Contains(tmpl, "{owner_name}") {
return rendered
}
trimmed := strings.TrimSpace(rendered)
trimmed = ogOwnerSeparatorRe.ReplaceAllString(trimmed, " · ")
trimmed = strings.TrimSpace(trimmed)
trimmed = strings.Trim(trimmed, "\u00b7")
return strings.TrimSpace(trimmed)
}
// FormatFileSize formats file size to human readable format // FormatFileSize formats file size to human readable format
func FormatFileSize(size int64) string { func FormatFileSize(size int64) string {
const ( const (

@ -3,6 +3,7 @@ package share
import ( import (
"context" "context"
"net/url" "net/url"
"path"
"strings" "strings"
"github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/application/dependency"
@ -26,6 +27,15 @@ type (
ShortLinkRedirectParamCtx struct{} ShortLinkRedirectParamCtx struct{}
) )
const (
ogStatusInvalidLink = "Invalid Link"
ogStatusNeedLogin = "Need Login"
ogStatusShareExpired = "Share Expired"
ogStatusPasswordRequired = "Password Required"
ogDefaultFileName = "Shared File"
ogDefaultFolderName = "Shared Folder"
)
func (s *ShortLinkRedirectService) RedirectTo(c *gin.Context) string { func (s *ShortLinkRedirectService) RedirectTo(c *gin.Context) string {
shareLongUrl := routes.MasterShareLongUrl(s.ID, s.Password) shareLongUrl := routes.MasterShareLongUrl(s.ID, s.Password)
@ -86,53 +96,50 @@ func (s *ShortLinkRedirectService) RenderOGPage(c *gin.Context) (string, error)
thumbnailURL = base.ResolveReference(&url.URL{Path: thumbnailURL}).String() 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: s.ID,
ThumbnailURL: thumbnailURL,
RedirectURL: s.RedirectTo(c),
}
// Get file info (hide for invalid/expired/password-protected/login-required shares) // Get file info (hide for invalid/expired/password-protected/login-required shares)
var fileName, fileSize, ownerName string switch {
if shareNotFound { case shareNotFound:
fileName = siteBasic.Name return renderStatusOG(ogData, siteBasic.Name, ogStatusInvalidLink)
fileSize = "Invalid Link" case needLogin:
} else if needLogin { return renderStatusOG(ogData, siteBasic.Name, ogStatusNeedLogin)
fileName = siteBasic.Name case shareExpired:
fileSize = "Need Login" return renderStatusOG(ogData, siteBasic.Name, ogStatusShareExpired)
} else if shareExpired { case share.Password != "" && !isShareUnlocked(share, s.Password, u):
fileName = siteBasic.Name // Password required but not provided or incorrect.
fileSize = "Share Expired" return renderStatusOG(ogData, siteBasic.Name, ogStatusPasswordRequired)
} else if share.Password != "" && !isShareUnlocked(share, s.Password, u) { case share.Edges.File == nil:
// Password required but not provided or incorrect return renderStatusOG(ogData, siteBasic.Name, ogStatusInvalidLink)
fileName = siteBasic.Name
fileSize = "Password Required"
} else if share.Edges.File != nil {
fileName = share.Edges.File.Name
if fileName == "" {
fileName = "Shared File"
}
// Show "Folder" for directories, file size for files
if types.FileType(share.Edges.File.Type) == types.FileTypeFolder {
fileSize = "Folder"
} else {
fileSize = FormatFileSize(share.Edges.File.Size)
}
// Get owner name (don't expose email for privacy)
if share.Edges.User != nil {
ownerName = share.Edges.User.Nick
}
} else {
fileName = siteBasic.Name
fileSize = "Invalid Link"
} }
// Prepare OG data // Get owner name (don't expose email for privacy).
ogData := &ShareOGData{ if share.Edges.User != nil {
SiteName: siteBasic.Name, ogData.OwnerName = share.Edges.User.Nick
FileName: fileName, }
FileSize: fileSize,
OwnerName: ownerName, shareFile := share.Edges.File
ShareURL: shareURL.String(), if types.FileType(shareFile.Type) == types.FileTypeFolder {
ThumbnailURL: thumbnailURL, ogData.FolderName = defaultIfEmpty(shareFile.Name, ogDefaultFolderName)
RedirectURL: s.RedirectTo(c), ogData.DisplayName = ogData.FolderName
return RenderFolderOGHTML(ogData, ogFolderTitleTemplate, ogFolderDescTemplate)
} }
return RenderOGHTML(ogData) ogData.FileName = defaultIfEmpty(shareFile.Name, ogDefaultFileName)
ogData.FileSize = FormatFileSize(shareFile.Size)
ogData.FileExt = strings.TrimPrefix(path.Ext(ogData.FileName), ".")
ogData.DisplayName = ogData.FileName
return RenderOGHTML(ogData, ogFileTitleTemplate, ogFileDescTemplate)
} }
// isAbsoluteURL checks if the URL is absolute (starts with http:// or https://) // isAbsoluteURL checks if the URL is absolute (starts with http:// or https://)
@ -153,6 +160,19 @@ func isShareUnlocked(share *ent.Share, password string, viewer *ent.User) bool {
return false return false
} }
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
}
type ( type (
ShareInfoService struct { ShareInfoService struct {
Password string `form:"password"` Password string `form:"password"`

Loading…
Cancel
Save