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.
253 lines
6.2 KiB
253 lines
6.2 KiB
package middleware
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/cloudreve/Cloudreve/v4/application/constants"
|
|
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
|
"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/hashid"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
|
"github.com/cloudreve/Cloudreve/v4/service/explorer"
|
|
"github.com/cloudreve/Cloudreve/v4/service/share"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const (
|
|
ogStatusInvalidLink = "Invalid Link"
|
|
)
|
|
|
|
type ogData struct {
|
|
SiteName string
|
|
Title string
|
|
Description string
|
|
ImageURL string
|
|
ShareURL string
|
|
RedirectURL string
|
|
}
|
|
|
|
const ogHTMLTemplate = `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta property="og:title" content="{{.Title}}">
|
|
<meta property="og:description" content="{{.Description}}">
|
|
<meta property="og:image" content="{{.ImageURL}}">
|
|
<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="{{.Title}}">
|
|
<meta name="twitter:description" content="{{.Description}}">
|
|
<meta name="twitter:image" content="{{.ImageURL}}">
|
|
<title>{{.Title}} - {{.SiteName}}</title>
|
|
</head>
|
|
<body>
|
|
<script>window.location.href = "{{.RedirectURL}}";</script>
|
|
<noscript><a href="{{.RedirectURL}}">{{.Title}}</a></noscript>
|
|
</body>
|
|
</html>`
|
|
|
|
var ogTemplate = template.Must(template.New("og").Parse(ogHTMLTemplate))
|
|
|
|
var socialMediaBots = []string{
|
|
"facebookexternalhit",
|
|
"facebookcatalog",
|
|
"facebot",
|
|
"twitterbot",
|
|
"linkedinbot",
|
|
"discordbot",
|
|
"telegrambot",
|
|
"slackbot",
|
|
"whatsapp",
|
|
}
|
|
|
|
func isSocialMediaBot(ua string) bool {
|
|
ua = strings.ToLower(ua)
|
|
for _, bot := range socialMediaBots {
|
|
if strings.Contains(ua, bot) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// SharePreview 为社交媒体爬虫渲染OG预览页面
|
|
func SharePreview(dep dependency.Dep) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if !isSocialMediaBot(c.GetHeader("User-Agent")) {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
id, password := extractShareParams(c)
|
|
if id == "" {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
html := renderShareOGPage(c, dep, id, password)
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
c.Header("Cache-Control", "public, no-cache")
|
|
c.String(200, html)
|
|
c.Abort()
|
|
}
|
|
}
|
|
|
|
func extractShareParams(c *gin.Context) (id, password string) {
|
|
urlPath := c.Request.URL.Path
|
|
|
|
if strings.HasPrefix(urlPath, "/s/") {
|
|
parts := strings.Split(strings.TrimPrefix(urlPath, "/s/"), "/")
|
|
if len(parts) >= 1 && parts[0] != "" {
|
|
id = parts[0]
|
|
if len(parts) >= 2 {
|
|
password = parts[1]
|
|
}
|
|
}
|
|
} else if urlPath == "/home" || urlPath == "/home/" {
|
|
rawPath := c.Query("path")
|
|
uri, err := fs.NewUriFromString(rawPath)
|
|
if err != nil || uri.FileSystem() != constants.FileSystemShare {
|
|
return "", ""
|
|
}
|
|
|
|
return uri.ID(""), uri.Password()
|
|
}
|
|
|
|
return id, password
|
|
}
|
|
|
|
func renderShareOGPage(c *gin.Context, dep dependency.Dep, id, password string) string {
|
|
settings := dep.SettingProvider()
|
|
siteBasic := settings.SiteBasic(c)
|
|
pwa := settings.PWA(c)
|
|
base := settings.SiteURL(c)
|
|
|
|
data := &ogData{
|
|
SiteName: siteBasic.Name,
|
|
Title: siteBasic.Name,
|
|
Description: siteBasic.Description,
|
|
ShareURL: routes.MasterShareUrl(base, id, password).String(),
|
|
RedirectURL: routes.MasterShareLongUrl(id, password).String(),
|
|
}
|
|
|
|
if pwa.LargeIcon != "" {
|
|
data.ImageURL = resolveURL(base, pwa.LargeIcon)
|
|
} else if pwa.MediumIcon != "" {
|
|
data.ImageURL = resolveURL(base, pwa.MediumIcon)
|
|
}
|
|
|
|
shareID, err := dep.HashIDEncoder().Decode(id, hashid.ShareID)
|
|
if err != nil {
|
|
data.Description = ogStatusInvalidLink
|
|
return renderOGHTML(data)
|
|
}
|
|
|
|
shareInfo, err := loadShareForOG(c, shareID, password)
|
|
if err != nil {
|
|
var appErr serializer.AppError
|
|
if errors.As(err, &appErr) {
|
|
data.Description = appErr.Msg
|
|
} else {
|
|
data.Description = ogStatusInvalidLink
|
|
}
|
|
return renderOGHTML(data)
|
|
}
|
|
|
|
data.Title = shareInfo.Name
|
|
if shareInfo.SourceType != nil && *shareInfo.SourceType == types.FileTypeFolder {
|
|
data.Description = "Folder"
|
|
} else if shareInfo.Unlocked {
|
|
data.Description = formatFileSize(shareInfo.Size)
|
|
thumbnail, err := loadShareThumbnail(c, id, password, shareInfo)
|
|
if err == nil {
|
|
data.ImageURL = thumbnail
|
|
}
|
|
}
|
|
|
|
data.Description += " · " + shareInfo.Owner.Nickname
|
|
return renderOGHTML(data)
|
|
}
|
|
|
|
func loadShareThumbnail(c *gin.Context, shareID, password string, shareInfo *explorer.Share) (string, error) {
|
|
shareUri, err := fs.NewUriFromString(fs.NewShareUri(shareID, password))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to construct share uri: %w", err)
|
|
}
|
|
|
|
subService := &explorer.FileThumbService{
|
|
Uri: shareUri.Join(shareInfo.Name).String(),
|
|
}
|
|
|
|
if err := SetUserCtx(c, 0); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
res, err := subService.Get(c)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return res.Url, nil
|
|
}
|
|
|
|
func loadShareForOG(c *gin.Context, shareID int, password string) (*explorer.Share, error) {
|
|
subService := &share.ShareInfoService{
|
|
Password: password,
|
|
CountViews: false,
|
|
}
|
|
|
|
if err := SetUserCtx(c, 0); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
util.WithValue(c, hashid.ObjectIDCtx{}, shareID)
|
|
return subService.Get(c)
|
|
}
|
|
|
|
func renderOGHTML(data *ogData) string {
|
|
var buf bytes.Buffer
|
|
if err := ogTemplate.Execute(&buf, data); err != nil {
|
|
return ""
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func resolveURL(base *url.URL, path string) string {
|
|
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
|
return path
|
|
}
|
|
return base.ResolveReference(&url.URL{Path: path}).String()
|
|
}
|
|
|
|
func formatFileSize(size int64) string {
|
|
const (
|
|
KB = 1024
|
|
MB = 1024 * KB
|
|
GB = 1024 * MB
|
|
TB = 1024 * GB
|
|
)
|
|
|
|
switch {
|
|
case size >= TB:
|
|
return fmt.Sprintf("%.2f TB", float64(size)/TB)
|
|
case size >= GB:
|
|
return fmt.Sprintf("%.2f GB", float64(size)/GB)
|
|
case size >= MB:
|
|
return fmt.Sprintf("%.2f MB", float64(size)/MB)
|
|
case size >= KB:
|
|
return fmt.Sprintf("%.2f KB", float64(size)/KB)
|
|
default:
|
|
return fmt.Sprintf("%d B", size)
|
|
}
|
|
}
|