mirror of
https://github.com/ONLYOFFICE/document-server-integration.git
synced 2026-04-07 14:06:11 +08:00
388 lines
16 KiB
JavaScript
388 lines
16 KiB
JavaScript
/**
|
|
*
|
|
* (c) Copyright Ascensio System SIA 2023
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
|
|
const reqConsts = require('./request');
|
|
const fileUtility = require('../fileUtility');
|
|
const lockManager = require('./lockManager');
|
|
const fileSystem = require('fs');
|
|
const mime = require('mime');
|
|
const path = require('path');
|
|
const users = require('../users');
|
|
const docManager = require('../docManager');
|
|
|
|
// lock file editing
|
|
const lock = function (wopi, req, res, userHost) {
|
|
const requestLock = req.headers[reqConsts.requestHeaders.Lock.toLowerCase()];
|
|
|
|
const userAddress = req.docManager.curUserHostAddress(userHost); // get current user host address
|
|
const filePath = req.docManager.storagePath(wopi.id, userAddress); // get the storage path of the given file
|
|
|
|
if (!lockManager.hasLock(filePath)) {
|
|
// file isn't locked => lock
|
|
lockManager.lock(filePath, requestLock);
|
|
res.sendStatus(200);
|
|
} else if (lockManager.getLock(filePath) == requestLock) {
|
|
// lock matches current lock => extend duration
|
|
lockManager.lock(filePath, requestLock);
|
|
res.sendStatus(200);
|
|
} else {
|
|
// file locked by someone else => return lock mismatch
|
|
const lock = lockManager.getLock(filePath);
|
|
returnLockMismatch(res, lock, `File already locked by ${ lock}`)
|
|
}
|
|
}
|
|
|
|
// retrieve a lock on a file
|
|
const getLock = function (wopi, req, res, userHost) {
|
|
const userAddress = req.docManager.curUserHostAddress(userHost);
|
|
const filePath = req.docManager.storagePath(wopi.id, userAddress);
|
|
|
|
// get the lock of the specified file and set it as the X-WOPI-Lock header
|
|
res.setHeader(reqConsts.requestHeaders.lock, lockManager.getLock(filePath));
|
|
res.sendStatus(200);
|
|
}
|
|
|
|
// refresh the lock on a file by resetting its automatic expiration timer to 30 minutes
|
|
const refreshLock = function (wopi, req, res, userHost) {
|
|
const requestLock = req.headers[reqConsts.requestHeaders.Lock.toLowerCase()];
|
|
|
|
const userAddress = req.docManager.curUserHostAddress(userHost);
|
|
const filePath = req.docManager.storagePath(wopi.id, userAddress);
|
|
|
|
if (!lockManager.hasLock(filePath)) {
|
|
// file isn't locked => mismatch
|
|
returnLockMismatch(res, '', 'File isn\'t locked');
|
|
} else if (lockManager.getLock(filePath) == requestLock) {
|
|
// lock matches current lock => extend duration
|
|
lockManager.lock(filePath, requestLock);
|
|
res.sendStatus(200);
|
|
} else {
|
|
// lock mismatch
|
|
returnLockMismatch(res, lockManager.getLock(filePath), 'Lock mismatch');
|
|
}
|
|
}
|
|
|
|
// allow for file editing
|
|
const unlock = function (wopi, req, res, userHost) {
|
|
const requestLock = req.headers[reqConsts.requestHeaders.Lock.toLowerCase()];
|
|
|
|
const userAddress = req.docManager.curUserHostAddress(userHost);
|
|
const filePath = req.docManager.storagePath(wopi.id, userAddress);
|
|
|
|
if (!lockManager.hasLock(filePath)) {
|
|
// file isn't locked => mismatch
|
|
returnLockMismatch(res, '', 'File isn\'t locked');
|
|
} else if (lockManager.getLock(filePath) == requestLock) {
|
|
// lock matches current lock => unlock
|
|
lockManager.unlock(filePath);
|
|
res.sendStatus(200);
|
|
} else {
|
|
// lock mismatch
|
|
returnLockMismatch(res, lockManager.getLock(filePath), 'Lock mismatch');
|
|
}
|
|
}
|
|
|
|
// allow for file editing, and then immediately take a new lock on the file
|
|
const unlockAndRelock = function (wopi, req, res, userHost) {
|
|
const requestLock = req.headers[reqConsts.requestHeaders.Lock.toLowerCase()];
|
|
const oldLock = req.headers[reqConsts.requestHeaders.oldLock.toLowerCase()]; // get the X-WOPI-OldLock header
|
|
|
|
const userAddress = req.docManager.curUserHostAddress(userHost);
|
|
const filePath = req.docManager.storagePath(wopi.id, userAddress);
|
|
|
|
if (!lockManager.hasLock(filePath)) {
|
|
// file isn't locked => mismatch
|
|
returnLockMismatch(res, '', 'File isn\'t locked');
|
|
} else if (lockManager.getLock(filePath) == oldLock) {
|
|
// lock matches current lock => lock with new key
|
|
lockManager.lock(filePath, requestLock);
|
|
res.sendStatus(200);
|
|
} else {
|
|
// lock mismatch
|
|
returnLockMismatch(res, lockManager.getLock(filePath), 'Lock mismatch');
|
|
}
|
|
}
|
|
|
|
// request a message to retrieve a file
|
|
const getFile = function (wopi, req, res, userHost) {
|
|
const userAddress = req.docManager.curUserHostAddress(userHost);
|
|
|
|
const path = req.docManager.storagePath(wopi.id, userAddress);
|
|
|
|
res.setHeader('Content-Length', fileSystem.statSync(path).size);
|
|
res.setHeader('Content-Type', mime.getType(path));
|
|
|
|
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${ encodeURIComponent(wopi.id)}`);
|
|
|
|
const filestream = fileSystem.createReadStream(path); // open a file as a readable stream
|
|
filestream.pipe(res); // retrieve data from file stream and output it to the response object
|
|
}
|
|
|
|
// request a message to update a file
|
|
const putFile = function (wopi, req, res, userHost) {
|
|
const requestLock = req.headers[reqConsts.requestHeaders.Lock.toLowerCase()];
|
|
|
|
const userAddress = req.docManager.curUserHostAddress(userHost);
|
|
const storagePath = req.docManager.storagePath(wopi.id, userAddress);
|
|
|
|
if (!lockManager.hasLock(storagePath)) {
|
|
// ToDo: if body length is 0 bytes => handle document creation
|
|
|
|
// file isn't locked => mismatch
|
|
returnLockMismatch(res, '', 'File isn\'t locked');
|
|
} else if (lockManager.getLock(storagePath) == requestLock) {
|
|
// lock matches current lock => put file
|
|
saveFileFromBody(req, wopi.id, userAddress, true, (err, version) => {
|
|
if (!err) {
|
|
res.setHeader(reqConsts.requestHeaders.ItemVersion, version); // set the X-WOPI-ItemVersion header
|
|
}
|
|
res.sendStatus(err ? 404 : 200);
|
|
});
|
|
} else {
|
|
// lock mismatch
|
|
returnLockMismatch(res, lockManager.getLock(storagePath), 'Lock mismatch');
|
|
}
|
|
}
|
|
|
|
const putRelativeFile = function (wopi, req, res, userHost) {
|
|
const userAddress = req.docManager.curUserHostAddress(userHost);
|
|
const storagePath = req.docManager.storagePath(wopi.id, userAddress);
|
|
|
|
let filename = req.headers[reqConsts.requestHeaders.RelativeTarget.toLowerCase()]; // we cannot modify this filename
|
|
if (filename) {
|
|
if (req.docManager.existsSync(storagePath)) { // check if already exists
|
|
const overwrite = req.headers[reqConsts.requestHeaders.OverwriteRelativeTarget.toLowerCase()]; // overwrite header
|
|
if (overwrite && overwrite === 'true') { // check if we can overwrite
|
|
if (lockManager.hasLock(storagePath)) { // check if file locked
|
|
returnValidRelativeTarget(res, req.docManager.getCorrectName(wopi.id, userAddress)); // file is locked, cannot overwrite
|
|
return;
|
|
}
|
|
} else {
|
|
returnValidRelativeTarget(res, req.docManager.getCorrectName(wopi.id, userAddress)); // file exists and overwrite header is false
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
filename = req.headers[reqConsts.requestHeaders.SuggestedTarget.toLowerCase()]; // we can modify this filename
|
|
|
|
if (filename.startsWith('.')) { // check if extension
|
|
filename = fileUtility.getFileName(wopi.id, true) + filename; // get original filename with new extension
|
|
}
|
|
|
|
filename = req.docManager.getCorrectName(filename, userAddress); // get correct filename if already exists
|
|
}
|
|
|
|
const isConverted = req.headers[reqConsts.requestHeaders.FileConversion.toLowerCase()];
|
|
console.log(`putRelativeFile after conversation: ${ isConverted}`);
|
|
|
|
// if we got here, then we can save a file
|
|
saveFileFromBody(req, filename, userAddress, false, (err) => {
|
|
if (err) {
|
|
res.sendStatus(404);
|
|
return;
|
|
}
|
|
|
|
const serverUrl = req.docManager.getServerUrl(true);
|
|
const fileActionUrl = `${serverUrl }/wopi-action/${ filename }?action=`;
|
|
|
|
const fileInfo = {
|
|
Name: filename,
|
|
Url: `${serverUrl }/wopi/files/${ filename}`,
|
|
HostViewUrl: `${fileActionUrl }view`,
|
|
HostEditNewUrl: `${fileActionUrl }editnew`,
|
|
HostEditUrl: `${fileActionUrl }edit`,
|
|
};
|
|
res.status(200).send(fileInfo);
|
|
});
|
|
}
|
|
|
|
// return information about the file properties, access rights and editor settings
|
|
const checkFileInfo = function (wopi, req, res, userHost) {
|
|
const userAddress = req.docManager.curUserHostAddress(userHost);
|
|
const version = req.docManager.getKey(wopi.id, userAddress);
|
|
|
|
const path = req.docManager.storagePath(wopi.id, userAddress);
|
|
// add wopi query
|
|
let query = new URLSearchParams(wopi.accessToken);
|
|
const user = users.getUser(query.get('userid'));
|
|
|
|
// create the file information object
|
|
const fileInfo = {
|
|
BaseFileName: wopi.id,
|
|
OwnerId: req.docManager.getFileData(wopi.id, userAddress)[1],
|
|
Size: fileSystem.statSync(path).size,
|
|
UserId: user.id,
|
|
UserFriendlyName: user.name,
|
|
Version: version,
|
|
UserCanWrite: true,
|
|
SupportsGetLock: true,
|
|
SupportsLocks: true,
|
|
SupportsUpdate: true,
|
|
};
|
|
res.status(200).send(fileInfo);
|
|
}
|
|
|
|
const saveFileFromBody = function (req, filename, userAddress, isNewVersion, callback) {
|
|
if (req.body) {
|
|
let storagePath = req.docManager.storagePath(filename, userAddress);
|
|
let historyPath = req.docManager.historyPath(filename, userAddress); // get the path to the file history
|
|
if (historyPath == '') { // if it is empty
|
|
historyPath = req.docManager.historyPath(filename, userAddress, true); // create it
|
|
req.docManager.createDirectory(historyPath); // and create a new directory for the history
|
|
}
|
|
|
|
let version = 0;
|
|
if (isNewVersion) {
|
|
let count_version = req.docManager.countVersion(historyPath); // get the last file version
|
|
version = count_version + 1; // get a number of a new file version
|
|
let versionPath = req.docManager.versionPath(filename, userAddress, version); // get the path to the specified file version
|
|
req.docManager.createDirectory(versionPath); // and create a new directory for the specified version
|
|
|
|
let path_prev = path.join(versionPath, `prev${ fileUtility.getFileExtension(filename)}`); // get the path to the previous file version
|
|
fileSystem.renameSync(storagePath, path_prev); // synchronously rename the given file as the previous file version
|
|
}
|
|
|
|
const filestream = fileSystem.createWriteStream(storagePath);
|
|
req.pipe(filestream);
|
|
req.on('end', () => {
|
|
filestream.close();
|
|
callback(null, version);
|
|
})
|
|
} else {
|
|
callback('empty body');
|
|
}
|
|
}
|
|
|
|
// return name that wopi-client can use as the value of X-WOPI-RelativeTarget in a future PutRelativeFile operation
|
|
const returnValidRelativeTarget = function (res, filename) {
|
|
res.setHeader(reqConsts.requestHeaders.ValidRelativeTarget, filename); // set the X-WOPI-ValidRelativeTarget header
|
|
res.sendStatus(409); // file with that name already exists
|
|
}
|
|
|
|
// return lock mismatch
|
|
const returnLockMismatch = function (res, lock, reason) {
|
|
res.setHeader(reqConsts.requestHeaders.Lock, lock || ''); // set the X-WOPI-Lock header
|
|
if (reason) { // if there is a reason for lock mismatch
|
|
res.setHeader(reqConsts.requestHeaders.LockFailureReason, reason); // set it as the X-WOPI-LockFailureReason header
|
|
}
|
|
res.sendStatus(409); // conflict
|
|
}
|
|
|
|
// parse wopi request
|
|
const parseWopiRequest = function (req) {
|
|
const wopiData = {
|
|
requestType: reqConsts.requestType.None,
|
|
accessToken: req.query.access_token,
|
|
id: req.params.id
|
|
}
|
|
|
|
// get the request path
|
|
const reqPath = req.path.substring('/wopi/'.length)
|
|
|
|
if (reqPath.startsWith('files')) { // if it starts with "files"
|
|
if (reqPath.endsWith('/contents')) { // ends with "/contents"
|
|
if (req.method == 'GET') { // and the request method is GET
|
|
wopiData.requestType = reqConsts.requestType.GetFile; // then the request type is GetFile
|
|
} else if (req.method == 'POST') { // if the request method is POST
|
|
wopiData.requestType = reqConsts.requestType.PutFile; // then the request type is PutFile
|
|
}
|
|
} else {
|
|
if (req.method == 'GET') { // otherwise, if the request method is GET
|
|
wopiData.requestType = reqConsts.requestType.CheckFileInfo; // the request type is CheckFileInfo
|
|
} else if (req.method == 'POST') { // if the request method is POST
|
|
const wopiOverride = req.headers[reqConsts.requestHeaders.RequestType.toLowerCase()]; // get the X-WOPI-Override header which determines the request type
|
|
switch (wopiOverride) {
|
|
case 'LOCK': // if it is equal to LOCK
|
|
if (req.headers[reqConsts.requestHeaders.OldLock.toLowerCase()]) { // check if the request sends the X-WOPI-OldLock header
|
|
wopiData.requestType = reqConsts.requestType.UnlockAndRelock; // if yes, then the request type is UnlockAndRelock
|
|
} else {
|
|
wopiData.requestType = reqConsts.requestType.Lock; // otherwise, it is Lock
|
|
}
|
|
break;
|
|
|
|
case 'GET_LOCK': // if it is equal to GET_LOCK
|
|
wopiData.requestType = reqConsts.requestType.GetLock; // the request type is GetLock
|
|
break;
|
|
|
|
case 'REFRESH_LOCK': // if it is equal to REFRESH_LOCK
|
|
wopiData.requestType = reqConsts.requestType.RefreshLock; // the request type is RefreshLock
|
|
break;
|
|
|
|
case 'UNLOCK': // if it is equal to UNLOCK
|
|
wopiData.requestType = reqConsts.requestType.Unlock; // the request type is Unlock
|
|
break;
|
|
|
|
case 'PUT_RELATIVE': // if it is equal to PUT_RELATIVE
|
|
wopiData.requestType = reqConsts.requestType.PutRelativeFile; // the request type is PutRelativeFile (creates a new file on the host based on the current file)
|
|
break;
|
|
|
|
case 'RENAME_FILE': // if it is equal to RENAME_FILE
|
|
wopiData.requestType = reqConsts.requestType.RenameFile; // the request type is RenameFile (renames a file)
|
|
break;
|
|
|
|
case 'PUT_USER_INFO': // if it is equal to PUT_USER_INFO
|
|
wopiData.requestType = reqConsts.requestType.PutUserInfo; // the request type is PutUserInfo (stores some basic user information on the host)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else if (reqPath.startsWith('folders')) {
|
|
|
|
}
|
|
|
|
return wopiData;
|
|
}
|
|
|
|
const actionMapping = {};
|
|
actionMapping[reqConsts.requestType.GetFile] = getFile;
|
|
actionMapping[reqConsts.requestType.PutFile] = putFile;
|
|
actionMapping[reqConsts.requestType.PutRelativeFile] = putRelativeFile;
|
|
actionMapping[reqConsts.requestType.CheckFileInfo] = checkFileInfo;
|
|
actionMapping[reqConsts.requestType.UnlockAndRelock] = unlockAndRelock;
|
|
actionMapping[reqConsts.requestType.Lock] = lock;
|
|
actionMapping[reqConsts.requestType.GetLock] = getLock;
|
|
actionMapping[reqConsts.requestType.RefreshLock] = refreshLock;
|
|
actionMapping[reqConsts.requestType.Unlock] = unlock;
|
|
|
|
exports.fileRequestHandler = (req, res) => {
|
|
let userAddress = null;
|
|
req.docManager = new docManager(req, res);
|
|
if (req.params.id.includes('@')) { // if there is the "@" sign in the id parameter
|
|
const split = req.params.id.split('@'); // split this parameter by "@"
|
|
[req.params.id] = split; // rewrite id with the first part of the split parameter
|
|
[,userAddress] = split; // save the second part as the user address
|
|
}
|
|
|
|
const wopiData = parseWopiRequest(req); // get the wopi data
|
|
|
|
// an error of the unknown request type
|
|
if (wopiData.requestType == reqConsts.requestType.None) {
|
|
res.status(500).send({ title: 'fileHandler', method: req.method, id: req.params.id, error: 'unknown' });
|
|
return;
|
|
}
|
|
|
|
// an error of the unsupported request type
|
|
const action = actionMapping[wopiData.requestType];
|
|
if (!action) {
|
|
res.status(501).send({ title: 'fileHandler', method: req.method, id: req.params.id, error: 'unsupported' });
|
|
return;
|
|
}
|
|
|
|
action(wopiData, req, res, userAddress);
|
|
}
|