受欢迎的博客标签

ASP.NET Core 2.0集成Office Online Server(OWAS)实现办公文档的在线预览与编辑(支持word\excel\ppt\pdf等格式)

Published

Office Online Server是微软开发的一套基于Office实现在线文档预览编辑的技术框架(支持当前主流的浏览器,且浏览器上无需安装任何插件,支持word、excel、ppt、pdf等文档格式),其客户端通过WebApi方式可集成到自已的应用中,支持Java、C#等语言。Office Online Server原名为:Office Web Apps Server(简称OWAS)。因为近期有ASP.NET Core 2.0的项目中要实现在线文档预览与编辑,就想着将Office Online Server集成到项目中来,通过网上查找,发现大部分的客户端的实现都是基于ASP.NET的,而我在实现到ASP.NET Core 2.0的过程中也遇到了不少的问题,所以就有了今天这篇文章。

 

安装Office Online Server

微软的东西在安装上都是很简单的,下载安装包一路”下一步“就可完成。也可参考如下说明来进行安装:https://docs.microsoft.com/zh-cn/officeonlineserver/deploy-office-online-server

完成安装后会在服务器上的IIS上自动创建两个网站,分别为:HTTP80、HTTP809。其中HTTP80站绑定80、443端口,HTTP809站绑定809、810端口。

 

业务关系

1、Office Online Server服务端(WOPI Server),安装在服务器上用于受理来自客户端的预览、编辑请求等。服务端很吃内存的,单机一定不能低于8G内存。

2、Office Online Server客户端(WOPI Client),这里因为集成在了自已的项目中,所以Office Online Server客户端也就是自已的项目中的子系统。

用户通过项目中的业务系统请求客户端并发起对某一文档的预览或编辑请求,客户端接受请求后再通过调用服务端的WebApi完成一系列约定通讯后,服务端在线输出文档并完成预览与编辑功能。

 

实现原理

可通过如下图(图片来自互联网)能清晰的看出浏览器、Office Online Server服务端、Office Online Server客户端之间的交互顺序与关系。在这过程中,Office Online Server客户端需自行生成Token及身份验证,这也是为保障Office Online Server客户端的安全手段。

 

实现代码

客户端编写拦截器,拦截器中主要接受来自服务端的请求,并根据服务端的请求类型做出相应动作,请求类型包含如下几种:CheckFileInfo、GetFile、Lock、GetLock、RefreshLock、Unlock、UnlockAndRelock、PutFile、PutRelativeFile、RenameFile、DeleteFile、PutUserInfo等。具体代码如下:

 
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
 //编写一个处理WOPI请求的客户端拦截器
namespace Lezhima.Wopi.Base
{
    public class ContentProvider
    {
        //声明请求代理
        private readonly RequestDelegate _nextDelegate;


        public ContentProvider(RequestDelegate nextDelegate)
        {
            _nextDelegate = nextDelegate;
        }


        //拉截并接受所有请求
        public async Task Invoke(HttpContext context)
        {
		//判断是否为来自WOPI服务端的请求
            if (context.Request.Path.ToString().ToLower().IndexOf("files") >= 0)
            {
                WopiRequest requestData = ParseRequest(context.Request);

                switch (requestData.Type)
                {
			//获取文件信息
                    case RequestType.CheckFileInfo:
                        await HandleCheckFileInfoRequest(context, requestData);
                        break;

                    //尝试解锁并重新锁定
                    case RequestType.UnlockAndRelock:
                        HandleUnlockAndRelockRequest(context, requestData);
                        break;

                    //获取文件
                    case RequestType.GetFile:
                        await HandleGetFileRequest(context, requestData);
                        break;

                    //写入文件
                    case RequestType.PutFile:
                        await HandlePutFileRequest(context, requestData);
                        break;

                    default:
                        ReturnServerError(context.Response);
                        break;
                }
            }
            else
            {
                await _nextDelegate.Invoke(context);
            }
        }




        /// <summary>
        /// 接受并处理获取文件信息的请求
        /// </summary>
        /// <remarks>
        /// </remarks>
        private async Task HandleCheckFileInfoRequest(HttpContext context, WopiRequest requestData)
        {
		//判断是否有合法token
            if (!ValidateAccess(requestData, writeAccessRequired: false))
            {
                ReturnInvalidToken(context.Response);
                return;
            }
            //获取文件
            IFileStorage storage = FileStorageFactory.CreateFileStorage();
            DateTime? lastModifiedTime = DateTime.Now;
            try
            {
                CheckFileInfoResponse responseData = new CheckFileInfoResponse()
                {
			//获取文件名称
                    BaseFileName = Path.GetFileName(requestData.Id),
                    Size = Convert.ToInt32(size),
                    Version = Convert.ToDateTime((DateTime)lastModifiedTime).ToFileTimeUtc().ToString(),
                    SupportsLocks = true,
                    SupportsUpdate = true,
                    UserCanNotWriteRelative = true,

                    ReadOnly = false,
                    UserCanWrite = true
                };

                var jsonString = JsonConvert.SerializeObject(responseData);

                ReturnSuccess(context.Response);

                await context.Response.WriteAsync(jsonString);

            }
            catch (UnauthorizedAccessException ex)
            {
                ReturnFileUnknown(context.Response);
            }
        }

        /// <summary>
        /// 接受并处理获取文件的请求
        /// </summary>
        /// <remarks>
        /// </remarks>
        private async Task HandleGetFileRequest(HttpContext context, WopiRequest requestData)
        {
     	//判断是否有合法token
            if (!ValidateAccess(requestData, writeAccessRequired: false))
            {
                ReturnInvalidToken(context.Response);
                return;
            }


            //获取文件
            var stream = await storage.GetFile(requestData.FileId);

            if (null == stream)
            {
                ReturnFileUnknown(context.Response);
                return;
            }

            try
            {
                int i = 0;
                List<byte> bytes = new List<byte>();
                do
                {
                    byte[] buffer = new byte[1024];
                    i = stream.Read(buffer, 0, 1024);
                    if (i > 0)
                    {
                        byte[] data = new byte[i];
                        Array.Copy(buffer, data, i);
                        bytes.AddRange(data);
                    }
                }
                while (i > 0);


                ReturnSuccess(context.Response);
		    await context.Response.Body.WriteAsync(bytes, bytes.Count);

            }
            catch (UnauthorizedAccessException)
            {
                ReturnFileUnknown(context.Response);
            }
            catch (FileNotFoundException ex)
            {
                ReturnFileUnknown(context.Response);
            }

        }

        /// <summary>
        /// 接受并处理写入文件的请求
        /// </summary>
        /// <remarks>
        /// </remarks>
        private async Task HandlePutFileRequest(HttpContext context, WopiRequest requestData)
        {
		//判断是否有合法token
            if (!ValidateAccess(requestData, writeAccessRequired: true))
            {
                ReturnInvalidToken(context.Response);
                return;
            }

            try
            {
                //写入文件
                int result = await storage.UploadFile(requestData.FileId, context.Request.Body);
                if (result != 0)
                {
                    ReturnServerError(context.Response);
                    return;
                }

                ReturnSuccess(context.Response);
            }
            catch (UnauthorizedAccessException)
            {
                ReturnFileUnknown(context.Response);
            }
            catch (IOException ex)
            {
                ReturnServerError(context.Response);
            }
        }



        private static void ReturnServerError(HttpResponse response)
        {
            ReturnStatus(response, 500, "Server Error");
        }

    }
}
 

 

拦截器有了后,再到Startup.cs文件中注入即可,具体代码如下:

 

 
  1    

 

至止,整个基于Office Online Server技术框架在ASP.NET Core上的文档预览/编辑功能就完成了。够简单的吧!!

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();
            app.UseAuthentication();

	        //注入中间件拦截器,这是将咱们写的那个Wopi客户端拦截器注入进来
            app.UseMiddleware<ContentProvider>();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{name?}");
            });
        }

总结

1、Office Online Server服务端建议在服务器上独立部署,不要与其它业务系统混合部署。因为这货实在是太能吃内存了,其内部用了WebCached缓存机制是导致内存增高的一个因素。

2、Office Online Server很多资料上要求要用AD域,但我实际在集成客户端时没有涉及到这块,也就是说服务端是开放的,但客户端是通过自行颁发的Token与验证来保障安全的。

3、利用编写中间件拦截器,并在Startup.cs文件中注入中间件的方式来截获来自WOPI服务端的所有请求,并对不同的请求类型做出相应的处理。