mirror of https://github.com/cloudreve/Cloudreve
426 lines
12 KiB
Go
426 lines
12 KiB
Go
package explorer
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"github.com/cloudreve/Cloudreve/v4/application/constants"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
|
"github.com/cloudreve/Cloudreve/v4/ent"
|
|
"github.com/cloudreve/Cloudreve/v4/inventory"
|
|
"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/filemanager/fs/dbfs"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/lock"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/wopi"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type WopiService struct {
|
|
}
|
|
|
|
func prepareFs(c *gin.Context) (*fs.URI, manager.FileManager, *ent.User, *manager.ViewerSessionCache, dependency.Dep, error) {
|
|
dep := dependency.FromContext(c)
|
|
user := inventory.UserFromContext(c)
|
|
m := manager.NewFileManager(dep, user)
|
|
defer m.Recycle()
|
|
|
|
viewerSession := manager.ViewerSessionFromContext(c)
|
|
uri, err := fs.NewUriFromString(viewerSession.Uri)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
|
|
}
|
|
|
|
return uri, m, user, viewerSession, dep, nil
|
|
}
|
|
|
|
func (service *WopiService) Unlock(c *gin.Context) error {
|
|
_, m, _, _, dep, err := prepareFs(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
l := dep.Logger()
|
|
|
|
lockToken := c.GetHeader(wopi.LockTokenHeader)
|
|
if err = m.Unlock(c, lockToken); err != nil {
|
|
l.Debug("WOPI unlock, not locked or not match: %w", err)
|
|
c.Status(http.StatusConflict)
|
|
c.Header(wopi.LockTokenHeader, "")
|
|
return nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (service *WopiService) RefreshLock(c *gin.Context) error {
|
|
uri, m, _, _, dep, err := prepareFs(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
l := dep.Logger()
|
|
|
|
// Make sure file exists and readable
|
|
file, err := m.Get(c, uri, dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityLockFile), dbfs.WithNotRoot())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get file: %w", err)
|
|
}
|
|
|
|
lockToken := c.GetHeader(wopi.LockTokenHeader)
|
|
release, _, err := m.ConfirmLock(c, file, file.Uri(false), lockToken)
|
|
if err != nil {
|
|
// File not locked for token not match
|
|
|
|
l.Debug("WOPI refresh lock, not locked or not match: %w", err)
|
|
c.Status(http.StatusConflict)
|
|
c.Header(wopi.LockTokenHeader, "")
|
|
return nil
|
|
|
|
}
|
|
|
|
// refresh lock
|
|
release()
|
|
_, err = m.Refresh(c, wopi.LockDuration, lockToken)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.Header(wopi.LockTokenHeader, lockToken)
|
|
return nil
|
|
}
|
|
|
|
func (service *WopiService) Lock(c *gin.Context) error {
|
|
uri, m, user, viewerSession, dep, err := prepareFs(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
l := dep.Logger()
|
|
|
|
// Make sure file exists and readable
|
|
file, err := m.Get(c, uri, dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityLockFile), dbfs.WithNotRoot())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get file: %w", err)
|
|
}
|
|
|
|
lockToken := c.GetHeader(wopi.LockTokenHeader)
|
|
release, _, err := m.ConfirmLock(c, file, file.Uri(false), lockToken)
|
|
if err != nil {
|
|
// File not locked for token not match
|
|
|
|
// Try to lock using given token
|
|
app := lock.Application{
|
|
Type: string(fs.ApplicationViewer),
|
|
ViewerID: viewerSession.ViewerID,
|
|
}
|
|
_, err = m.Lock(c, wopi.LockDuration, user, true, app, file.Uri(false), lockToken)
|
|
if err != nil {
|
|
// Token not match
|
|
var lockConflict lock.ConflictError
|
|
if errors.As(err, &lockConflict) {
|
|
c.Status(http.StatusConflict)
|
|
c.Header(wopi.LockTokenHeader, lockConflict[0].Token)
|
|
|
|
l.Debug("WOPI lock, lock conflict: %w", err)
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("failed to lock file: %w", err)
|
|
}
|
|
|
|
// Lock success, return the token
|
|
c.Header(wopi.LockTokenHeader, lockToken)
|
|
return nil
|
|
|
|
}
|
|
|
|
// refresh lock
|
|
release()
|
|
_, err = m.Refresh(c, wopi.LockDuration, lockToken)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.Header(wopi.LockTokenHeader, lockToken)
|
|
return nil
|
|
}
|
|
|
|
func (service *WopiService) PutContent(c *gin.Context, isPutRelative bool) error {
|
|
uri, m, user, viewerSession, _, err := prepareFs(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Make sure file exists and readable
|
|
file, err := m.Get(c, uri, dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityUploadFile), dbfs.WithNotRoot())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get file: %w", err)
|
|
}
|
|
|
|
var lockSession fs.LockSession
|
|
lockToken := c.GetHeader(wopi.LockTokenHeader)
|
|
if lockToken != "" {
|
|
// File not locked for token not match
|
|
|
|
release, ls, err := m.ConfirmLock(c, file, file.Uri(false), lockToken)
|
|
if err != nil {
|
|
// File not locked for token not match
|
|
|
|
// Try to lock using given token
|
|
app := lock.Application{
|
|
Type: string(fs.ApplicationViewer),
|
|
ViewerID: viewerSession.ViewerID,
|
|
}
|
|
ls, err := m.Lock(c, wopi.LockDuration, user, true, app, file.Uri(false), lockToken)
|
|
if err != nil {
|
|
// Token not match
|
|
// If the file is currently locked and the X-WOPI-Lock value doesn't match the lock currently on the file, the host must
|
|
//
|
|
// Return a lock mismatch response (409 Conflict)
|
|
// Include an X-WOPI-Lock response header containing the value of the current lock on the file.
|
|
var lockConflict lock.ConflictError
|
|
if errors.As(err, &lockConflict) {
|
|
c.Status(http.StatusConflict)
|
|
c.Header(wopi.LockTokenHeader, lockConflict[0].Token)
|
|
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("failed to lock file: %w", err)
|
|
}
|
|
|
|
// In cases where the file is unlocked, the host must set X-WOPI-Lock to the empty string.
|
|
c.Header(wopi.LockTokenHeader, "")
|
|
_ = m.Unlock(c, ls.LastToken())
|
|
} else {
|
|
defer release()
|
|
}
|
|
|
|
lockSession = ls
|
|
}
|
|
|
|
fileUri := viewerSession.Uri
|
|
if isPutRelative {
|
|
// If the header contains only a file extension (starts with a period), then the resulting file name will consist of this extension and the initial file name without extension.
|
|
// If the header contains a full file name, then it will be a name for the resulting file.
|
|
fileName, err := wopi.UTF7Decode(c.GetHeader(wopi.SuggestedTargetHeader))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decode X-WOPI-SuggestedTarget header (UTF-7): %w", err)
|
|
}
|
|
|
|
fileUriParsed, err := fs.NewUriFromString(fileUri)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse file uri: %w", err)
|
|
}
|
|
|
|
if strings.HasPrefix(fileName, ".") {
|
|
fileName = strings.TrimSuffix(fileUriParsed.Name(), filepath.Ext(fileUriParsed.Name())) + fileName
|
|
}
|
|
fileUri = fileUriParsed.DirUri().JoinRaw(fileName).String()
|
|
}
|
|
|
|
subService := FileUpdateService{
|
|
Uri: fileUri,
|
|
}
|
|
|
|
res, err := subService.PutContent(c, lockSession)
|
|
if err != nil {
|
|
var appErr serializer.AppError
|
|
if errors.As(err, &appErr) {
|
|
switch appErr.Code {
|
|
case serializer.CodeFileTooLarge:
|
|
c.Status(http.StatusRequestEntityTooLarge)
|
|
c.Header(wopi.ServerErrorHeader, err.Error())
|
|
case serializer.CodeNotFound:
|
|
c.Status(http.StatusNotFound)
|
|
c.Header(wopi.ServerErrorHeader, err.Error())
|
|
case 0:
|
|
c.Status(http.StatusOK)
|
|
default:
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
c.Header(wopi.ItemVersionHeader, res.PrimaryEntity)
|
|
if isPutRelative {
|
|
c.JSON(http.StatusOK, PutRelativeResponse{
|
|
Name: res.Name,
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (service *WopiService) GetFile(c *gin.Context) error {
|
|
uri, m, _, viewerSession, dep, err := prepareFs(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Make sure file exists and readable
|
|
file, err := m.Get(c, uri, dbfs.WithExtendedInfo(), dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityDownloadFile), dbfs.WithNotRoot())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get file: %w", err)
|
|
}
|
|
|
|
versionType := types.EntityTypeVersion
|
|
find, targetEntity := fs.FindDesiredEntity(file, viewerSession.Version, dep.HashIDEncoder(), &versionType)
|
|
if !find {
|
|
return serializer.NewError(serializer.CodeNotFound, "version not found", nil)
|
|
}
|
|
|
|
if targetEntity.Size() > dep.SettingProvider().MaxOnlineEditSize(c) {
|
|
return fs.ErrFileSizeTooBig
|
|
}
|
|
|
|
entitySource, err := m.GetEntitySource(c, targetEntity.ID(), fs.WithEntity(targetEntity))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get entity source: %w", err)
|
|
}
|
|
|
|
defer entitySource.Close()
|
|
|
|
entitySource.Serve(c.Writer, c.Request,
|
|
entitysource.WithContext(c),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (service *WopiService) FileInfo(c *gin.Context) (*WopiFileInfo, error) {
|
|
uri, m, user, viewerSession, dep, err := prepareFs(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hasher := dep.HashIDEncoder()
|
|
settings := dep.SettingProvider()
|
|
|
|
opts := []fs.Option{
|
|
dbfs.WithFilePublicMetadata(),
|
|
dbfs.WithExtendedInfo(),
|
|
dbfs.WithNotRoot(),
|
|
dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityDownloadFile, dbfs.NavigatorCapabilityInfo),
|
|
}
|
|
file, err := m.Get(c, uri, opts...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get file: %w", err)
|
|
}
|
|
|
|
if file == nil {
|
|
return nil, serializer.NewError(serializer.CodeNotFound, "file not found", nil)
|
|
}
|
|
|
|
versionType := types.EntityTypeVersion
|
|
find, targetEntity := fs.FindDesiredEntity(file, viewerSession.Version, hasher, &versionType)
|
|
if !find {
|
|
return nil, serializer.NewError(serializer.CodeNotFound, "version not found", nil)
|
|
}
|
|
|
|
canEdit := file.PrimaryEntityID() == targetEntity.ID() && file.OwnerID() == user.ID && uri.FileSystem() == constants.FileSystemMy
|
|
cantPutRelative := !canEdit
|
|
siteUrl := settings.SiteURL(c)
|
|
info := &WopiFileInfo{
|
|
BaseFileName: file.DisplayName(),
|
|
Version: hashid.EncodeEntityID(hasher, targetEntity.ID()),
|
|
BreadcrumbBrandName: settings.SiteBasic(c).Name,
|
|
BreadcrumbBrandUrl: siteUrl.String(),
|
|
FileSharingPostMessage: file.OwnerID() == user.ID,
|
|
EnableShare: file.OwnerID() == user.ID,
|
|
FileVersionPostMessage: true,
|
|
ClosePostMessage: true,
|
|
PostMessageOrigin: "*",
|
|
FileNameMaxLength: dbfs.MaxFileNameLength,
|
|
LastModifiedTime: file.UpdatedAt().Format(time.RFC3339),
|
|
IsAnonymousUser: inventory.IsAnonymousUser(user),
|
|
UserFriendlyName: user.Nick,
|
|
UserId: hashid.EncodeUserID(hasher, user.ID),
|
|
ReadOnly: !canEdit,
|
|
Size: targetEntity.Size(),
|
|
OwnerId: hashid.EncodeUserID(hasher, file.OwnerID()),
|
|
SupportsRename: true,
|
|
SupportsReviewing: true,
|
|
SupportsLocks: true,
|
|
UserCanReview: canEdit,
|
|
UserCanWrite: canEdit,
|
|
UserCanNotWriteRelative: cantPutRelative,
|
|
BreadcrumbFolderName: uri.Dir(),
|
|
BreadcrumbFolderUrl: routes.FrontendHomeUrl(siteUrl, uri.DirUri().String()).String(),
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
type (
|
|
CreateViewerSessionService struct {
|
|
Uri string `json:"uri" form:"uri" binding:"required"`
|
|
Version string `json:"version" form:"version"`
|
|
ViewerID string `json:"viewer_id" form:"viewer_id" binding:"required"`
|
|
PreferredAction types.ViewerAction `json:"preferred_action" form:"preferred_action" binding:"required"`
|
|
}
|
|
CreateViewerSessionParamCtx struct{}
|
|
)
|
|
|
|
func (s *CreateViewerSessionService) Create(c *gin.Context) (*ViewerSessionResponse, error) {
|
|
dep := dependency.FromContext(c)
|
|
user := inventory.UserFromContext(c)
|
|
m := manager.NewFileManager(dep, user)
|
|
defer m.Recycle()
|
|
|
|
uri, err := fs.NewUriFromString(s.Uri)
|
|
if err != nil {
|
|
return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
|
|
}
|
|
|
|
// Find the given viewer
|
|
viewers := dep.SettingProvider().FileViewers(c)
|
|
var targetViewer *types.Viewer
|
|
for _, group := range viewers {
|
|
for _, viewer := range group.Viewers {
|
|
if viewer.ID == s.ViewerID && !viewer.Disabled {
|
|
targetViewer = &viewer
|
|
break
|
|
}
|
|
}
|
|
|
|
if targetViewer != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
if targetViewer == nil {
|
|
return nil, serializer.NewError(serializer.CodeParamErr, "unknown viewer id", err)
|
|
}
|
|
|
|
viewerSession, err := m.CreateViewerSession(c, uri, s.Version, targetViewer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := &ViewerSessionResponse{Session: viewerSession}
|
|
if targetViewer.Type == types.ViewerTypeWopi {
|
|
// For WOPI viewer, generate WOPI src
|
|
wopiSrc, err := wopi.GenerateWopiSrc(c, s.PreferredAction, targetViewer, viewerSession)
|
|
if err != nil {
|
|
return nil, serializer.NewError(serializer.CodeInternalSetting, "failed to generate wopi src", err)
|
|
}
|
|
res.WopiSrc = wopiSrc.String()
|
|
}
|
|
|
|
return res, nil
|
|
}
|