@@ -25,9 +25,11 @@ import { haveSpaceEnvPermissions } from '../../../spaces/utils/permissions'
2525import type { UserModel } from '../../../users/models/user.model'
2626import { getAvatarBase64 } from '../../../users/utils/avatar'
2727import { DEPTH , LOCK_SCOPE } from '../../../webdav/constants/webdav'
28- import { WebDAVLock } from '../../../webdav/interfaces/webdav.interface'
2928import { FILE_MODE } from '../../constants/operations'
3029import type { FileDBProps } from '../../interfaces/file-db-props.interface'
30+ import { FileLockOptions } from '../../interfaces/file-lock.interface'
31+ import { FileLockProps } from '../../interfaces/file-props.interface'
32+ import { LockConflict } from '../../models/file-lock-error'
3133import { FilesLockManager } from '../../services/files-lock-manager.service'
3234import {
3335 copyFileContent ,
@@ -41,12 +43,12 @@ import {
4143 writeFromStream
4244} from '../../utils/files'
4345import {
46+ ONLY_OFFICE_APP_LOCK ,
4447 ONLY_OFFICE_CACHE_KEY ,
4548 ONLY_OFFICE_CONVERT_ERROR ,
4649 ONLY_OFFICE_CONVERT_EXTENSIONS ,
4750 ONLY_OFFICE_EXTENSIONS ,
4851 ONLY_OFFICE_INTERNAL_URI ,
49- ONLY_OFFICE_OWNER_LOCK ,
5052 ONLY_OFFICE_TOKEN_QUERY_PARAM_NAME
5153} from './only-office.constants'
5254import { OnlyOfficeReqDto } from './only-office.dtos'
@@ -81,20 +83,31 @@ export class OnlyOfficeManager {
8183 if ( ! ONLY_OFFICE_EXTENSIONS . has ( fileExtension ) ) {
8284 throw new HttpException ( 'Document not supported' , HttpStatus . BAD_REQUEST )
8385 }
84- const mode : FILE_MODE = haveSpaceEnvPermissions ( space , SPACE_OPERATION . MODIFY ) ? FILE_MODE . EDIT : FILE_MODE . VIEW
86+ let hasLock : false | FileLockProps = false
87+ let mode : FILE_MODE = haveSpaceEnvPermissions ( space , SPACE_OPERATION . MODIFY ) ? FILE_MODE . EDIT : FILE_MODE . VIEW
8588 if ( mode === FILE_MODE . EDIT ) {
8689 // Check lock conflicts
8790 try {
88- await this . filesLockManager . checkConflicts ( space . dbFile , DEPTH . RESOURCE , { userId : user . id , lockScope : LOCK_SCOPE . SHARED } )
89- } catch {
90- throw new HttpException ( 'The file is locked' , HttpStatus . LOCKED )
91+ await this . filesLockManager . checkConflicts ( space . dbFile , DEPTH . RESOURCE , {
92+ userId : user . id ,
93+ app : ONLY_OFFICE_APP_LOCK ,
94+ lockScope : LOCK_SCOPE . SHARED
95+ } )
96+ } catch ( e ) {
97+ if ( e instanceof LockConflict ) {
98+ hasLock = this . filesLockManager . convertLockToFileLockProps ( e . lock )
99+ mode = FILE_MODE . VIEW
100+ } else {
101+ this . logger . error ( `${ this . getSettings . name } - ${ e } ` )
102+ throw new HttpException ( 'Unable to check file lock' , HttpStatus . INTERNAL_SERVER_ERROR )
103+ }
91104 }
92105 }
93106 const isMobile : boolean = this . mobileRegex . test ( req . headers [ 'user-agent' ] )
94107 const authToken : string = await this . genAuthToken ( user )
95108 const fileUrl = this . buildUrl ( API_ONLY_OFFICE_DOCUMENT , space . url , authToken )
96109 const callBackUrl = this . buildUrl ( API_ONLY_OFFICE_CALLBACK , space . url , authToken )
97- const config : OnlyOfficeReqDto = await this . genConfiguration ( user , space , mode , fileUrl , fileExtension , callBackUrl , isMobile )
110+ const config : OnlyOfficeReqDto = await this . genConfiguration ( user , space , mode , fileUrl , fileExtension , callBackUrl , isMobile , hasLock )
98111 config . config . token = await this . genPayloadToken ( config . config )
99112 return config
100113 }
@@ -104,24 +117,28 @@ export class OnlyOfficeManager {
104117 try {
105118 switch ( callBackData . status ) {
106119 case 1 :
120+ // users connect / disconnect
107121 await this . checkFileLock ( user , space , callBackData )
108122 this . logger . debug ( `document is being edited : ${ space . url } ` )
109123 break
110124 case 2 :
125+ // No active users on the document
111126 await this . checkFileLock ( user , space , callBackData )
112127 if ( callBackData . notmodified ) {
113128 this . logger . debug ( `document was edited but closed with no changes : ${ space . url } ` )
114129 } else {
115- this . logger . debug ( `document was edited but not saved (let's do it) : ${ space . url } ` )
130+ this . logger . debug ( `document was edited and closed but not saved (let's do it) : ${ space . url } ` )
116131 await this . saveDocument ( space , callBackData . url )
117132 }
133+ await this . removeFileLock ( user . id , space )
118134 await this . removeDocumentKey ( space )
119135 break
120136 case 3 :
121137 this . logger . error ( `document cannot be saved, an error has occurred (try to save it) : ${ space . url } ` )
122138 await this . saveDocument ( space , callBackData . url )
123139 break
124140 case 4 :
141+ // No active users on the document
125142 await this . removeFileLock ( user . id , space )
126143 await this . removeDocumentKey ( space )
127144 this . logger . debug ( `document was closed with no changes : ${ space . url } ` )
@@ -151,10 +168,13 @@ export class OnlyOfficeManager {
151168 fileUrl : string ,
152169 fileExtension : string ,
153170 callBackUrl : string ,
154- isMobile : boolean
171+ isMobile : boolean ,
172+ hasLock : false | FileLockProps
155173 ) : Promise < OnlyOfficeReqDto > {
174+ const canEdit = mode === FILE_MODE . EDIT
156175 const documentType = ONLY_OFFICE_EXTENSIONS . get ( fileExtension )
157176 return {
177+ hasLock : hasLock ,
158178 documentServerUrl : this . externalOnlyOfficeServer || `${ this . contextManager . headerOriginUrl ( ) } ${ ONLY_OFFICE_INTERNAL_URI } ` ,
159179 config : {
160180 type : isMobile ? 'mobile' : 'desktop' ,
@@ -167,12 +187,12 @@ export class OnlyOfficeManager {
167187 key : await this . getDocumentKey ( space ) ,
168188 permissions : {
169189 download : true ,
170- edit : mode === FILE_MODE . EDIT ,
190+ edit : canEdit ,
171191 changeHistory : false ,
172- comment : true ,
173- fillForms : true ,
192+ comment : canEdit ,
193+ fillForms : canEdit ,
174194 print : true ,
175- review : mode === FILE_MODE . EDIT
195+ review : canEdit
176196 } ,
177197 url : fileUrl
178198 } ,
@@ -240,11 +260,17 @@ export class OnlyOfficeManager {
240260 private async checkFileLock ( user : UserModel , space : SpaceEnv , callBackData : OnlyOfficeCallBack ) {
241261 for ( const action of callBackData . actions ) {
242262 if ( action . type === 0 ) {
243- // disconnect
244- await this . removeFileLock ( parseInt ( action . userid ) , space )
263+ // Disconnect
264+ // Remove the lock if no other users are active on the document
265+ if ( ! Array . isArray ( callBackData . users ) ) {
266+ await this . removeFileLock ( parseInt ( action . userid ) , space )
267+ }
245268 } else if ( action . type === 1 ) {
246- // connect
247- await this . createFileLock ( user , space )
269+ // Connect
270+ // Create the lock if it's the first user to open the document
271+ if ( Array . isArray ( callBackData . users ) && callBackData . users . length === 1 ) {
272+ await this . createFileLock ( user , space )
273+ }
248274 }
249275 }
250276 }
@@ -253,13 +279,13 @@ export class OnlyOfficeManager {
253279 const [ ok , _fileLock ] = await this . filesLockManager . create (
254280 user ,
255281 space . dbFile ,
282+ ONLY_OFFICE_APP_LOCK ,
256283 DEPTH . RESOURCE ,
257284 {
258- lockroot : null ,
259- locktoken : null ,
260- lockscope : LOCK_SCOPE . SHARED ,
261- owner : `${ ONLY_OFFICE_OWNER_LOCK } - ${ user . fullName } (${ user . email } )`
262- } satisfies WebDAVLock ,
285+ lockRoot : null ,
286+ lockToken : null ,
287+ lockScope : LOCK_SCOPE . SHARED
288+ } satisfies FileLockOptions ,
263289 this . expiration
264290 )
265291 if ( ! ok ) {
@@ -284,6 +310,7 @@ export class OnlyOfficeManager {
284310 }
285311
286312 private async getDocumentKey ( space : SpaceEnv ) : Promise < string > {
313+ // Uniq key to identify the document in OnlyOffice
287314 const cacheKey = this . getCacheKey ( space . dbFile )
288315 const existingDocKey : string = await this . cache . get ( cacheKey )
289316 if ( existingDocKey ) {
0 commit comments