diff --git a/service/share/og_template.go b/service/share/og_template.go
index e55eed08..266b3762 100644
--- a/service/share/og_template.go
+++ b/service/share/og_template.go
@@ -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 = `
-
-
+
+
-
-
+
+
- {{.FileName}} - {{.SiteName}}
+ {{.Title}} - {{.SiteName}}
`
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 (
diff --git a/service/share/visit.go b/service/share/visit.go
index e941a0c3..d66a5534 100644
--- a/service/share/visit.go
+++ b/service/share/visit.go
@@ -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"`