Whenever we find ourselves trying to add any complex functionality to an app, the question arises, “should I roll my own?” And unless your goal is to build that functionality, the answer is almost always a straight “no”.

What you need is something to help you get to an MVP as quickly as possible, and the best way to achieve that is to use a complete out-of-the-box solution that can help you save time, which, in turn, translates into saving on development costs.

I’ll assume that you’re still here because the above resonates with you. So, now that we’re in sync, what I want to show you in this article is how easy it is to integrate Onlyoffice in your web app.

What is Onlyoffice?

From their website:

ONLYOFFICE offers the most feature-rich office suite available, highly compatible with Microsoft Office and OpenDocument file formats. View, edit and collaboratively work with documents, spreadsheets and presentations directly from your web application.

In this case, we are going to be using the Developer Edition, since it’s the best for our purpose, but if you’re looking to integrate with other services like SharePoint then you should check out the Integration Edition.

Developer Edition

The Developer Edition not only gives you enough freedom to integrate the editors within your app, but it also comes with a “White Label” option which lets you fully customize the editors to use them under your own brand.

Document Server Integration

To integrate with your web app, you first need to download the Document Server installation and set it up on your local server.

Another option is to install it with Docker using the following command:

docker run -i -t -d -p 8080:80 onlyoffice/documentserver

After you’ve installed it you can start implementing the requests to handle documents on your server. Onlyoffice provides some very nice examples for .NET, Java, Node.js, PHP, Python and Ruby.

You can download the Document Server and your preferred example and try it straight away on your machine.

I’ll demonstrate how you can go about starting to integrate into your app. For this purpose, we’ll use a very simple example with Node.js and Express. I won’t go into much detail on the implementation, I’ll lay out the bare bone essentials and let you fill in the blanks to build a robust and scalable system.

I have an app with the following structure:

- node_modules
- public
    - backups
    - css
        - main.css
    - documents
        - sample.docx
    - javascript
        - main.js
    - samples
        - new.docx
        - new.xlsx
        - new.pptx
- app.js
- index.html
- package.json

We’ll use the public/documents folder to store the documents. The app.js file is where our Express app code is, and index.html is where we’ll show our documents. I’ve dropped a sample.docx file in the documents folder for testing purposes.

The tree files inside public/samples/ are the blank files that we’ll copy when “creating” new files.

The backups folder, as you’ll see later, will not only help us keep backups of previous versions but also assist us in generating the unique identifier for our documents after modifying them.

The public/css/main.css and public/javascript/main.js files will be used by the index.html. We’ll look into that later.

Let’s take a look at the app.js file:

const express = require('express');
const bodyParser = require("body-parser");
const path = require('path');
const fs = require('fs');
const syncRequest = require('sync-request');

const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded( extended: false ));

app.use(express.static("public"));

app.get("/", (req, res) => 
  res.sendFile(path.join(__dirname, "/index.html"));
);

const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`App listening on http://localhost:$port`));

What we’re doing is serving the files as localhost:3000/documents/filename.

I’ve also gotten ahead of myself and added syncRequest, fs, and bodyParser. These are not relevant right now but we’ll use them later.

Fetch Documents

To show the available documents we’ll need to get a list of all the filenames and send them to the client. We’ll create the /documents route for this:

app.get("/documents", (req, res) => 
  const docsPath = path.join(__dirname, "public/documents");
  const docsPaths = fs.readdirSync(docsPath);

  const fileNames = [];

  docsPaths.forEach(filePath => 
    const fileName = path.basename(filePath);
    fileNames.push(fileName);
  );

  res.send(fileNames);
);

Create Documents

At the beginning we’ll just have a sample document, but that’s no fun at all. Let’s add a /create route to assist us with adding some files. We’ll simply take a fileName and copy the corresponding template into the public/documents folder with its new name:

app.post("/create", async (req, res) => 
  const ext = path.extname(req.query.fileName);
  const fileName = req.query.fileName;

  const samplePath = path.join(__dirname, "public/samples", "new" + ext);
  const newFilePath = path.join(__dirname, "public/documents", fileName);

  // Copy the sample file to the documents folder with its new name.
  try 
    fs.copyFileSync(samplePath, newFilePath);
    res.sendStatus(200);
   catch (e) 
    res.sendStatus(400);
  
);

Delete Documents

We also need a way to delete documents. Let’s create a the /delete route:

app.delete("/delete", (req, res) => 
  const fileName = req.query.fileName;
  const filePath = path.join(__dirname, "public/documents", fileName);

  try 
    fs.unlinkSync(filePath);
    res.sendStatus(200);
   catch (e) 
    res.sendStatus(400);
  
);

This one’s super simple. We’ll delete the file and send a 200 status code to let the user know it all went fine. Otherwise, they’ll get a 400 status code.

Save Documents

So far, we can open our documents for editing, but we have no way of saving our changes. Let’s do that now. We’ll add a /track route to save our files:

app.post("/track", async (req, res) => 
  const fileName = req.query.fileName;

  const backupFile = filePath => 
    const time = new Date().getTime();
    const ext = path.extname(filePath);
    const backupFolder = path.join(__dirname, "public/backups", fileName + "-history");

    // Create the backups folder if it doesn't exist
    !fs.existsSync(backupFolder) && fs.mkdirSync(backupFolder);

    // Remove previous backup if any
    const previousBackup = fs.readdirSync(backupFolder)[0];
    previousBackup && fs.unlinkSync(path.join(backupFolder, previousBackup));

    const backupPath = path.join(backupFolder, time + ext);

    fs.copyFileSync(filePath, backupPath);
  

  const updateFile = async (response, body, path) => 
    if (body.status == 2) 
      backupFile(path);
      const file = syncRequest("GET", body.url);
      fs.writeFileSync(path, file.getBody());
    

    response.write(""error":0");
    response.end();
  

  const readbody = (request, response, path) => 
    const content = "";
    request.on("data", function (data) 
      content += data;
    );
    request.on("end", function () 
      const body = JSON.parse(content);
      updateFile(response, body, path);
    );
  

  if (req.body.hasOwnProperty("status")) 
    const filePath = path.join(__dirname, "public/documents", fileName);
    updateFile(res, req.body, filePath);
   else 
    readbody(req, res, filePath);
  
);

This is a tricky one, since it’s going to be used by the Document Server when the file is saved by the editor. As you can see, we’re returning ""error":0", which tells the server that it’s all good.

When the editor is closed, the current version of the file will be backed up in public/backups/fileName-history/ with the current time in milliseconds as the file’s name. We’ll use the file’s name later in the front end, as you’ll see.

In this example, we’re replacing the previous backup every time we save a new one. How would you go about keeping more backups?

Fetching backups

We’ll need a way to get the backups for a particular file, so we’re adding a /backups route to handle this:

app.get("/backups", (req, res) => 
  const fileName = req.query.fileName;
  const backupsPath = path.join(__dirname, "public/backups", fileName + "-history");

  if (!fs.existsSync(backupsPath)) 
    return res.send([]);
  

  const backupsPaths = fs.readdirSync(backupsPath);

  const fileNames = [];

  backupsPaths.forEach(filePath => 
    const fileName = path.basename(filePath);
    fileNames.push(fileName);
  );

  res.send(fileNames);
);

Here we’re making sure that the backup folder for that file exists, and returning an array of all the backup files in that folder. Yes, this will help you in your task of keeping more backups for a single file. I can’t keep doing all the work for you!

Continue reading
Add Office Functionality to Your Web App with OnlyOffice
on SitePoint.