Edit Office Documents on Server using Local office client with WebDAV

Sandun Isuru Niraj
14 min readDec 6, 2023

WebDAV, also recognized as Web Distributed Authoring and Versioning, constitutes a subset of implementations within the HTTP protocol. It facilitates document creation and modification within a server environment. Specifically, it empowers users to edit documents, such as Office files, seamlessly through the local Office client, akin to OneDrive functionality.

Today’s discussion centers around the real-time editing of documents stored within AWS S3 using WebDAV. To augment its capabilities, I’ve integrated MongoDB as the repository for document metadata. Once you’ve grasped the fundamentals, these technologies can be seamlessly substituted with alternatives such as Azure or personalized document storage solutions.

To commence, I’ve integrated a third-party library known as webdav-server accessible via npm, for this purpose. My development environment consists of NodeJS in tandem with the ExpressJS framework. Notably, TypeScript is also a viable option given that the aforementioned library extends support to TypeScript.

First, I am running npm init for initialize a project and create an index.js for my entry point.

Then I will install few dependencies that I need for the implementation.

npm install express webdav-server mongoose mime-db etag cors body-parser aws-sdk dotenv mime-lookup uuid lodash moment

We’re integrating the AWS SDK to access S3 along with the webdav-server library, which serves as a supportive framework for our WebDAV implementation.

Within the webdav-server, customizing involves extending the FileSystem class with our bespoke class. Additionally, there’s a set of abstract class methods to implement, specifically tailored to facilitate reading and writing files and metadata. Our current focus lies in establishing a foundational server implementation, offering a starting point for further customizations and tailored adjustments.

So these are the abstract class methods as per the documentation.

abstract class FileSystem implements ISerializableFileSystem
{
protected _fastExistCheck?(ctx : RequestContext, path : Path, callback : (exists : boolean) => void) : void
protected _create?(path : Path, ctx : CreateInfo, callback : SimpleCallback) : void
protected _etag?(path : Path, ctx : ETagInfo, callback : ReturnCallback<string>) : void
protected _delete?(path : Path, ctx : DeleteInfo, callback : SimpleCallback) : void
protected _openWriteStream?(path : Path, ctx : OpenWriteStreamInfo, callback : ReturnCallback<Writable>) : void
protected _openReadStream?(path : Path, ctx : OpenReadStreamInfo, callback : ReturnCallback<Readable>) : void
protected _move?(pathFrom : Path, pathTo : Path, ctx : MoveInfo, callback : ReturnCallback<boolean>) : void
protected _copy?(pathFrom : Path, pathTo : Path, ctx : CopyInfo, callback : ReturnCallback<boolean>) : void
protected _rename?(pathFrom : Path, newName : string, ctx : RenameInfo, callback : ReturnCallback<boolean>) : void
protected _mimeType?(path : Path, ctx : MimeTypeInfo, callback : ReturnCallback<string>) : void
protected _size?(path : Path, ctx : SizeInfo, callback : ReturnCallback<number>) : void
protected _availableLocks?(path : Path, ctx : AvailableLocksInfo, callback : ReturnCallback<LockKind[]>) : void
protected abstract _lockManager(path : Path, ctx : LockManagerInfo, callback : ReturnCallback<ILockManager>) : void
protected abstract _propertyManager(path : Path, ctx : PropertyManagerInfo, callback : ReturnCallback<IPropertyManager>) : void
protected _readDir?(path : Path, ctx : ReadDirInfo, callback : ReturnCallback<string[] | Path[]>) : void
protected _creationDate?(path : Path, ctx : CreationDateInfo, callback : ReturnCallback<number>) : void
protected _lastModifiedDate?(path : Path, ctx : LastModifiedDateInfo, callback : ReturnCallback<number>) : void
protected _displayName?(path : Path, ctx : DisplayNameInfo, callback : ReturnCallback<string>) : void
protected abstract _type(path : Path, ctx : TypeInfo, callback : ReturnCallback<ResourceType>) : void
protected _privilegeManager?(path : Path, info : PrivilegeManagerInfo, callback : ReturnCallback<PrivilegeManager>)
}

But all from these we are going to implement few of these.

First of all create a class and export it.

module.exports = class S3FileSystem extends webdav.FileSystem {
useCache = false;
resources = {};
constructor() {
super();
}
}

Initially, let’s configure our database models. To connect to MongoDB, we’ve already installed Mongoose. Begin by creating a new folder titled ‘database’ and include the DocumentSchema.js file within it.

const { Schema } = require("mongoose");

module.exports.DocumentSchema = new Schema({
documentId: { type: String, required: true, unique: true },
title: { type: String, required: true, unique: false },
createdOn: { type: Date, required: true, unique: false },
updatedOn: { type: Date, required: true, unique: false },
extension: { type: String, required: true, unique: false },
key: { type: String, required: true, unique: false },
})

Next, create a file named dbHelper.js to export the database connection setup. For secure handling of sensitive information, consider creating an .env file and incorporating the dotenv library within your main index.js file. Subsequently, ensure to include and assign the MongoDB URL within the .env file.

const { connect, connection, model } = require("mongoose")
const mongoDBUrl = process.env.MONGODB_URL;

connect(mongoDBUrl);
const db = connection;

db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', function () {
console.log('Connected to MongoDB');
})

const { DocumentSchema } = require("./schema/DocumentSchema")

module.exports.DocumentModel = model('Document', DocumentSchema, "documents");

Now you can access information in database using this Singleton “DocumentModel” object.

To begin, our primary goal is to craft a collection of reusable helper methods within the S3FileSystem class. These methods will streamline the retrieval of document metadata and file contents from S3 storage. Furthermore, we’ll also devise an additional auxiliary function focused on obtaining Path information, specifically to extract document ID and version details from the provided WebDAV path in the application.

We’ll have the WebDAV path as like below.

/{Document ID}/{version}/document.{extension}

So we have to get the path information as separate variable with below method.

getPathInformation(path) {
const pathParts = path.paths;
const documentVersion = pathParts[1];
const documentId = pathParts[0];
return { documentVersion, documentId };
}

For getting document Metadata, we have imported DocumentModel object to query document from the Database and store it in the local cache in the app call resources for fast data fetching.

getMetaData(path, callback) {
const { documentId } = this.getPathInformation(path);
if (this.useCache && this.resources[documentId] && this.resources[documentId].metadata) {
callback(undefined, this.resources[documentId].metadata);
} else {
if (path.isRoot()) {
callback(undefined, {
'.tag': 'folder',
name: '',
size: 0
});
} else {
DocumentModel.findOne({ documentId }).then(document => {
if (!this.resources[documentId]) {
this.resources[documentId].metadata = {};
}
this.resources[documentId].metadata = document;
callback(undefined, document);
}).catch(err => {
console.log(err)
callback(err);
});
}
}
}

Also, for fetching file content from S3, we have crafted another method using AWS SDK. If we need to view the latest version of document every time, we can pass “latest” instead of document version in WebDAV URL.

getFileData(key, version, callback) {
s3.listObjectVersions({
Bucket: bucketName,
Prefix: key
}, (e, versionData) => {
if (e) {
console.log(e)
callback(webdav.Errors.ResourceNotFound);
}
const isLatest = version === "latest";
const requestedVersion = isLatest ? _.first(versionData.Versions, (x) => x.IsLatest).VersionId : documentVersion;
let params = {
Key: key,
Bucket: bucketName
}
if (versionData.Versions.length) params.VersionId = requestedVersion;

s3.getObject(params, (err, fileData) => {
if (err) {
console.log(err)
callback(webdav.Errors.ResourceNotFound);
}

callback(undefined, { size: fileData.ContentLength, content: fileData.Body });
})
})
}

After adding those methods, our class will see like this.

const webdav = require('webdav-server').v2;
const AWS = require('aws-sdk');
const _ = require('lodash');
const s3 = new AWS.S3({ region: 'us-east-1' });
const { DocumentModel } = require('./database/dbHelper');
const bucketName = process.env.BUCKET_NAME;

module.exports = class S3FileSystem extends webdav.FileSystem {

useCache = false;
resources = {};

constructor() {
super();
this.useCache = false;
}

getPathInformation(path) {
const pathParts = path.paths;
const documentVersion = pathParts[1];
const documentId = pathParts[0];

return { documentVersion, documentId };
}

getMetaData(path, callback) {
const { documentId } = this.getPathInformation(path);
if (this.useCache && this.resources[documentId] && this.resources[documentId].metadata) {
callback(undefined, this.resources[documentId].metadata);
} else {
if (path.isRoot()) {
callback(undefined, {
'.tag': 'folder',
name: '',
size: 0
});
} else {
DocumentModel.findOne({ documentId }).then(document => {
console.log(document)
if (!this.resources[documentId]) {
this.resources[documentId].metadata = {};
}
this.resources[documentId].metadata = document;
callback(undefined, document);
}).catch(err => {
console.log(err)
callback(err);
});
}
}
}

getFileData(key, version, callback) {
s3.listObjectVersions({
Bucket: bucketName,
Prefix: key
}, (e, versionData) => {
if (e) {
console.log(e)
callback(webdav.Errors.ResourceNotFound);
}
const isLatest = version === "latest";
const requestedVersion = isLatest ? _.first(versionData.Versions, (x) => x.IsLatest).VersionId : documentVersion;
let params = {
Key: key,
Bucket: bucketName
}
if (versionData.Versions.length) params.VersionId = requestedVersion;

s3.getObject(params, (err, fileData) => {
if (err) {
console.log(err)
callback(webdav.Errors.ResourceNotFound);
}

callback(undefined, { size: fileData.ContentLength, content: fileData.Body });
})
})
}
}

Next on our agenda is the implementation of essential methods required for the WebDAV server to operate seamlessly. Our initial focus will be on implementing the ‘_openReadStream’ method, responsible for retrieving document content from S3 and facilitating its transfer to the WebDAV server.

_openReadStream(path, ctx, callback) {
this.getMetaData(path, (err, metadata) => {
if (err) {
callback(webdav.Errors.ResourceNotFound);
}
const { documentVersion } = this.getPathInformation(path);
const documentKey = metadata.key;
const etagValue = etag(new Date(metadata.updatedOn).toUTCString());

if (ctx.context.request.headers['if-none-match'] === etagValue) {
console.log('Returning 304 as file has not changed.');
ctx.context.response.statusCode = 304
ctx.context.response.end();
return;
}

this.getFileData(documentKey, documentVersion, (err, data) => {
if (err) {
callback(webdav.Errors.ResourceNotFound)
}

const content = data.content;
var stream = new webdav.VirtualFileReadable([content]);
var contentType = mime.lookup(metadata.extension);
ctx.context.response.setHeader('etag', etagValue);
ctx.context.response.setHeader('Content-type', contentType);

callback(undefined, stream)
})
});
};

To ensure the tagging of each document version opened in the WebDAV server for conflict resolution, we need to import the ‘etag’ library into the app. This library facilitates the addition of an etag, aiding in identifying conflicts effectively.

Subsequently, our focus shifts to implementing the ‘_openWriteStream’ method, tasked with saving edited content to S3, updating document versions, and handling related functionalities. In here we are updating the last updated time on the document for version handling purposes.

_openWriteStream(path, ctx, callback) {
this.getMetaData(path, (err, metadata) => {
if (err) {
callback(webdav.Errors.ResourceNotFound);
}

const documentKey = metadata.key;
let content = [];
let stream = new webdav.VirtualFileWritable(content);
stream.on('finish', () => {
s3.upload({
Bucket: bucketName,
Key: documentKey,
Body: Buffer.concat(content)
}, (err, data) => {
if (err) {
console.log(err);
}
})
});
DocumentModel.updateOne({ documentId: metadata.documentId }, {
$set: {
updatedOn: new Date().toISOString()
}
}).then(data => {
callback(null, stream);
})

})
};

We already done the most important methods now. Then we have to implement two other important methods that responsible for property and lock management.

_lockManager(path, ctx, callback) {
const { documentId } = this.getPathInformation(path);
this.getMetaData(path, (e) => {
if (e) {
return callback(webdav.Errors.ResourceNotFound);
}

if (!this.resources[documentId])
this.resources[documentId] = {};

if (!this.resources[documentId].locks)
this.resources[documentId].locks = new webdav.LocalLockManager();

callback(undefined, this.resources[documentId].locks);
})
};

_propertyManager(path, ctx, callback) {
this.getMetaData(path, (e) => {
if (e) {
return callback(webdav.Errors.ResourceNotFound);
}

if (!this.resources[documentId])
this.resources[documentId] = {};

if (!this.resources[documentId].props)
this.resources[documentId].props = new webdav.LocalPropertyManager();

callback(undefined, this.resources[documentId].props);
})
};

You can implement any kind of custom lock managers and property managers in the future with the help of documentation, but for the simplest form we can use inbuilt lock manager and property manager.

Then we have to implement few other methods that supports for basic functions on WebDAV server.

_size(path, ctx, callback) {
const { documentVersion, documentId } = this.getPathInformation(path);
if (this.useCache && this.resources[documentId] && this.resources[documentId].size) {
callback(undefined, this.resources[documentId].size)
} else {
this.getMetaData(path, (err, metadata) => {
if (err) {
callback(webdav.Errors.ResourceNotFound);
}
const documentKey = metadata.key;
this.getFileData(documentKey, documentVersion, (err, data) => {
if (err) {
callback(webdav.Errors.ResourceNotFound)
}
const size = data.size;

if (!this.resources[documentId])
this.resources[documentId] = {};

this.resources[documentId].size = size;
callback(undefined, size);
})
})
}
};

_creationDate(path, ctx, callback) {
this._lastModifiedDate(path, ctx, callback);

this.getMetaData(path, (e, data) => {
if (e)
return callback(webdav.Errors.ResourceNotFound);

callback(undefined, new Date(data.createdOn).toISOString());
})
};

_lastModifiedDate(path, ctx, callback) {
this.getMetaData(path, (e, data) => {
if (e)
return callback(webdav.Errors.ResourceNotFound);

callback(undefined, new Date(data.createdOn).toISOString());
})
};

_type(path, ctx, callback) {
const { documentId } = this.getPathInformation(path);
if (this.useCache && this.resources[documentId] && this.resources[documentId].type) {
callback(undefined, this.resources[documentId].type);
} else {
this.getMetaData(path, (e, data) => {
if (e)
callback(webdav.Errors.ResourceNotFound);

const type = webdav.ResourceType.File;

if (!this.resources[documentId])
this.resources[documentId] = {};

this.resources[documentId].type = type;
callback(undefined, type);
})
}
};

_mimeType(path, ctx, callback) {
this.getMetaData(path, (e, data) => {
if (e)
return callback(webdav.Errors.ResourceNotFound);

callback(null, mime.lookup(data.extension));
})
}

After adding all the nessesary methods, final S3FileSystem class can be seen like below.

const webdav = require('webdav-server').v2;
const AWS = require('aws-sdk');
var MimeLookup = require('mime-lookup');
var mime = new MimeLookup(require('mime-db'));
const s3 = new AWS.S3({ region: 'us-east-1' });
const _ = require('lodash');
var etag = require('etag');
const { DocumentModel } = require('./database/dbHelper');
const bucketName = process.env.BUCKET_NAME;

module.exports = class S3FileSystem extends webdav.FileSystem {

useCache = false;
resources = {};

constructor() {
super();
this.useCache = false;
}

getPathInformation(path) {
const pathParts = path.paths;
const documentVersion = pathParts[1];
const documentId = pathParts[0];

return { documentVersion, documentId };
}

getMetaData(path, callback) {
const { documentId } = this.getPathInformation(path);
if (this.useCache && this.resources[documentId] && this.resources[documentId].metadata) {
callback(undefined, this.resources[documentId].metadata);
} else {
if (path.isRoot()) {
callback(undefined, {
'.tag': 'folder',
name: '',
size: 0
});
} else {
DocumentModel.findOne({ documentId }).then(document => {
if (!this.resources[documentId]) {
this.resources[documentId].metadata = {};
}
this.resources[documentId].metadata = document;
callback(undefined, document);
}).catch(err => {
console.log(err)
callback(err);
});
}
}
}

getFileData(key, version, callback) {
s3.listObjectVersions({
Bucket: bucketName,
Prefix: key
}, (e, versionData) => {
if (e) {
console.log(e)
callback(webdav.Errors.ResourceNotFound);
}
const isLatest = version === "latest";
const requestedVersion = isLatest ? _.first(versionData.Versions, (x) => x.IsLatest).VersionId : documentVersion;
let params = {
Key: key,
Bucket: bucketName
}
if (versionData.Versions.length) params.VersionId = requestedVersion;

s3.getObject(params, (err, fileData) => {
if (err) {
console.log(err)
callback(webdav.Errors.ResourceNotFound);
}

callback(undefined, { size: fileData.ContentLength, content: fileData.Body });
})
})
}

_openWriteStream(path, ctx, callback) {
this.getMetaData(path, (err, metadata) => {
if (err) {
callback(webdav.Errors.ResourceNotFound);
}

const documentKey = metadata.key;
let content = [];
let stream = new webdav.VirtualFileWritable(content);
stream.on('finish', () => {
s3.upload({
Bucket: bucketName,
Key: documentKey,
Body: Buffer.concat(content)
}, (err, data) => {
if (err) {
console.log(err);
}
})
});
DocumentModel.updateOne({ documentId: metadata.documentId }, {
$set: {
updatedOn: new Date().toISOString()
}
}).then(data => {
callback(null, stream);
})

})
};

_openReadStream(path, ctx, callback) {
this.getMetaData(path, (err, metadata) => {
if (err) {
callback(webdav.Errors.ResourceNotFound);
}
const { documentVersion } = this.getPathInformation(path);
const documentKey = metadata.key;
const etagValue = etag(new Date(metadata.updatedOn).toUTCString());

if (ctx.context.request.headers['if-none-match'] === etagValue) {
console.log('Returning 304 as file has not changed.');
ctx.context.response.statusCode = 304
ctx.context.response.end();
return;
}

this.getFileData(documentKey, documentVersion, (err, data) => {
if (err) {
callback(webdav.Errors.ResourceNotFound)
}

const content = data.content;
var stream = new webdav.VirtualFileReadable([content]);
var contentType = mime.lookup(metadata.extension);
ctx.context.response.setHeader('etag', etagValue);
ctx.context.response.setHeader('Content-type', contentType);

callback(undefined, stream)
})
});
};

_size(path, ctx, callback) {
const { documentVersion, documentId } = this.getPathInformation(path);
if (this.useCache && this.resources[documentId] && this.resources[documentId].size) {
callback(undefined, this.resources[documentId].size)
} else {
this.getMetaData(path, (err, metadata) => {
if (err) {
callback(webdav.Errors.ResourceNotFound);
}
const documentKey = metadata.key;
this.getFileData(documentKey, documentVersion, (err, data) => {
if (err) {
callback(webdav.Errors.ResourceNotFound)
}
const size = data.size;

if (!this.resources[documentId])
this.resources[documentId] = {};

this.resources[documentId].size = size;
callback(undefined, size);
})
})
}
};

_lockManager(path, ctx, callback) {
const { documentId } = this.getPathInformation(path);
this.getMetaData(path, (e) => {
if (e) {
return callback(webdav.Errors.ResourceNotFound);
}

if (!this.resources[documentId])
this.resources[documentId] = {};

if (!this.resources[documentId].locks)
this.resources[documentId].locks = new webdav.LocalLockManager();

callback(undefined, this.resources[documentId].locks);
})
};

_propertyManager(path, ctx, callback) {
this.getMetaData(path, (e) => {
if (e) {
return callback(webdav.Errors.ResourceNotFound);
}

if (!this.resources[documentId])
this.resources[documentId] = {};

if (!this.resources[documentId].props)
this.resources[documentId].props = new webdav.LocalPropertyManager();

callback(undefined, this.resources[documentId].props);
})
};

_creationDate(path, ctx, callback) {
this._lastModifiedDate(path, ctx, callback);

this.getMetaData(path, (e, data) => {
if (e)
return callback(webdav.Errors.ResourceNotFound);

callback(undefined, new Date(data.createdOn).toISOString());
})
};

_lastModifiedDate(path, ctx, callback) {
this.getMetaData(path, (e, data) => {
if (e)
return callback(webdav.Errors.ResourceNotFound);

callback(undefined, new Date(data.createdOn).toISOString());
})
};

_type(path, ctx, callback) {
const { documentId } = this.getPathInformation(path);
if (this.useCache && this.resources[documentId] && this.resources[documentId].type) {
callback(undefined, this.resources[documentId].type);
} else {
this.getMetaData(path, (e, data) => {
if (e)
callback(webdav.Errors.ResourceNotFound);

const type = webdav.ResourceType.File;

if (!this.resources[documentId])
this.resources[documentId] = {};

this.resources[documentId].type = type;
callback(undefined, type);
})
}
};

_mimeType(path, ctx, callback) {
this.getMetaData(path, (e, data) => {
if (e)
return callback(webdav.Errors.ResourceNotFound);

callback(null, mime.lookup(data.extension));
})
}
}

Now, hardest part is done. Now we have to connect this S3FileSystem class with our index.js file.

Initially, configure the Authentication settings for the WebDAV server. For the standard Server setup, we employ the HTTPNoAuthentication method, which utilizes a single designated user to access the server. Additionally, the server can be customized to integrate more sophisticated authentication mechanisms by referencing the Documentation on User Management.

To achieve this, create a function titled HTTPNoAuthentication, utilizing the SimpleUserManager class to generate a user. Include their realm in the WWW-Authenticate header as the basic realm. This method involves establishing a hardcoded user within the code, allowing access to the server using their credentials. Please note, this setup is designed for a basic server configuration. However, as you progress towards more secure implementations, consider alternative methods such as utilizing Azure AD users for further enhancements.

const userManager = new webdav.SimpleUserManager();
userManager.addUser("TestUser", "TestUser01", true);

var HTTPNoAuthentication = (function () {
function HTTPNoAuthentication(userManager, realm) {
if (realm === void 0) { realm = 'realm'; }
this.userManager = userManager;
this.realm = realm;
}
HTTPNoAuthentication.prototype.askForAuthentication = function () {
return {
'WWW-Authenticate': 'Basic realm="' + this.realm + '"'
};
};
HTTPNoAuthentication.prototype.getUser = function (ctx, callback) {
var _this = this;
_this.userManager.getDefaultUser(function (defaultUser) {
callback(null, defaultUser);
});
};
return HTTPNoAuthentication;
}());

Now, you need to create server object with WebDAVServer class and add httpAuthentication as the above created function.

const server = new webdav.WebDAVServer({
httpAuthentication: new HTTPNoAuthentication(userManager, 'Default realm')
});

Now set file system for WebDAV server with S3FileSystem class we already created. We have set ‘/webdav’ path for redirecting all of the webdav related request to that path. Because we need to implement few other endpoints as well for our frontend client implementation.

server.setFileSystem('/webdav', new S3FileSystem(), (success) => {
console.log('READY');
})

Then we have to set CORS (Cross Origin Resource Sharing) for the WebDAV server with allowing several headers on WebDAV response.

const setHeaders = (arg) => {
if (arg.request.method === "OPTIONS") {
arg.response.setHeader(
"Access-Control-Allow-Methods",
"PROPPATCH,PROPFIND,OPTIONS,DELETE,UNLOCK,COPY,LOCK,MKCOL,MOVE,HEAD,POST,PUT,GET"
);
arg.response.setHeader(
"allow",
"PROPPATCH,PROPFIND,OPTIONS,DELETE,UNLOCK,COPY,LOCK,MKCOL,MOVE,HEAD,POST,PUT,GET"
);
arg.response.setHeader("Access-Control-Allow-Headers", "*");
arg.response.setHeader("Access-Control-Allow-Origin", "*");
}
arg.response.setHeader("MS-Author-Via", "DAV");
}

server.beforeRequest((arg, next) => {
setHeaders(arg);
next();
});

Then start the server and allow server to listen for webdav requests.

app.use(webdav.extensions.express('/', server));
app.listen(1901);

Full implementation can be summed up like below.

const webdav = require('webdav-server').v2;
const express = require('express');
const AWS = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');
require('dotenv').config()
const S3FileSystem = require('./S3FileSystem');
const { DocumentModel } = require('./database/dbHelper');
const bucketName = process.env.BUCKET_NAME;

const app = express();

const userManager = new webdav.SimpleUserManager();
userManager.addUser("TestUser", "TestUser01", true);

var HTTPNoAuthentication = (function () {
function HTTPNoAuthentication(userManager, realm) {
if (realm === void 0) { realm = 'realm'; }
this.userManager = userManager;
this.realm = realm;
}
HTTPNoAuthentication.prototype.askForAuthentication = function () {
return {
'WWW-Authenticate': 'Basic realm="' + this.realm + '"'
};
};
HTTPNoAuthentication.prototype.getUser = function (ctx, callback) {
var _this = this;
_this.userManager.getDefaultUser(function (defaultUser) {
callback(null, defaultUser);
});
};
return HTTPNoAuthentication;
}());

const setHeaders = (arg) => {
if (arg.request.method === "OPTIONS") {
arg.response.setHeader(
"Access-Control-Allow-Methods",
"PROPPATCH,PROPFIND,OPTIONS,DELETE,UNLOCK,COPY,LOCK,MKCOL,MOVE,HEAD,POST,PUT,GET"
);
arg.response.setHeader(
"allow",
"PROPPATCH,PROPFIND,OPTIONS,DELETE,UNLOCK,COPY,LOCK,MKCOL,MOVE,HEAD,POST,PUT,GET"
);
arg.response.setHeader("Access-Control-Allow-Headers", "*");
arg.response.setHeader("Access-Control-Allow-Origin", "*");
}
arg.response.setHeader("MS-Author-Via", "DAV");
}

const server = new webdav.WebDAVServer({
httpAuthentication: new HTTPNoAuthentication(userManager, 'Default realm')
});

server.setFileSystem('/webdav', new S3FileSystem(), (success) => {
console.log('READY');
})

server.beforeRequest((arg, next) => {
setHeaders(arg);
next();
});

app.use(webdav.extensions.express('/', server));
app.listen(1901);

Now we have to add few other endpoints to cosume from the Frontend implementation.

First one to get all the documents in the server.

app.get("/getFiles", cors(), async (req, res) => {
try{
const files = await DocumentModel.find({});
res.status(200).json({ files })
}catch(e){
res.status(500).json({ error: e.message })
}
});

Next one to generate a Signed URL to upload a file and save document metadata on Server.

app.post("/getSignedUrl", cors(), async (req, res) => {
try {
const s3 = new AWS.S3({ region: 'us-east-1' });
const { filename, type } = req.body;
const extension = filename.split(".").pop();
const documentId = uuidv4();

const signedUrl = await s3.getSignedUrlPromise('putObject', {
Bucket: bucketName,
Key: `${documentId}.${extension}`,
ContentType: type,
Expires: 60,
});

const file = {
documentId: documentId,
title: filename.replace(`.${extension}`, "").trim(),
createdOn: new Date(),
updatedOn: new Date(),
extension: extension,
key: `${documentId}.${extension}`,
}

await DocumentModel.create(file);
res.status(200).json({ signedUrl })
} catch (e) {
res.status(500).json({ error: e.message })
}
})

The backend development phase has been completed. To initiate the backend functionality, execute node index.js. Upon successful execution, utilizing Postman, you can upload a document. Access the uploaded file via a Local Office client by navigating to a URL structured in the following format using a web browser. Let’s assume you are going to open a docx file on Microsoft Word.

ms-word:ofe|u|http://localhost:1901/webdav/<Document ID>/latest/document.docx

Going forward you can open any kind of file by changing the URL and part with ms-word. These phrases can be used for different Office clients.

doc, docx, odt, rtf - ms-word
csv, xls, xlsx, ods - ms-excel
ppt, pptx, odp, pps - ms-powerpoint

This will open the document on office and can be save the edits as well.

We’re ready to integrate this functionality into a compact web client. I’ve developed a React application specifically tailored to display a web interface, allowing for file listing, opening, and file uploads. However, I won’t delve into the specifics of this React application here. You’ll find comprehensive details within the GitHub repository, housing all the code pertinent to this project.

After all the things are done, you can see it like this.

You can find code for this project in below GitHub repository.

sandunisuru/webdav-s3 (github.com)

Cheers for the future!

--

--