受欢迎的博客标签

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是微软开发的一套基于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客户端的安全手段。19092925-56e50ede7a59467d8ba8d9047f5dfcb9 实现代码客户端编写拦截器,拦截器中主要接受来自服务端的请求,并根据服务端的请求类型做出相应动作,请求类型包含如下几种:CheckFileInfo、GetFile、Lock、GetLock、RefreshLock、Unlock、UnlockAndRelock、PutFile、PutRelativeFile、RenameFile、DeleteFile、PutUserInfo等。具体代码如下:复制代码  1 using Microsoft.AspNetCore.Http;  2 using Newtonsoft.Json;  3 using System;  4 using System.Collections.Generic;  5 using System.IO;  6 using System.Linq;  7 using System.Text;  8 using System.Threading;  9 using System.Threading.Tasks; 10 using System.Web; 11 //编写一个处理WOPI请求的客户端拦截器 12 namespace Lezhima.Wopi.Base 13 { 14     public class ContentProvider   15     { 16         //声明请求代理 17         private readonly RequestDelegate _nextDelegate; 18  19  20         public ContentProvider(RequestDelegate nextDelegate) 21         { 22             _nextDelegate = nextDelegate; 23         } 24  25  26         //拉截并接受所有请求 27         public async Task Invoke(HttpContext context) 28         { 29 //判断是否为来自WOPI服务端的请求 30             if (context.Request.Path.ToString().ToLower().IndexOf("files") >= 0) 31             { 32                 WopiRequest requestData = ParseRequest(context.Request); 33  34                 switch (requestData.Type) 35                 { 36 //获取文件信息 37                     case RequestType.CheckFileInfo: 38                         await HandleCheckFileInfoRequest(context, requestData); 39                         break; 40  41                     //尝试解锁并重新锁定 42                     case RequestType.UnlockAndRelock: 43                         HandleUnlockAndRelockRequest(context, requestData); 44                         break; 45  46                     //获取文件 47                     case RequestType.GetFile: 48                         await HandleGetFileRequest(context, requestData); 49                         break; 50  51                     //写入文件 52                     case RequestType.PutFile: 53                         await HandlePutFileRequest(context, requestData); 54                         break; 55  56                     default: 57                         ReturnServerError(context.Response); 58                         break; 59                 } 60             } 61             else 62             { 63                 await _nextDelegate.Invoke(context); 64             } 65         } 66  67  68  69  70         /// <summary> 71         /// 接受并处理获取文件信息的请求 72         /// </summary> 73         /// <remarks> 74         /// </remarks> 75         private async Task HandleCheckFileInfoRequest(HttpContext context, WopiRequest requestData) 76         { 77 //判断是否有合法token     78             if (!ValidateAccess(requestData, writeAccessRequired: false)) 79             { 80                 ReturnInvalidToken(context.Response); 81                 return; 82             } 83             //获取文件            84             IFileStorage storage = FileStorageFactory.CreateFileStorage(); 85             DateTime? lastModifiedTime = DateTime.Now; 86             try 87             { 88                 CheckFileInfoResponse responseData = new CheckFileInfoResponse() 89                 { 90 //获取文件名称 91                     BaseFileName = Path.GetFileName(requestData.Id), 92                     Size = Convert.ToInt32(size), 93                     Version = Convert.ToDateTime((DateTime)lastModifiedTime).ToFileTimeUtc().ToString(), 94                     SupportsLocks = true, 95                     SupportsUpdate = true, 96                     UserCanNotWriteRelative = true, 97  98                     ReadOnly = false, 99                     UserCanWrite = true100                 };101 102                 var jsonString = JsonConvert.SerializeObject(responseData);103 104                 ReturnSuccess(context.Response);105 106                 await context.Response.WriteAsync(jsonString);107 108             }109             catch (UnauthorizedAccessException ex)110             {111                 ReturnFileUnknown(context.Response);112             }113         }114 115         /// <summary>116         /// 接受并处理获取文件的请求117         /// </summary>118         /// <remarks>119         /// </remarks>120         private async Task HandleGetFileRequest(HttpContext context, WopiRequest requestData)121         {122      //判断是否有合法token    123             if (!ValidateAccess(requestData, writeAccessRequired: false))124             {125                 ReturnInvalidToken(context.Response);126                 return;127             }128 129 130             //获取文件             131             var stream = await storage.GetFile(requestData.FileId);132 133             if (null == stream)134             {135                 ReturnFileUnknown(context.Response);136                 return;137             }138 139             try140             {141                 int i = 0;142                 List<byte> bytes = new List<byte>();143                 do144                 {145                     byte[] buffer = new byte[1024];146                     i = stream.Read(buffer, 0, 1024);147                     if (i > 0)148                     {149                         byte[] data = new byte[i];150                         Array.Copy(buffer, data, i);151                         bytes.AddRange(data);152                     }153                 }154                 while (i > 0);155 156 157                 ReturnSuccess(context.Response);158     await context.Response.Body.WriteAsync(bytes, bytes.Count);159 160             }161             catch (UnauthorizedAccessException)162             {163                 ReturnFileUnknown(context.Response);164             }165             catch (FileNotFoundException ex)166             {167                 ReturnFileUnknown(context.Response);168             }169 170         }171 172         /// <summary>173         /// 接受并处理写入文件的请求174         /// </summary>175         /// <remarks>176         /// </remarks>177         private async Task HandlePutFileRequest(HttpContext context, WopiRequest requestData)178         {179 //判断是否有合法token    180             if (!ValidateAccess(requestData, writeAccessRequired: true))181             {182                 ReturnInvalidToken(context.Response);183                 return;184             }185 186             try187             {188                 //写入文件 189                 int result = await storage.UploadFile(requestData.FileId, context.Request.Body);190                 if (result != 0)191                 {192                     ReturnServerError(context.Response);193                     return;194                 }195 196                 ReturnSuccess(context.Response);197             }198             catch (UnauthorizedAccessException)199             {200                 ReturnFileUnknown(context.Response);201             }202             catch (IOException ex)203             {204                 ReturnServerError(context.Response);205             }206         }207 208 209 210         private static void ReturnServerError(HttpResponse response)211         {212             ReturnStatus(response, 500, "Server Error");213         }214 215     }216 }复制代码 拦截器有了后,再到Startup.cs文件中注入即可,具体代码如下:复制代码  1         public void Configure(IApplicationBuilder app, IHostingEnvironment env)  2         {  3             if (env.IsDevelopment())  4             {  5                 app.UseDeveloperExceptionPage();  6                 app.UseBrowserLink();  7             }  8             else  9             { 10                 app.UseExceptionHandler("/Home/Error"); 11             } 12  13             app.UseStaticFiles(); 14             app.UseAuthentication(); 15  16         //注入中间件拦截器,这是将咱们写的那个Wopi客户端拦截器注入进来 17             app.UseMiddleware<ContentProvider>(); 18  19             app.UseMvc(routes => 20             { 21                 routes.MapRoute( 22                     name: "default", 23                     template: "{controller=Home}/{action=Index}/{name?}"); 24             }); 25         }复制代码 至止,整个基于Office Online Server技术框架在ASP.NET Core上的文档预览/编辑功能就完成了。够简单的吧!! 总结1、Office Online Server服务端建议在服务器上独立部署,不要与其它业务系统混合部署。因为这货实在是太能吃内存了,其内部用了WebCached缓存机制是导致内存增高的一个因素。2、Office Online Server很多资料上要求要用AD域,但我实际在集成客户端时没有涉及到这块,也就是说服务端是开放的,但客户端是通过自行颁发的Token与验证来保障安全的。3、利用编写中间件拦截器,并在Startup.cs文件中注入中间件的方式来截获来自WOPI服务端的所有请求,并对不同的请求类型做出相应的处理。.