zoukankan      html  css  js  c++  java
  • 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,如果有的话。


    this is the real end.






  • 相关阅读:
    __getitem__ 方法的使用
    python加载测试用例时,修改用例不必须以“test”开头
    python 反射
    python 类中__getattr__的使用
    python 中 __dict__函数的使用
    python 类中__int__和__str__的使用
    ubuntu16.04Nvidia驱动、CUDA、cuDNN安装与卸载
    C++中的各种可调用对象
    ubuntu16.04安装QGIS工具
    C/C++中extern关键字详解
  • 原文地址:https://www.cnblogs.com/jianyi0115/p/818566.html
Copyright © 2011-2022 走看看