文件和流(文件系统)
大多数 Web 应用程序依赖数据库来存储信息。在多用户场景中,数据库是无与伦比的。但还是会有不可避免的遇到访问存储在其他地方(如文件系统)的数据的问题。常见的示例有:读取其他程序产生的信息、为测试而编写的临时日志等。
使用文件系统
最基本的文件访问包括获取现有文件和目录的信息,以及执行典型的文件系统操作,如复制文件和创建目录。这些任务并没有设计真正的打开或写文件。
.NET 提供了几个基类(System.IO 命名空间中)用于获取文件系统信息:
- Directory 和 File :这两个类提供一组静态方法,可以通过它们获取任意服务器上可见的文件和目录的信息。
- DirectoryInfo、FileInfo、DriveInfo :这些类用相似的实例方法获取同样的信息。
这两组类提供相似的方法和属性。一般而言,Directory 和 File 类更适合处理一次性任务。DirectoryInfo 和 FileInfo 更适合获取若干信息,这样不必再每次调用方法时都提供文件或目录的信息。同时,它们还更快一些,因为 DirectoryInfo 和 FileInfo 类只执行一次安全检查(创建对象的实例时),而 Directory 和 File 在每次调用方法时都需要执行安全检查。
Directory 类和 File 类
它们提供了很多有用的方法,每个方法都接受相同的参数:用于识别执行操作的文件和目录的完全限定路径名。
Directory 方法:
CreateDirectory() | 创建一个或一组目录(创建不存在的目录中的子目录时)。 |
Delete() | 删除对应的空目录。如果要删除该目录及其所有内容(子目录和文件),第二个可选参数设为 True |
Exists() | 返回 bool 值表示指定的目录是否存在 |
GetCreationTime() GetLastAccessTime() GetLastWriteTime() | 返回一个 DateTime 对象,代表创建、访问、写入目录的时间。每个Get方法都有一个对应的Set方法,这里没有列出。 |
GetDirectories() GetFiles() | 返回一个字符串数组,数组元素代表指定目录中的子目录或文件。第二个可选参数支持搜索表达式,如 (ASP*.*) |
GetLogicaDrives() | 返回一个字符串数组,数组元素代表当前计算机上定义的一个驱动器,如 c:\ |
GetParent() | 返回指定目录的父目录 |
GetCurrentDirectory() SetCurrentDirectory() | 设置和获取当前目录 |
Move() | 接收两个参数:源路径和目标路径。目录及其内容可以移动到任意路径,但必须在同一个磁盘 |
GetAccessControl() SetAccessControl() | 返回或设置一个 System.Secutiry.AccessControl.DirectorySecurity 对象。可以使用这个对象检查 Windows 访问控制表(ACL),它们应用于这个目录且可以通过编程来修改。 |
File 方法:
Copy() | 接收两个参数:源文件和目标文件的完整文件名。如果允许覆写,第三个可选参数设为 true |
Delete() | 删除指定的文件,如果找不到文件也不会抛出异常。 |
Exists() | 文件是否存在 |
GetAttributes() SetAttributes() | 获取或设置一个可以包含 FillAttribute 枚举值的任意组合的枚举值 |
GetCreationTime() GetLastAccessTime() GetLastWriteTime() | 返回一个 DateTime 对象,代表创建、访问、写入文件的时间。每个Get方法都有一个对应的Set方法,这里没有列出。 |
Move() | 接收两个参数:源文件和目标文件的完整文件名。可以跨磁盘移动文件甚至同时重命名文件 |
Create() CreateText() | 创建指定的文件并返回一个可用于写的 FileStream 对象。CreateText() 执行同样的任务但是返回一个封装了流的 StreamWriter 对象。 |
Open() OpenText() OpenRead() OpenWrite() | 打开一个文件(假设其存在)。OpenText() 和 OpenRead() 以只读方式打开文件,返回一个 FileStream 或 StreamReader 。OpenWrite() 以只写方式打开文件,返回 FileStream |
ReadAllText() ReadAllLines() ReadAllBytes() | 读取整个文件。分别以单一字符串、字符串数组、字节数组的形式返回内容。仅在小文件才使用这些方法,对于大型文件,使用流每次读取一块以节省内存使用。 |
WriteAllText() WriteAllLines() WriteAllBytes() | 将单一字符串、字符串数组、字节数组一次写入完整文件内容。可以覆盖已存在的文件。 |
GetAccessControl() SetAccessControl() | 返回或设置一个 System.Secutiry.AccessControl.FileSecurity 对象。可以使用这个对象检查 Windows 访问控制表(ACL),它们应用于这个目录且可以通过编程来修改。 |
提示:File 类缺乏的唯一功能是获取给定文件的大小(可由 FileInfo 类提供)。
string directoryName = @"d:\AnswerQuestion";
ListBox1.DataSource = Directory.GetFiles(directoryName);
ListBox1.DataBind();
DirectoryInfo 类和 FileInfo 类
DirectoryInfo 和 FileInfo 类镜像 Directory 和 File 类的功能。此外,它们还可以轻易的遍历文件和目录的关系。
Directory 类和 File 类只提供了方法,而 DirectoryInfo 类和 FileInfo 类同时还提供了属性。DirectoryInfo 类和 FileInfo 类的另一个好处是它们共享一组属性和方法,因为它们都继承自 FileSystemInfo 基类。
DirectoryInfo 和 FileInfo 的共有成员:
Attributes | 获取和设置 FileAttributes 枚举值,从而设置文件特性 |
CreationTime()
LastAccessTime() LastWriteTime() |
允许使用 DateTime 对象获取和设置创建、最后访问和最后写入的时间 |
Exists | 按照文件和目录是否存在返回 true 或 false |
FullName
Name Extension |
返回一个字符串,分别代表完全限定名、目录或文件名(带有扩展名)、扩展名 |
Delete() | 如果文件或目录存在,则删除。删除目录时必须为空,否则指定一个为 true 的可选参数 |
Refresh() | 更新对象使其与文件系统发生的任意变化同步。 |
Create() | 创建指定的文件或目录 |
MoveTo() | 复制目录及其内容或者文件。对于 DirectoryInfo 需要指定新路径,对于 FileInfo 需要指定路径和文件名 |
DirectoryInfo 特有成员:
Parent 和 Root | 返回父目录或根目录 |
CreateSubdirectory() | 在 DirectoryInfo 对象代表的目录中创建一个目录,返回一个代表子目录的 DirectoryInfo 对象 |
GetDirectories() | 返回一个 DirectoryInfo 数组,代表当前目录的所有子目录 |
GetFiles() | 返回代表当前目录中所有文件的 FileInfo 对象的数组 |
FileInfo 特有成员:
Directory | 返回一个代表父目录的 DirectoryInfo 对象 |
DirectoryName | 返回父目录名称的字符串 |
Length | 返回长整形的文件字节数 |
CopyTo() | 复制文件到指定的新路径和文件名。返回一个代表新文件(复制的)的 FileInfo 对象 |
Create()
CreateText() |
创建指定的文件,返回一个可用来写文件的 FileStream 或 封装了流的 StreamWriter 对象 |
Open()
OpenText() OpenReader() OpenWrite() |
打开一个文件(假设存在)。OpenRead 和 OpenText 以只读方式打开文件,返回一个 FileStream 或 StreamReader 。OpenWrite 以只写方式打开文件,返回一个 FileStream |
// new 对象时不一定要对应真实的物理文件或目录
DirectoryInfo myDerectory = new DirectoryInfo(@"d:\AnswerQuestion");
FileInfo myFile = new FileInfo(@"d:\AnswerQuestion\test.txt");
// 如果不确定,可以这样检查
if (!myDerectory.Exists)
{
myDerectory.Create();
}
if (!myFile.Exists)
{
// Create() 返回一个可写的文件流
FileStream stream = myFile.Create();
stream.Close();
}
DirectoryInfo 和 FileInfo 对象在你第一次查询某个属性时获取来自文件系统的信息,在后续的使用中不再检查新的信息。如果此时文件发生了变化会导致不一致性。如果你知道或者怀疑数据有不一致的可能,可以调用 ReFresh()方法获取最新的信息。
DirectoryInfo 没有提供任何获取目录大小的属性。不过你可以累加所有文件的 File.Lengh 属性:
private static long CalculateDirectorySize(DirectoryInfo directory, bool includeSubdirectories)
{
long totalSize = 0;
FileInfo[] files = directory.GetFiles();
foreach (FileInfo file in files)
{
totalSize += file.Length;
}
if (includeSubdirectories)
{
DirectoryInfo[] dirs = directory.GetDirectories();
foreach (DirectoryInfo dir in dirs)
{
totalSize += CalculateDirectorySize(dir, true);
}
}
return totalSize;
}
DriveInfo
关于剩余空间的信息,要借助 DriveInfo 类。DriveInfo 允许你获得计算机驱动器的信息。没什么信息会让你感兴趣,除了获取磁盘已经使用的空间大小和剩余空间的大小。
TotalSize | 驱动器的总字节数 |
TotalFreeSpace | 磁盘剩余字节数 |
AvailableFreeSpace | 磁盘可用空间的字节数 |
DriveFormat | 驱动器所使用的文件系统名称(NTFS 或 FAT32) |
DriveType | 返回一个 DriveType 枚举值,表明驱动器是固定的、网络的、CD-ROM等等或者返回 Unknown |
IsReady | 返回驱动器是否已经准备好用于读或写 |
Name | 返回驱动器符号(如 C: E: ) |
VolumeLabel | 返回或设置驱动器卷标 |
RootDirectory | 返回驱动器根目录的 DirectoryInfo 对象 |
GetDrives() | 获取一个 DriveInfo 数组,代表当前计算机的所有逻辑分区 |
使用 Attributes
FileInfo 和 DirectoryInfo 类的 Attributes 属性代表文件或目录的文件系统特性,所有它包含一组 FileAttributes 枚举值。
FileAttributes 枚举值:
Archive | 项已经归档。应用程序可以基于这个特性标志备份或删除的文件。 |
Compressed | 项被压缩 |
Device | 当前不使用,为以后保留的枚举关键字 |
Directory | 项是一个目录 |
Encrypted | 项被加密 |
Hidden | 项是隐藏的(但仍然可以在 Windows 资源管理器里看到它) |
Normal | 项是普通的,没有任何特性设置。这个特性仅在单独使用时有效 |
NotContentIndexed | 项不会按照操作系统的内容索引服务来服务 |
Offline | 文件不在线且当前不可用 |
Readonly | 项是只读的 |
ReparsePoint | 文件包含再解析点,它是与NTFS文件系统中的文件或目录关联的一块用户定义的数据 |
SparseFile | 文件是稀疏文件。稀疏文件通常是由大量零组成的大型文件。这个项仅被 NTFS 支持 |
System | 项是操作系统的一部分或者只被操作系统使用 |
Temporary | 项是临时的,应用程序不使用它时可以被删除 |
FileInfo myFile = new FileInfo(@"d:\AnswerQuestion\TestCSharp.pdb");
// 调用 Attributes.ToString() 返回逗号间隔的特性列表
lblInfo.Text = myFile.Attributes.ToString();
if (myFile.Attributes == FileAttributes.ReadOnly)
{
// 这个 if 块的逻辑是错误的,仅当文件只含有只读属性时才成立
}
if ((myFile.Attributes & FileAttributes.ReadOnly) != 0)
{
// 判断特性是不是含有 ReadOnly,需要进行位与运算
// 如果 == 0,那么就不包含这个特性
}
// 设置 只读 特性并且保留其他所有原特性
myFile.Attributes = myFile.Attributes | FileAttributes.ReadOnly;
// 先或后与,下面这步会移除 ReadOnly 之外所有的特性
myFile.Attributes = myFile.Attributes & FileAttributes.ReadOnly;
使用通配符过滤文件
DirectoryInfo 和 Directory 都提供了一种方式:可以根据搜索表达式来匹配特定的目录或文件。* 代表0或多个任意字符 ? 代表任意单个字符:
DirectoryInfo dir = new DirectoryInfo(@"d:\AnswerQuestion");
FileInfo[] files = dir.GetFiles("*.txt");
DirectoryInfo[] dirs = dir.GetDirectories();
foreach (FileInfo file in files)
{
// ......
}
foreach (DirectoryInfo dir in dirs)
{
// ......
}
// GetFiles() 和 GetDirectories() 仅搜索当前目录
// 如果要包含子目录进行搜索,需要使用递归
获取文件的版本信息
文件版本信息是你在资源管理器中查看 EXE 或 DLL 文件时所看到的信息。版本信息通常包括版本号、生产组件的公司名称、商标信息等。
FileInfo 和 File 都没有提供获取文件版本信息的方法。不过可以使用 System.Diagnostics.FileVersionInfo.GetVersionInfo() 获取 FileVersionInfo 对象。
FileVersionInfo 属性:
FileVersion、FileMajorPart
FileMinorPart、FileBuildPart FilePrivatePart |
一个典型的主版本号显示格式为:【主版本号】.【次版本号】.【内部版本号】.【专用版本号】 |
FileName | 获取 FileVersionInfo 实例所描述的文件的名称 |
OriginalFilename | 获取文件创建时的名称 |
InternalName | 如果存在,则获取文件的内部名称 |
FileDescription | 获取文件的描述 |
CompanyName | 获取创建文件的公司名 |
ProductName | 获取随文件一起发布的产品的名称 |
ProductVersion、ProductMajorPart
ProductMinorPart、ProductBuildPart ProductPrivatePart |
获取完整产品版本号(ProductVersion) |
IsDebug | 文件是否包含调试信息或是否在调试启用的情况下被编译 |
IsPatched | 文件是否已修改且是否不同于相同版本号的原始文件 |
IsPreRelease | 表示文件是开发版本,而不是用于商业目的的发行版本 |
IsPrivateBuild | 是否是采用标准的发行过程开发的 |
IsSpecialBuild | 文件是否为特殊的内部版本 |
Comments | 获取与文件关联的注释 |
Language | 获取版本信息块的默认语言字符串 |
LegalCopyright | 获取所有适用于指定文件的版权声明 |
LegalTrademarks | 获取应用到文件的商标和注册商标 |
Path 类
如果你正在使用文件,那么也很有可能要用用到文件路径和目录路径。路径信息用普通的字符串保存。因此,有时需要字符串解析代码来操作这些字符。
Path 类提供了若干个执行常见路径处理任务的静态辅助方法,Path.Combine()可以把一个完整的目录路径与那个目录中的任意文件名一起使用:
DirectoryInfo dirInfo = new DirectoryInfo(@"d:\AnswerQuestion");
string file = "test.txt";
string path = Path.Combine(dirInfo.FullName, file);
为了防止规范化错误之类的风险,也可以使用 Path 类。
规范化错误是一种特定类型的应用程序错误,它会在你的代码中假定用户提供的值总是符合标准格式时发生。规范化错误时一项低级但非常严重的技术,它们通常会导致用户可以执行某个本应该受到限制的动作。(一个恶名昭彰的规范化错误是 SQL 注入,其他形式的规范化错误可能发生在 路径 和 URL 中)。
考虑下面的代码:
// 攻击者加入提供一个限定文件名,如: ..\filename
// 连接后的路径 WebApp\Documents\..\filename 将会从父目录 WepApp 获取文件
FileInfo file1 = new FileInfo(Server.MapPath("Documents\\" + txtBox.Text));
// 代码的修复其实很简单,借助一次 Path 类
// 使用 GetFileName() 仅提取字符串最终的文件名部分
string filename = Path.GetFileName(txtBox.Text);
FileInfo file2 = new FileInfo(Server.MapPath(Path.Combine("Documents", filename)));
如果要处理 URL,可以用 Syestem.Uri 类型实现同样的操作。下面的代码演示了 删除查询字符串并确保它只指向给定的服务器和虚拟目录:
string uriString = "http://www.wrongsite.com/page.aspx?cmd=run";
Uri uri = new Uri(uriString);
// 获取 Uri 的绝对路径 page 的值现在是: page.aspx
string page = Path.GetFileName(uri.AbsolutePath);
// 创建正确的虚拟目录路径及合适的文件名
Uri baseUri = new Uri("http://www.rightsite.com");
uri = new Uri(baseUri, page);
Path 类最有用的方法:
Combine() | 合并文件或子目录的路径 |
ChangeExtension() | 修改字符串中当前文件的扩展名。如果未指定扩展名,当前扩展名会被删除 |
FetDirectoryName() | 返回目录信息。它是第一个和最后一个目录分隔符(\)之间的文本 |
GetFileName() | 返回路径的文件名部分 |
GetFileNameWithoutExtension() | 与 GetFileName() 相似,但返回的字符串中忽略了扩展名 |
GetFullPath() | 该方法对绝对路径无效。它按当前目录把相对路径变成绝对路径 |
GetPathRoot() | 获取根目录的字符串信息(例如 C:\),对于相对目录,返回空引用 |
HasExtension() | 如果路径以文件扩展名结束,则返回 true |
IsPathRooted() | 如果路径是绝对路径,则返回 true,如果是相对路径,则返回 false |
虽然 Path 类有从目录结构向下延伸的方法(在路径中加入子目录),但它没有提供任何返回上层目录的方法(从路径中删除子目录)。不过,可以在 Combine()里使用相对路径打破这一限制,然后用 GetFullPath()让它以正常的格式返回。
string path = @"c:\temp\subdir";
path = Path.Combine(path, "..");// path = c:\temp\subdir\..
path = Path.GetFullPath(path); // path = c:\temp