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 2 weeks ago
parent 5e02fbcd53
commit 017e52dd63
No known key found for this signature in database

@ -4,54 +4,165 @@ import (
"bytes"
"fmt"
"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 {
SiteName string
FileName string
FileSize string
OwnerName string
ShareURL string
ThumbnailURL string
RedirectURL string
SiteName string
SiteDescription string
SiteURL string
ShareURL string
ShareID string
FileName 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>
<html>
<head>
<meta charset="utf-8">
<meta property="og:title" content="{{.FileName}}">
<meta property="og:description" content="{{.FileSize}}{{if .OwnerName}} · {{.OwnerName}}{{end}}">
<meta property="og:title" content="{{.Title}}">
<meta property="og:description" content="{{.Description}}">
<meta property="og:image" content="{{.ThumbnailURL}}">
<meta property="og:url" content="{{.ShareURL}}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="{{.SiteName}}">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{{.FileName}}">
<meta name="twitter:description" content="{{.FileSize}}{{if .OwnerName}} · {{.OwnerName}}{{end}}">
<meta name="twitter:title" content="{{.Title}}">
<meta name="twitter:description" content="{{.Description}}">
<meta name="twitter:image" content="{{.ThumbnailURL}}">
<title>{{.FileName}} - {{.SiteName}}</title>
<title>{{.Title}} - {{.SiteName}}</title>
</head>
<body>
<script>window.location.href = "{{.RedirectURL}}";</script>
<noscript>
<p><a href="{{.RedirectURL}}">{{.FileName}}</a></p>
<p><a href="{{.RedirectURL}}">{{.DisplayName}}</a></p>
</noscript>
</body>
</html>`
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
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 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
func FormatFileSize(size int64) string {
const (

@ -3,6 +3,7 @@ package share
import (
"context"
"net/url"
"path"
"strings"
"github.com/cloudreve/Cloudreve/v4/application/dependency"
@ -26,6 +27,15 @@ type (
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 {
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()
}
// 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)
var fileName, fileSize, ownerName string
if shareNotFound {
fileName = siteBasic.Name
fileSize = "Invalid Link"
} else if needLogin {
fileName = siteBasic.Name
fileSize = "Need Login"
} else if shareExpired {
fileName = siteBasic.Name
fileSize = "Share Expired"
} else if share.Password != "" && !isShareUnlocked(share, s.Password, u) {
// Password required but not provided or incorrect
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"
switch {
case shareNotFound:
return renderStatusOG(ogData, siteBasic.Name, ogStatusInvalidLink)
case needLogin:
return renderStatusOG(ogData, siteBasic.Name, ogStatusNeedLogin)
case shareExpired:
return renderStatusOG(ogData, siteBasic.Name, ogStatusShareExpired)
case share.Password != "" && !isShareUnlocked(share, s.Password, u):
// Password required but not provided or incorrect.
return renderStatusOG(ogData, siteBasic.Name, ogStatusPasswordRequired)
case share.Edges.File == nil:
return renderStatusOG(ogData, siteBasic.Name, ogStatusInvalidLink)
}
// Prepare OG data
ogData := &ShareOGData{
SiteName: siteBasic.Name,
FileName: fileName,
FileSize: fileSize,
OwnerName: ownerName,
ShareURL: shareURL.String(),
ThumbnailURL: thumbnailURL,
RedirectURL: s.RedirectTo(c),
// Get owner name (don't expose email for privacy).
if share.Edges.User != nil {
ogData.OwnerName = share.Edges.User.Nick
}
shareFile := share.Edges.File
if types.FileType(shareFile.Type) == types.FileTypeFolder {
ogData.FolderName = defaultIfEmpty(shareFile.Name, ogDefaultFolderName)
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://)
@ -153,6 +160,19 @@ func isShareUnlocked(share *ent.Share, password string, viewer *ent.User) bool {
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 (
ShareInfoService struct {
Password string `form:"password"`

Loading…
Cancel
Save