惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
Engineering at Meta
Engineering at Meta
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Martin Fowler
Martin Fowler
雷峰网
雷峰网
Recent Announcements
Recent Announcements
博客园 - 叶小钗
F
Full Disclosure
C
Check Point Blog
A
About on SuperTechFans
L
LangChain Blog
Vercel News
Vercel News
T
The Blog of Author Tim Ferriss
博客园 - 司徒正美
C
Cybersecurity and Infrastructure Security Agency CISA
C
CXSECURITY Database RSS Feed - CXSecurity.com
Cisco Talos Blog
Cisco Talos Blog
Hugging Face - Blog
Hugging Face - Blog
Recorded Future
Recorded Future
MongoDB | Blog
MongoDB | Blog
Project Zero
Project Zero
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Know Your Adversary
Know Your Adversary
D
Docker
U
Unit 42
酷 壳 – CoolShell
酷 壳 – CoolShell
T
The Exploit Database - CXSecurity.com
L
Lohrmann on Cybersecurity
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Latest news
Latest news
V
Visual Studio Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LINUX DO - 最新话题
Google DeepMind News
Google DeepMind News
小众软件
小众软件
NISL@THU
NISL@THU
GbyAI
GbyAI
H
Heimdal Security Blog
Jina AI
Jina AI
I
InfoQ
PCI Perspectives
PCI Perspectives
P
Privacy International News Feed
P
Proofpoint News Feed
N
News and Events Feed by Topic
C
CERT Recently Published Vulnerability Notes
aimingoo的专栏
aimingoo的专栏
SecWiki News
SecWiki News
B
Blog RSS Feed
阮一峰的网络日志
阮一峰的网络日志

博客园 - kagar

挖掘经典:几乎被人遗忘的HTML七种用法 【转】P2P穿透NAT原理 关于delphi调用.net com的详细过程 3desj加密 C++ ,C#类型对照 DLL+ ActiveX控件+WEB页面调用例子 测试 表空间 Sql Server2005不同的表使用不同的表空间 如何在Web页面上直接打开、编辑、创建Office文档 Foursquare引爆了什么 承博士:让云计算落地生根的中国云计算平台 PHP - Smarty 工作流系统功能列表系列 跨域情况下Iframe高度自适应解决方案 - kagar - 博客园 五种丑陋的项目管理 Chart 报表 jQuery 数据仓库与事务型数据库的区别 RUP与XP的平衡之道
Office文档在线编辑的实现之二
kagar · 2010-05-19 · via 博客园 - kagar

Office文档在线编辑的实现之二

上篇文章 http://www.cnblogs.com/jianyi0115/archive/2007/03/16/677712.html
讲述了如何通过iis的webdav支持实现客户端的office直接编辑服务器上的文件,
本篇将讲解如何实现客户端的office直接编辑数据库中的二进制形式保存的office文件。

实现的关键:模拟IIS,自己实现一个webdav的服务器端。

首先,我们简单了解一下webdav:
webdav,中文可以翻译为网络分布式协作协议,它解决了http协议中一个问题:http无法实现版本和单访问控制。
什么是单访问控制呢?假设我们有一个页面编辑某条数据,这个页面可以同时被多个用户使用,那么最终的数据是最后一个用户提交的数据,
而其他用户是不知道的.我们的99%的web程序都存在此问题,当然通过编码可以解决,但http协议本身并没有提供对这种情形的支持。

webdav协议在标准的http协议的基础上,扩展了以下请求动作(verb):
PUT:用于客户端推送二进制文件。(好像http有这个verb)
LOCK:用户锁定一个资源,保证资源的单访问
UNLOCK:解锁一个资源
OPTIONS:获取服务器可以支持的请求类型
DELETE:删除服务器文件
PROPFIND:查询文件属性
其他动作: OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH
要详细地了解webdav,大家可以google一下,或访问http://en.wikipedia.org/wiki/WebDAV

笔者在实现这个解决方案的时候,是采用fiddler,debug IE的http请求,才搞懂了IIS本身的实现机制,为了形象化,可以看一下webdav请求相应的
数据:
发起一个OPTIONS请求
OPTIONS /PMDemo/Test/待办事务.doc HTTP/1.1
User-Agent: Fiddler
Host: localhost

响应如下:
HTTP/1.1 200 OK
Date: Wed, 27 Dec 2006 11:34:03 GMT
Server: Microsoft-IIS/6.0
MicrosoftOfficeWebServer: 5.0_Pub
X-Powered-By: ASP.NET
MS-Author-Via: DAV
Content-Length: 0
Accept-Ranges: bytes
DASL: <DAV:sql>
DAV: 1, 2
Public: OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH
Allow: OPTIONS, TRACE, GET, HEAD, DELETE, PUT, COPY, MOVE, PROPFIND, PROPPATCH, SEARCH, LOCK, UNLOCK
Cache-Control: private搞清楚了这些,下面我们的任务就是如何在asp.net中实现一个wevdav服务器.
显然,这要求我们需要在底层截获http请求,幸运的是asp.net中支持这种技术:HttpHandler.它可以让我们自己的代码来处理http请求.

首先,我们在web.config中做如下配置:

    <httpHandlers>
            
<remove verb="*" path="*"/>            
            
<add verb="GET,PUT,UNLOCK,LOCK,OPTIONS" path="*.doc,*.xml" type="Webdav.WebdavProtocolHandler,Webdav"/>
    
</httpHandlers>

通过这个配置,使我们的WebdavProtocolHandler可以来处理webdav请求.WebdavProtocolHandler类是一个标准的httphandler,实现了IHttpHandler接口,它按照客户端的请求类型,返回符合wevdav协议的数据.WebdavProtocolHandler类需要按照不同的webdav请求动作,做不同的处理,那么怎么来实现这个类呢?
这里就要用到一个设计模式:命令模式.

首先定义一个接口:

public interface IVerbHandler
{
      
void Process( HttpContext context );
}

实现对Options请求的处理:

class OptionsHandler : IVerbHandler
    {
        
#region IVerbHandler 成员public void Process(System.Web.HttpContext context)
        {
            context.Response.AppendHeader(
"DASL""<DAV:sql>");
            context.Response.AppendHeader(
"DAV""1, 2");

            context.Response.AppendHeader(

"Public""OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH");

            context.Response.AppendHeader(

"Allow""OPTIONS, TRACE, GET, HEAD, DELETE, PUT, COPY, MOVE, PROPFIND, PROPPATCH, SEARCH, LOCK, UNLOCK");
        }
#endregion
    }

webdav的请求verb多达15个以上,大多数情况下,我们并不需要一个完整的webdav支持,故我们只要对其中的几个进行实现即可。

实现对LOCK的支持:

 class LockHandler : IVerbHandler
    {
        
#region IVerbHandler 成员public void Process(System.Web.HttpContext context)
        {
            context.Response.ContentType 
= "text/xml";string token = Guid.NewGuid().ToString() + ":" + DateTime.Now.Ticks.ToString() ; 

            context.Response.AppendHeader(

"Lock-Token""<opaquelocktoken:" + token + ">");
          
            
string xml = @"<?xml version=""1.0""?>
<a:prop xmlns:a=""DAV:""><a:lockdiscovery>
<a:activelock><a:locktype><a:write/></a:locktype>
<a:lockscope><a:exclusive/></a:lockscope><owner xmlns=""DAV:"">Administrator</owner><a:locktoken>
<a:href>opaquelocktoken:{0}</a:href></a:locktoken>
<a:depth>0</a:depth><a:timeout>Second-180</a:timeout></a:activelock></a:lockdiscovery>
</a:prop>
";

            context.Response.Write( String.Format( xml , token ) );
            context.Response.End();
        }

#endregion
    }


注意这篇文章的主题:实现在线编辑。并没有版本控制等其他内容,大家仔细看以上的代码,服务器并没有真正实现"锁定",只是假装告诉客户端,你要的资源已经给你锁定了,你可以放心的编辑了。当然,有兴趣的朋友可以实现真正的锁定,无非可以通过给数据做一个状态字段来实现。但注意,要考虑一些复杂的情况,如自动解锁(用户打开一个文档,然后关机了,文档岂不永远锁定了?)等等。

接着,我们实现UNLOCK,同样是假的:

class UnLockHandler : IVerbHandler
    {
        
#region IVerbHandler 成员public void Process(System.Web.HttpContext context)
        {            
        }
#endregion
    }

下面,我们将实现两个最重要的请求动作的处理:Get和Put, office请求打开一个服务器上的文件时,采用get请求,office保存一个文件到服务器上时,发送put请求。

首先,我们要考虑一种数据项标识的传递策略,即:客户端发起访问数据库的office文件行,那么如何确认数据行的主键?
有两种策略:
1)通过不同的文件名 , 如,请求http://localhost/weboffice/1.doc  这个请求主键 为1的文件。
2)通过文件路径, 如,请求http://localhost/weboffice/1/文件名.doc  这个请求主键为1的文件。
我们将采用策略2。

再返回到我们对web.config做的配置:

<add verb="GET,PUT,UNLOCK,LOCK,OPTIONS" path="*.doc,*.xml" type="Webdav.WebdavProtocolHandler,Webdav"/>

这个配置允许

WebdavProtocolHandler处理所有对doc和xml的请求处理,为什么要允许xml呢,因为office2003之后,支持xml格式,可以直接在
数据库重以xml的格式存放office文件。

接着,我们要确认我们的数据存储结构,即,office文件在数据库中时如何存放的。

我们有一个附件表:Document

CREATE TABLE [dbo].[Document] (
    
[DocumentId] [int] IDENTITY (11NOT NULL ,
    
[Name] [varchar] (50) COLLATE Chinese_PRC_CI_AS NULL ,
    
[Description] [text] COLLATE Chinese_PRC_CI_AS NULL ,
    
[CreateTime] [datetime] NULL ,
    
[Size] [int] NULL ,
    
[CreatorId] [varchar] (50) COLLATE Chinese_PRC_CI_AS NULL ,
    
[CreatorName] [char] (10) COLLATE Chinese_PRC_CI_AS NULL ,
    
[CreateYear] [int] NULL ,
    
[ContentType] [varchar] (50) COLLATE Chinese_PRC_CI_AS NULL ,
    
[DeptId] [varchar] (50) COLLATE Chinese_PRC_CI_AS NULL ,
    
[DeptName] [varchar] (50) COLLATE Chinese_PRC_CI_AS NULL ,
    
[Content] [image] NULL ,
    
[ModifyTime] [datetime] NULL ,
    
[OwnerType] [varchar] (50) COLLATE Chinese_PRC_CI_AS NULL ,
    
[TemplateAble] [bit] NULL 
ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

设计一个文裆实体:

    [Serializable]
   
public class Document
   {
       
public Document()
       { }
static public Document FromPostFile(System.Web.HttpPostedFile file , User user )
       {
           Document doc 
= new Document(file);
           doc.CreateTime 
= DateTime.Now;
           doc.CreatorId 
= user.Id;
           doc.CreatorName 
= user.Name; 
           doc.DeptId 
= user.OrgId;
           doc.DeptName 
= user.OrgName;
           
return doc;
       }
public Document(System.Web.HttpPostedFile file)
        {
            
string[] strs = file.FileName.Split( '\\' );
            
this.Name = strs[strs.Length - 1];
            Size 
= file.ContentLength;
            
//读取文件的数据
            this.Content = new byte[Size];
            Stream fileDataStream 
= file.InputStream;
            fileDataStream.Read( 
this.Content , 0, Size );
            ContentType 
= file.ContentType;
        }
private int _DocumentId;
      
/// <summary>
      
///   任务名
      
/// </summary>
      private string _Name;
      
/// <summary>
      
///   任务描述
      
/// </summary>
      private string _Description;
      
/// <summary>
      
///   报表创建时间
      
/// </summary>
      private DateTime _CreateTime = DateTime.Now ;
      
private int _Size = 0 ;
       
private byte[] _Data;
      
/// <summary>
      
///   创建人Id
      
/// </summary>
      private string _CreatorId;
      
/// <summary>
      
///   创建人名
      
/// </summary>
      private string _CreatorName;private int _CreateYear;
      
private string _ContentType;
      
/// <summary>
      
///   部门ID(便于统计)
      
/// </summary>
      private string _DeptId;
      
/// <summary>
      
///   部门名
      
/// </summary>
      private string _DeptName;   
      
// Property DocumentId
      
public int DocumentId   
      {
         
get
         {
            
return _DocumentId;
         }
         
set
         {

           
this._DocumentId = value;
         }
      }      
      
// Property Name
      public string Name   
      {
         
get
         {
            
return _Name;
         }
         
set
         {

            
this._Name = value;
         }
      }      
      
// Property Description
      public string Description   
      {
         
get
         {
            
return _Description;
         }
         
set
         {

            
this._Description = value;
         }
      }      
      
// Property CreateTime
      public DateTime CreateTime   
      {
         
get
         {
            
return _CreateTime;
         }
         
set
         {

           
this._CreateTime = value;
         }
      }
       
private DateTime _ModifyTime = DateTime.Now;
       
public DateTime ModifyTime
       {
           
get
           {
               
return _ModifyTime;
           }
           
set
           {

              
this._ModifyTime = value;
           }
       }      
      
// Property Size
      public int Size   
      {
         
get
         {
            
return _Size;
         }
         
set
         {

           
this._Size = value;
         }
      }      
      
// Property Data
      public byte[] Content   
      {
         
get
         {
            
return _Data;
         }
         
set
         {

           
this._Data = value;
         }
      }      
      
// Property CreatorId
      public string CreatorId   
      {
         
get
         {
            
return _CreatorId;
         }
         
set
         {

           
this._CreatorId = value;
         }
      }
// Property CreatorName
      public string CreatorName
      {
          
get
          {
              
return _CreatorName;
          }
          
set
          {

             
this._CreatorName = value;
          }
      }      
      
// Property CreateYear
      public int CreateYear   
      {
         
get
         {
            
return _CreateYear;
         }
         
set
         {

           
this._CreateYear = value;
         }
      }      
      
// Property ContentType
      
//application/msword
      
//text/plain
      public string ContentType   
      {
         
get
         {
            
return _ContentType;
         }
         
set
         {

            
this._ContentType = value;
         }
      }
// Property DeptId
      public string DeptId
      {
          
get
          {
              
return _DeptId;
          }
          
set
          {
              
if (this._DeptId != value)
                  
this._DeptId = value;
          }
      }
      
// Property DeptName
      public string DeptName
      {
          
get
          {
              
return _DeptName;
          }
          
set
          {

             
this._DeptName = value;
          }
      }
private string _Type;
       
public string OwnerType
       {
           
get
           {
               
return _Type;
           }
           
set
           {

              
this._Type = value;
           }
       }
       
private bool _TemplateAble;
      
/// <summary>
      
/// 是否可以作为模版
      
/// </summary>
       public bool Templateable
       {
           
get
           {
               
return _TemplateAble;
           }
           
set
           {

              
this._TemplateAble = value;
           }
       }
       
public override string ToString()
       {
           
return Encoding.UTF8.GetString(this.Content);
       }        
public static Document FromString(string s, User user)
       {
           Document doc 
= new Document();
           doc.CreateTime 
= DateTime.Now;
           doc.CreatorId 
= user.Id;
           doc.CreatorName 
= user.Name;
           doc.DeptId 
= user.OrgId;
           doc.DeptName 
= user.OrgName;
           doc.Content 
= Encoding.UTF8.GetBytes(s);
           doc.Size 
= doc.Content.Length;
           doc.ContentType 
= "text/plain";
           
return doc;
       }
      
public static string ByteToString( byte[] bytes )
      {
          
return Encoding.UTF8.GetString( bytes );
      }
       
public static byte[] StringToByte(string s)
       {
           
return Encoding.UTF8.GetBytes(s); 
       }
       
public string GetExtendName()
       {
           
string[] arr = this.Name.Split( '.' );if (arr.Length < 1return "";
           
else return arr[ arr.Length - 1 ];
       }   
   }

考虑到数据操作逻辑的可变性,不同的项目里面附件表设计的不同,这里引入一个数据操作接口:

public interface IWebdavDocumentHandler
{
        Document GetDocument(
int id);//获取文档数据
        
void ModifyDocContent(int docId, byte[] data);//修改文档内容
}

具体的实现这里就不写了。

好了,我们的数据访问逻辑已经有了,那么首先看get动作处理的实现:

    class GetHandler : IVerbHandler
    {
        
#region IVerbHandler 成员
        
public void Process(System.Web.HttpContext context)
        {
            
int id = WebdavProtocolHandler.GetDocumentId( context ); //获取到主键IWebdavDocumentHandler docSvr = new DefaultWebdavDocumentHandler(); //修改此处代码,实现不同的数据操作逻辑,可引入工厂模式
            Document doc 
= docSvr.GetDocument(id);if (doc == null)
            {
                context.Response.Write(
"文档不存在!");
                
return;
            }

            context.Response.Clear();
            context.Response.ContentType 

= doc.ContentType;
            
//下载文件名限制32字符 16 汉字
            int maxlength = 15;
            
string fileName = doc.Name; //att.FileName ;
            if (fileName.Length > maxlength)
            {
                fileName 
= "-" + fileName.Substring(fileName.Length - maxlength, maxlength);
            }

            fileName 

= HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8); //必须编码,不然文件名会出现乱码
            context.Response.AppendHeader("Content-Disposition""attachment;filename=" + fileName + "");          if (doc.Content != null && doc.Content.Length > 0)
                context.Response.BinaryWrite(doc.Content);

            context.Response.End();
        }

#endregion
    }

很简单吧,跟我们普通实现文档下载的代码一样。

put动作的实现:

 class PutHandler : IVerbHandler
    {
        
#region IVerbHandler 成员public void Process(System.Web.HttpContext context)
        {
            
int docId = WebdavProtocolHandler.GetDocumentId(context);

            Document doc 

= GetDocFromInput(context.Request);

            doc.DocumentId 

= docId; IWebdavDocumentHandler docSvr = new DefaultWebdavDocumentHandler(); //修改此处代码,实现不同的数据操作逻辑,可引入工厂模式

            docSvr.ModifyDocContent( doc.DocumentId , doc.Content );
        }

private Document GetDocFromInput(System.Web.HttpRequest request )
        {
            Document doc 
= new Document();
            
//读取文件的数据
            doc.Content = new byte[ request.ContentLength ];
            doc.Size 
= request.ContentLength;
            Stream fileDataStream 
= request.InputStream;
            fileDataStream.Read( doc.Content , 
0, doc.Size );
            doc.ContentType 
= request.ContentType;
            
return doc;
        }
        
#endregion
    }

OK,主要的动作都实现了,下面,我们需要WebdavProtocolHandler将各命令处理对象整合到一起:

    public class WebdavProtocolHandler : IHttpHandler
    {
        
public static int GetDocumentId( HttpContext context )//按照前面确定的主键策略返回主键
        {
            
string url = context.Request.Url.ToString();
            
string[] arr = url.Split( '/' );
            
string id = arr[arr.Length - 2];
            
return Convert.ToInt32( id );
        }
        
public void ProcessRequest(HttpContext context)
        {
            HttpRequest Request 
= context.Request;
            context.Response.AppendHeader(
"OpenWebDavServer""1.0");
            
string verb = Request.HttpMethod;
            
//Log.Write(verb);
            IVerbHandler vh 
= GetVerbHandler( verb );if( vh == null )
                
return ;

            vh.Process(context);      
        }

private IVerbHandler GetVerbHandler(string verb)
        {
            
switch (verb)
            {
                
case "LOCK" :
                    
return new LockHandler();
                
case "UNLOCK":
                    
return new UnLockHandler();
                
case "GET":
                    
return new GetHandler();
                
case "PUT":
                    
return new PutHandler();               
                
case "OPTIONS":
                    
return new OptionsHandler();
                
default :
                    
return null;
            }
        }    

        
public bool IsReusable
        {
            
get { return false; }
        }

    }

到这里呢,已经基本上算game over了,基于以上代码设计,可以完全实现office文档的在线编辑。若要通过链接直接打开编辑,可以
采用Office文档在线编辑的实现之一Document_Edit2函数触发office编辑。

哦,IIS还需要做一点小配置:
1)将.doc , .xml 加入到站点虚拟目录的isapi映射, 不要选中 "确认文件是否存在",动作要选全部动作,
2)禁用IIS本身的Webdav扩展,
3)删除虚拟目录HTTP头中的自定义HTTP头: MicrosoftOfficeWebServer,如果有的话。