上篇文章
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
(
1
,
1
)
NOT
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
<
1
)
return
""
;
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.