基于 SQLite 的简单文件系统
实现一个易于使用的、由 SQLite 数据库支持的文件系统。
引言
SQLite 是一个非常好的数据库引擎,适用于存储关系数据的小型应用程序。然而,大多数时候,我们仍然希望以传统的方式使用文件系统来存储数据:拥有不同的目录,并在这些目录中保存不同的数据文件。
这样,您的数据就分散在文件系统和数据库文件中。也许您已经习惯了,但我希望将所有数据集中在一个地方。在备份 Android 应用数据时,如果所有数据都打包到一个数据库文件中,那将是非常棒的。那么,为什么不将 SQLite 表与传统文件系统结合起来呢?本项目将“创建”一个 SQLite 数据库文件内的简单文件系统。
背景
正如大家所知,任何文件系统都需要分配一些空间来存储关于文件系统本身、目录和文件的元数据。本项目只创建了三个表。让我们看一下表架构。
FsInfo(用于存储文件系统本身信息的表)
CREATE TABLE FsInfo (infoName varchar(128) primary key,infoVal varchar(128))
这个表只是一个简单的键值对结构。
FsBlock(用于存储目录和文件元数据的表)
CREATE TABLE FsBlock (fsID integer primary key autoincrement,
fsType integer,
fsCreateTime integer,
fsLastModTime integer,
fsFileSize integer,
fsName varchar(512),
fsParent integer,
fsChild blob)
此表中的每一行都描述了一个目录或文件。
- fsID -- 一个自动递增的 ID,用于唯一标识目录或文件(没有两个目录或文件会有相同的 ID)(ID 号 '1' 保留给根目录)
- fsType -- '0' 表示目录,'1' 表示文件
- fsCreateTime -- 创建时间,存储为 Windows 文件时间格式(即,自 1601 年 1 月 1 日以来的 100 纳秒数)
- fsLastModTime -- 最后修改时间
- fsFileSize -- 对于目录,为 '0';对于文件,为 'DataBlock' 表中存储的字节数
- fsName -- 文件或目录的名称(非完整路径)
- fsParent -- 父目录的 fsID
- fsChild -- 对于目录,它是其子目录 fsID 的数组(每个 ID 大小为 4 字节,但可以更改为 8 字节);对于文件,它是单个 'dID',即 'DataBlock' 表中的主键
DataBlock(用于存储所有文件实际数据的表)
CREATE TABLE DataBlock (dID integer primary key autoincrement,
dFileType integer,
dTextData text,
dRawBinData blob)
此表中的每一行包含文件的实际数据。
- dID -- 一个自动递增的 ID,用于唯一标识文件内容
- dFileType -- '0' 表示文本文件,文件数据应从 'dTextData' 获取;'1' 表示二进制文件,文件数据应从 'dRawBinData' 获取
- dTextData -- 如果对应的 'dFileType' 为 '0',则包含文件的文本内容
- dRawBinData -- 如果对应的 'dFileType' 为 '1',则包含文件的二进制内容
实际示例
让我们以实际示例来说明上述表。
FsInfo 中只存储了一些简单的数据。实际上,您可以存储任何您喜欢的内容。
上面的 FsBlock 显示了如下的目录层次结构。
/ (root dir)
|
+-- hello/
| |
| +-- hellotext.txt
| |
| +-- hellobin.bin
|
+-- dir2/
|
+-- dir3/
|
+-- yes.bin
|
+-- mytext.txt
DataBlock 显示了 FsBlock 中“文件”的映射。(FsBlock 表中的 ID 存储在 BLOB 中,因此在上面的图片中未显示。)
/yes.bin -- dID 1
/mytext.txt -- dID 2
/hello/hellotext.txt -- dID 3
/hello/hellobin.bin -- dID 4
SqlFs 库
背景和示例就到这里。我将实现上述文件系统的库称为“SqlFs”。让我们来看看 Java 类。
在 zip 压缩包 sqlfs.zip 中,有两个项目——SqlFs(一个库项目)和 TestSqlFs(包含 SqlFs 库的一些测试用例)。
如果您查看 SqlFs/src/com/sss/sqlfs 目录,最常用的类是:
SqlFs
SqlFsNode
SqlDir (derived from SqlFsNode)
SqlFile (derived from SqlFsNode)
IFileData
SimpleFileData (derived from IFileData)
FsID
您可以使用上面列出的这几个类来操作(创建、删除、读取、写入和更新目录和文件)这些 SQLite 表。我将简要介绍这些类。
SqlFs
一切都从 SqlFs
类开始。首先,您需要创建/打开一个数据库文件。
SqlFs fs = SqlFs.create("/sdcard/hello.db", appContext);
获得 SqlFs
实例后,您可以使用以下方法在 FsInfo 表中读写键值对。
String getInfo(String infoName);
void writeInfo(String infoName, String infoVal);
要在根目录下创建目录/文件,您需要先检索根目录。
SqlDir rootDir = fs.getRootDir();
或者,如果您知道目录或文件的绝对路径(非相对路径),请尝试:
SqlDir getDir(String dirPath)
SqlFile getFile(String filePath)
完成后,您需要“关闭”文件系统。
void close();
请查看下面的 SqlDir,了解如何使用它。
路径分隔符、当前目录和父目录
在继续之前,最好定义一些 SqlFs 中使用的符号。如果您查看 'SqlFsConst.java',您可以看到:
路径分隔符是正斜杠——'/'(类似于 Unix)。当前目录是单个点——'.'。父目录是两个点——'..'。目录和文件名中不允许的字符是——'\', '/', ':', '*', '?', '"', '<', '>', '|'。
因此,绝对路径可以写成 /sdcard/hello.txt,而相对路径可以写成 ../hello/hello.txt。就像您在 Unix 或 Linux 上所做的那样。
SqlFsNode
SqlFsNode
是 SqlDir
和 SqlFile
的父类。您永远不会直接实例化这个类。这个类包含了 SqlDir
和 SqlFile
共有的方法。
Calendar getCreateTime()
Calendar getLastModTime()
int getFileSize()
String getName()
SqlDir getParent()
boolean rename(String newName)
boolean isAncestor(SqlDir dir) // check if 'dir' is one of its ancestor
boolean move(String destPath)
boolean move(SqlDir destDir)
'move
' 方法的参数可以是绝对路径或相对路径。
SqlDir
SqlDir 可以执行的常规操作:
int getChildCount()
boolean isAlreadyExist(String name) // check if there is any child with the same name
SqlDir addDir(String dirName)
SqlFile addFile(String fileName)
boolean delete()
SqlFsNode getChild(String name)
ArrayList<SqlFsNode> getChildList()
ArrayList<SqlDir> getSubDirs()
ArrayList<SqlFile> getFiles()
SqlFsNode getFsNode(String path)
SqlDir getDir(String dirPath)
SqlFile getFile(String filePath)
这些方法非常直观。此处所有路径都必须是相对路径。
SqlFile 和 SimpleFileData
让我们看看 SqlFile 能做什么:
boolean delete()
boolean getFileData(IFileData fileData)
boolean saveFileData(IFileData fileData)
getFileData
的示例:
SqlFile file = rootDir.getFile("mytext.txt");
SimpleFileData fdRetrieve = new SimpleFileData();
file.getFileData(fdRetrieve);
if (fdRetrieve.isTextFile()) {
String dataRetStr = fdRetrieve.getText();
...
}
else {
byte[] dataRetrieve = fdRetrieve.getRawBinData();
...
}
saveFileData
的示例:
// save binary data
SqlFile file = rootDir.addFile("yes.bin");
SimpleFileData fd = new SimpleFileData();
byte[] dataBin = new byte[]{0x34, 0x12, 0x09, 0x11, 0x08};
fd.setRawBinData(dataBin);
file.saveFileData(fd);
// save text data
SqlFile file = rootDir.addFile("hellotext.txt");
SimpleFileData fd = new SimpleFileData();
String saveStr = "The system has recovered from a serious error.";
fd.setTextData(saveStr);
file.saveFileData(fd);
SimpleFileData
是 IFileData
的一个参考实现。如果您没有特殊需求,就使用它。实际上,您可以定义自己的 IFileData
实现,但那样您需要使用另一个 'SqlFs.create
' 来实例化 SqlFs
。
// assume MyFileData is your own implementation of IFileData
SqlFs fs = SqlFs.create("/sdcard/myfile.db", new MyFileData(), appContext);
在测试用例项目 TestSqlFs 中,有一个用户定义的 IFileData
实现(UrlFileData
)的示例。'DataBlock' 表的架构也与 SimpleFileData
使用的有所不同。
FsID
这是一个包装 FsBlock 中使用的 fsID 的类。默认情况下,它使用 32 位整数,但可以通过更改 FsID 内部标志来更改为使用 64 位。
private static final boolean useLongID
线程安全
我没有测试在 Android 上用两个不同的进程读取/写入同一个数据库文件,但有一个测试用例(在 TestSqlFs 中——TestMultiReadWrite.testReadWrite
)可以在同一个进程中使用两个不同的线程来读取/写入同一个 DB。
内部,SqlFs
、SqlDir
和 SqlFile
的每个公共操作都受到一个锁的保护。
java.util.concurrent.locks.ReentrantLock
因此,在同一个进程的两个不同线程中处理同一个 DB 是线程安全的。此外,由于许多操作无法仅通过一个 SQL 语句完成,因此它们被包装在 beginTransaction
和 endTransaction
中。
为了安全起见,每个线程都应该实例化自己的 SqlFs(即使访问同一个 DB),并且不要在线程之间传递 SqlFs
、SqlDir
和 SqlFile
实例。
如何运行 TestSqlFs.apk
它不是一个具有 GUI 的普通 Android APK,而是需要在 CmdConsole 下运行(https://codeproject.org.cn/Articles/202996/Write-a-console-app-on-Android-using-Java)。
关注点
这个库只适用于 Android 上的小型应用程序。如果您问我这个库的性能,嗯……老实说,不会非常好。但是,因为那些小型 Android 应用不会在任何时候进行大量的读写操作,所以这个库是适合使用的。