feat(wopi): implement required rest api as a WOPI host

This commit is contained in:
HFO4
2023-01-09 19:37:46 +08:00
parent 4541400755
commit 1c922ac981
12 changed files with 462 additions and 19 deletions

View File

@@ -18,6 +18,7 @@ import (
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/wopi"
"github.com/gin-gonic/gin"
)
@@ -192,7 +193,7 @@ func (service *FileAnonymousGetService) Source(ctx context.Context, c *gin.Conte
}
// CreateDocPreviewSession 创建DOC文件预览会话返回预览地址
func (service *FileIDService) CreateDocPreviewSession(ctx context.Context, c *gin.Context) serializer.Response {
func (service *FileIDService) CreateDocPreviewSession(ctx context.Context, c *gin.Context, editable bool) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
@@ -226,18 +227,47 @@ func (service *FileIDService) CreateDocPreviewSession(ctx context.Context, c *gi
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
var resp serializer.DocPreviewSession
// Use WOPI preview if available
if model.IsTrueVal(model.GetSettingByName("wopi_enabled")) && wopi.Default != nil {
maxSize := model.GetIntSetting("maxEditSize", 0)
if maxSize > 0 && fs.FileTarget[0].Size > uint64(maxSize) {
return serializer.Err(serializer.CodeFileTooLarge, "", nil)
}
action := wopi.ActionPreview
if editable {
action = wopi.ActionEdit
}
session, err := wopi.Default.NewSession(fs.User, &fs.FileTarget[0], action)
if err != nil {
return serializer.Err(serializer.CodeInternalSetting, "Failed to create WOPI session", err)
}
resp.URL = session.ActionURL.String()
resp.AccessTokenTTL = session.AccessTokenTTL
resp.AccessToken = session.AccessToken
return serializer.Response{
Code: 0,
Data: resp,
}
}
// 生成最终的预览器地址
srcB64 := base64.StdEncoding.EncodeToString([]byte(downloadURL))
srcEncoded := url.QueryEscape(downloadURL)
srcB64Encoded := url.QueryEscape(srcB64)
resp.URL = util.Replace(map[string]string{
"{$src}": srcEncoded,
"{$srcB64}": srcB64Encoded,
"{$name}": url.QueryEscape(fs.FileTarget[0].Name),
}, model.GetSettingByName("office_preview_service"))
return serializer.Response{
Code: 0,
Data: util.Replace(map[string]string{
"{$src}": srcEncoded,
"{$srcB64}": srcB64Encoded,
"{$name}": url.QueryEscape(fs.FileTarget[0].Name),
}, model.GetSettingByName("office_preview_service")),
Data: resp,
}
}

136
service/explorer/wopi.go Normal file
View File

@@ -0,0 +1,136 @@
package explorer
import (
"errors"
"fmt"
"github.com/cloudreve/Cloudreve/v3/middleware"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/wopi"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
type WopiService struct {
}
func (service *WopiService) Rename(c *gin.Context) error {
fs, _, err := service.prepareFs(c)
if err != nil {
return err
}
defer fs.Recycle()
return fs.Rename(c, []uint{}, []uint{c.MustGet("object_id").(uint)}, c.GetHeader(wopi.RenameRequestHeader))
}
func (service *WopiService) GetFile(c *gin.Context) error {
fs, _, err := service.prepareFs(c)
if err != nil {
return err
}
defer fs.Recycle()
resp, err := fs.Preview(c, fs.FileTarget[0].ID, true)
if err != nil {
return fmt.Errorf("failed to pull file content: %w", err)
}
// 重定向到文件源
if resp.Redirect {
return fmt.Errorf("redirect not supported in WOPI")
}
// 直接返回文件内容
defer resp.Content.Close()
c.Header("Cache-Control", "no-cache")
http.ServeContent(c.Writer, c.Request, fs.FileTarget[0].Name, fs.FileTarget[0].UpdatedAt, resp.Content)
return nil
}
func (service *WopiService) FileInfo(c *gin.Context) (*serializer.WopiFileInfo, error) {
fs, session, err := service.prepareFs(c)
if err != nil {
return nil, err
}
defer fs.Recycle()
parent, err := model.GetFoldersByIDs([]uint{fs.FileTarget[0].FolderID}, fs.User.ID)
if err != nil {
return nil, err
}
if len(parent) == 0 {
return nil, fmt.Errorf("failed to find parent folder")
}
parent[0].TraceRoot()
siteUrl := model.GetSiteURL()
// Generate url for parent folder
parentUrl := model.GetSiteURL()
parentUrl.Path = "/home"
query := parentUrl.Query()
query.Set("path", parent[0].Position)
parentUrl.RawQuery = query.Encode()
info := &serializer.WopiFileInfo{
BaseFileName: fs.FileTarget[0].Name,
Version: fs.FileTarget[0].Model.UpdatedAt.String(),
BreadcrumbBrandName: model.GetSettingByName("siteName"),
BreadcrumbBrandUrl: siteUrl.String(),
FileSharingPostMessage: false,
PostMessageOrigin: "*",
FileNameMaxLength: 256,
LastModifiedTime: fs.FileTarget[0].Model.UpdatedAt.Format(time.RFC3339),
IsAnonymousUser: true,
ReadOnly: true,
ClosePostMessage: true,
}
if session.Action == wopi.ActionEdit {
info.FileSharingPostMessage = true
info.IsAnonymousUser = false
info.SupportsRename = true
info.SupportsReviewing = true
info.SupportsUpdate = true
info.UserFriendlyName = fs.User.Nick
info.UserId = hashid.HashID(fs.User.ID, hashid.UserID)
info.UserCanRename = true
info.UserCanReview = true
info.UserCanWrite = true
info.ReadOnly = false
info.BreadcrumbFolderName = parent[0].Name
info.BreadcrumbFolderUrl = parentUrl.String()
}
return info, nil
}
func (service *WopiService) prepareFs(c *gin.Context) (*filesystem.FileSystem, *wopi.SessionCache, error) {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return nil, nil, err
}
session := c.MustGet(middleware.WopiSessionCtx).(*wopi.SessionCache)
if err := fs.SetTargetFileByIDs([]uint{session.FileID}); err != nil {
fs.Recycle()
return nil, nil, fmt.Errorf("failed to find file: %w", err)
}
maxSize := model.GetIntSetting("maxEditSize", 0)
if maxSize > 0 && fs.FileTarget[0].Size > uint64(maxSize) {
return nil, nil, errors.New("file too large")
}
return fs, session, nil
}