2025-04-20 17:31:25 +08:00
package explorer
import (
"errors"
"fmt"
2025-05-16 13:44:48 +08:00
"github.com/cloudreve/Cloudreve/v4/application/constants"
2025-04-21 19:43:09 +08:00
"net/http"
2025-05-23 15:37:02 +08:00
"path/filepath"
"strings"
2025-04-21 19:43:09 +08:00
"time"
2025-04-20 17:31:25 +08:00
"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/setting"
"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
2025-04-21 19:43:09 +08:00
file , err := m . Get ( c , uri , dbfs . WithRequiredCapabilities ( dbfs . NavigatorCapabilityLockFile ) , dbfs . WithNotRoot ( ) )
2025-04-20 17:31:25 +08:00
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
2025-04-21 19:43:09 +08:00
file , err := m . Get ( c , uri , dbfs . WithRequiredCapabilities ( dbfs . NavigatorCapabilityLockFile ) , dbfs . WithNotRoot ( ) )
2025-04-20 17:31:25 +08:00
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
}
2025-05-23 15:37:02 +08:00
func ( service * WopiService ) PutContent ( c * gin . Context , isPutRelative bool ) error {
2025-04-20 17:31:25 +08:00
uri , m , user , viewerSession , _ , err := prepareFs ( c )
if err != nil {
return err
}
// Make sure file exists and readable
2025-04-21 19:43:09 +08:00
file , err := m . Get ( c , uri , dbfs . WithRequiredCapabilities ( dbfs . NavigatorCapabilityUploadFile ) , dbfs . WithNotRoot ( ) )
2025-04-20 17:31:25 +08:00
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
}
2025-05-23 15:37:02 +08:00
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 ( )
}
2025-04-20 17:31:25 +08:00
subService := FileUpdateService {
2025-05-23 15:37:02 +08:00
Uri : fileUri ,
2025-04-20 17:31:25 +08:00
}
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 )
2025-05-23 15:37:02 +08:00
if isPutRelative {
c . JSON ( http . StatusOK , PutRelativeResponse {
Name : res . Name ,
} )
}
2025-04-20 17:31:25 +08:00
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
2025-04-21 19:43:09 +08:00
file , err := m . Get ( c , uri , dbfs . WithExtendedInfo ( ) , dbfs . WithRequiredCapabilities ( dbfs . NavigatorCapabilityDownloadFile ) , dbfs . WithNotRoot ( ) )
2025-04-20 17:31:25 +08:00
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 ( ) ,
2025-04-21 19:43:09 +08:00
dbfs . WithNotRoot ( ) ,
2025-05-16 13:44:48 +08:00
dbfs . WithRequiredCapabilities ( dbfs . NavigatorCapabilityDownloadFile , dbfs . NavigatorCapabilityInfo ) ,
2025-04-20 17:31:25 +08:00
}
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 )
}
2025-05-16 13:44:48 +08:00
canEdit := file . PrimaryEntityID ( ) == targetEntity . ID ( ) && file . OwnerID ( ) == user . ID && uri . FileSystem ( ) == constants . FileSystemMy
2025-05-23 15:37:02 +08:00
cantPutRelative := ! canEdit
2025-04-20 17:31:25 +08:00
siteUrl := settings . SiteURL ( c )
info := & WopiFileInfo {
2025-05-23 15:37:02 +08:00
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 ( ) ,
2025-04-20 17:31:25 +08:00
}
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 setting . 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 * setting . 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 == setting . 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
}