Office Online Server是微軟開發的一套基於Office實現線上文檔預覽編輯的技術框架(支持當前主流的瀏覽器,且瀏覽器上無需安裝任何插件,支持word、excel、ppt、pdf等文檔格式),其客戶端通過WebApi方式可集成到自已的應用中,支持Java、C#等語言。Office O... ...
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等。具體代碼如下:
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 = true 100 }; 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 try 140 { 141 int i = 0; 142 List<byte> bytes = new List<byte>(); 143 do 144 { 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 try 187 { 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服務端的所有請求,並對不同的請求類型做出相應的處理。