本系列将记述使用Java实现一个简单的二级文件系统的过程。这个项目是本学期专业课操作系统的第三个实验。上一篇文章说明了如何利用已经搭建好的代码构造一个简单的文件系统。本文将记述人机交互界面的构建,并最终让整个系统运行起来。
架构
构建项目的时候,避免代码变成一坨屎的最重要的技巧在于区分哪些功能是哪些部分应该实现的,从语义和逻辑上考察这个问题,搞清楚了之后代码就不会变成一团乱。
之前对于文件、用户和磁盘的操作全都在文件系统中实现了。而我们要写的交互界面,将起到操作系统的作用。换句话说它要处理用户的认证、命令解析、打印必要的提示信息、询问命令执行依赖的参数,并最终根据已有的信息调用文件系统或其他代码产生影响。
对于命令,我设计了一种比较便于解析的格式:
help
打印帮助信息exit
退出系统user
是与用户相关的命令user create
创建用户user login
用户登录- ......
file
是与文件相关的命令file list
列出用户的文件file create
创建文件- ......
在此基础上,交互界面的大体框架如下:
package info.skyblond.os.exp.experiment3;
import info.skyblond.os.exp.experiment3.model.IndexEntry;
import info.skyblond.os.exp.experiment3.model.OpenedFile;
import info.skyblond.os.exp.experiment3.model.User;
import info.skyblond.os.exp.utils.Pair;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.stream.Collectors;
/**
* 文件系统模拟器的主入口
* 模仿一个简单的操作系统,利用文件系统进行用户鉴权、文件操作
*/
public class Exp3 {
private static final Simulator simulator = Simulator.getInstance();
public static String getPromptLine(String username, String hostname) {
if (username.isBlank()) {
return "nobody@" + hostname + "> ";
} else {
return username + "@" + hostname + "> ";
}
}
public static void main(String[] args) {
var dataFile = new File("./data.bin");
simulator.setDataFile(dataFile);
if (!dataFile.exists()) {
System.out.println("未检测到数据文件,开始初始化。");
simulator.initDataFile();
System.out.println("已创建默认用户:root,密码:" + Simulator.ROOT_DEFAULT_PASSWORD);
}
var hostname = "FSS";
var currentUser = "";
// prompt
System.out.print(getPromptLine(currentUser, hostname));
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String[] line = scanner.nextLine().split(" ");
if (line[0].equalsIgnoreCase("help")) {
// 帮助
} else if (line[0].equalsIgnoreCase("exit")) {
// 退出
} else if (line.length == 2) {
// 功能命令
switch (line[0].toLowerCase()) {
case "user":
// 用户相关命令
break;
case "file":
// 文件相关命令
break;
default:
// 未知命令
break;
} else {
// 未知命令
}
System.out.print(getPromptLine(currentUser, hostname));
}
}
}
一上来有一个工具方法getPromptLine
帮我们拼接出一个提示符,如果当前登录的用户为空,说明没有人登录,就把用户名替换成nobody
,然后在解析命令之前先把这个提示符打印出来,像下面这样:
nobody@FSS> user login
root@FSS> file list
这样一个优化能够让简陋的命令行一下子上档次不少。
主函数一上来要先准备数据文件,如果数据文件不存在就要初始化一个,并且提供用户默认账户的信息。之后开始命令的解析工作
帮助
这是系统中两个特殊命令之一。通过忽略大小写判断,如果用户在命令行内输入的内容等于help
,那么就打印一个帮助信息,告诉用户这个系统都支持什么命令:
System.out.println("user list \t\t 列出所有已知用户");
System.out.println("user create \t 创建新用户(仅限root)");
System.out.println("user delete \t 删除已有用户(仅限root)");
System.out.println("user login \t\t 登录(仅未登录时)");
System.out.println("user logout \t 登出(仅登录时)");
System.out.println("user chpasswd \t 修改密码(仅登录时)");
System.out.println("-".repeat(50));
System.out.println("file list \t\t 列出文件(仅登录时)");
System.out.println("file create \t 创建文件(仅登录时)");
System.out.println("file delete \t 删除文件(仅登录时)");
System.out.println("file open \t\t 打开文件(仅登录时)");
System.out.println("file close \t\t 关闭文件(仅登录时)");
System.out.println("file read \t\t 读文件(仅登录时)");
System.out.println("file write \t\t 写文件(仅登录时)");
System.out.println("file cutoff \t 截断文件(仅登录时)");
System.out.println("-".repeat(50));
System.out.println("help \t\t\t 打印帮助信息");
System.out.println("exit \t\t\t 退出系统");
就是一堆打印命令。
退出系统
这个是系统中第二个特殊命令。如果用户输入了exit
,那么就退出系统:
System.out.println("Bye!");
return;
因为在主函数里,所以直接打印一个Bye!
之后return,主函数结束,系统自然就退出了。
功能命令
这部分是实际执行一些操作的命令。由于规定这些命令都是两个单词构成,所以只要判断单词的个数就能确定是这一类命令:
if (line[0].equalsIgnoreCase("help")) {
// 帮助
} else if (line[0].equalsIgnoreCase("exit")) {
// 退出
} else if (line.length == 2) {
// 功能命令
switch (line[0].toLowerCase()) {
case "user":
// 用户相关命令
break;
case "file":
// 文件相关命令
break;
default:
// 未知命令
System.out.println("未知命令");
break;
} else {
// 未知命令
System.out.println("命令格式有误");
}
所有用户相关的命令都以user
开头,所有文件相关的命令都由file
开头,所以直接一个switch语句就可以将二者区分开。随后为了提高鲁棒性,加一个default处理所有不认识的命令。
用户相关命令
直接看第二个字段就可以判断出命令:
case "user":
switch (line[1].toLowerCase()) {
case "list": {
// 列出当前所有用户
break;
}
case "create": {
// 创建
break;
}
case "delete": {
// 删除
break;
}
case "login": {
// 登录
break;
}
case "logout": {
// 登出
break;
}
case "chpasswd": {
// 修改密码
break;
}
default: {
System.out.println("未知命令");
break;
}
}
break;
列出用户
case "list": {
// 列出当前所有用户
var userTable = simulator.readUserTable();
var usernames = userTable.listUsername();
for (String username : usernames) {
System.out.println(username);
}
break;
}
通过文件系统读出UserTable,然后列出用户名一一打印即可。
创建用户
case "create": {
// 创建
if (currentUser.equals("root")) {
var userTable = simulator.readUserTable();
var username = "";
System.out.print("请输入用户名:");
username = scanner.nextLine();
if (username.isBlank()) {
System.out.println("用户名不可为空");
break;
}
if (userTable.get(username) != null) {
System.out.println("用户已存在");
break;
}
System.out.print("请输入密码:");
var password = scanner.nextLine();
var result = simulator.createUser(username, password);
if (result) {
System.out.println("用户创建成功");
} else {
System.out.println("创建用户失败,请重试");
}
} else {
System.out.println("请使用root账户登录后重试");
}
break;
}
创建用户要求root账户操作,所以要先检查当前登陆的用户是否为root。不是则直接进入else分支打印提示信息。
对于创建流程来说,询问用户意图创建的用户名,然后检查用户名是否为空、用户名是否已经存在。如果用户名不满足条件,直接结束当前语句的执行。一开始是使用do..while来做的,后来认为如果用户中途改变主意不想新增用户了,那么没有办法退出。因此只要不满足条件,直接打印失败信息结束命令。对于密码则没有那么多要求,如果高兴的话密码为空也不是不行。最后调用文件系统创建用户,并根据文件系统的结果打印成功或失败的信息。
删除用户
case "delete": {
// 删除
if (currentUser.equals("root")) {
var userTable = simulator.readUserTable();
var username = "";
System.out.print("请输入用户名:");
username = scanner.nextLine();
if (userTable.get(username) == null) {
System.out.println("用户不存在");
break;
}
var result = simulator.deleteUser(username);
if (result) {
// 删除成功回收打开的文件
for (String filename : queryAllOpenedFilename(username)) {
closeFile(username, filename);
}
System.out.println("用户删除成功");
} else {
System.out.println("删除用户失败,请重试");
}
} else {
System.out.println("请使用root账户登录后重试");
}
break;
}
删除用户同样要求root操作。如果用户名不存在则直接打印提示并结束命令。否则删除用户,如果删除成功,再回收被删除用户已经打开的文件,这部分内容将在后面打开或关闭文件的时候提到。
用户登录
case "login": {
// 登录
if (!currentUser.isBlank()) {
System.out.println("已登录,请先登出");
} else {
var userTable = simulator.readUserTable();
User user;
System.out.print("请输入用户名:");
var username = scanner.nextLine();
user = userTable.get(username);
if (user == null) {
System.out.println("找不到用户");
break;
}
System.out.print("请输入用户密码:");
var password = scanner.nextLine();
if (user.getPassword().equals(password)) {
currentUser = user.getUsername();
System.out.println("登陆成功");
} else {
System.out.println("密码错误,请重试");
}
}
break;
}
对于用户登录,在逻辑上已经登陆的用户不应该再次登录,所以只有已登录用户名为空的时候才允许登录。首先询问用户名,如果找不到用户则直接结束。找到了就继续询问密码,如果输入的密码和记录的密码相匹配,则成功登录。顺带一说:这里使用的equals
方法来判断相等,实践上并不安全。一般来说使用XOR做对比,如果结果全0就说明相等,它与equals
的区别在于后者会在第一个不相等的地方返回,这样攻击者可以根据运算时间的变化猜测正确的密码,而XOR则是要全部过一遍才能得出结果,从运行时间上并不能判断出是哪一位出错。但是由于这个系统偏向演示,所以只用equals
就足以了。
用户登出
case "logout": {
// 登出
if (currentUser.isBlank()) {
System.out.println("请先登录");
} else {
currentUser = "";
}
break;
}
有登录就得有登出,登出的话就得先登录。具体操作就是把当前已登录的用户名置为空。
修改密码
case "chpasswd": {
// 修改密码
if (currentUser.isBlank()) {
System.out.println("请先登录");
} else {
var userTable = simulator.readUserTable();
User user = userTable.get(currentUser);
if (user == null) {
System.out.println("未找到当前已登录用户信息,已自动登出,请重新登陆后尝试");
currentUser = "";
break;
} else {
var password = "";
System.out.print("请输入旧密码:");
password = scanner.nextLine();
if (!user.getPassword().equals(password)) {
System.out.println("旧密码错误,请重试");
break;
}
System.out.print("请输入新密码:");
password = scanner.nextLine();
var result = simulator.changePassword(user.getUsername(), password);
if (result) {
System.out.println("修改密码成功");
} else {
System.out.println("修改密码失败,请重试");
}
}
}
虽然实验要求没说要有改密码的功能,但代码都写了,不做浪费了。改密码也很简单,首先得要求用户登录,其次要验证旧密码。为了避免意料之外的事情,先要确保当前登录的用户是存在的,如果不存在直接自动登出。旧密码验证失败也直接结束命令的执行。旧密码验证成功之后,询问新密码并调用文件系统做出相应修改。
文件相关命令
文件相关的命令会有一些共同的操作,例如获取当前用户的目录文件Inode等,因此在开始指令之前,先把这些公用的东西获取出来:
case "file":
if (currentUser.isBlank()) {
System.out.println("请先登录");
} else {
var user = simulator.readUserTable().get(currentUser);
if (user == null) {
System.out.println("找不到当前登录用户的信息,已自动登出,请重新登陆后重试");
currentUser = "";
break;
}
var homeInode = simulator.readInodeTable().get(user.getHomeInodeIndex());
switch (line[1].toLowerCase()) {
case "list": {
// 列出当前所文件
break;
}
case "create": {
// 创建一个空文件
break;
}
case "delete": {
// 删除一个文件
break;
}
case "open": {
// 打开一个文件
break;
}
case "close": {
// 关闭一个文件
break;
}
case "read": {
// 读一个文件
break;
}
case "write": {
// 写一个文件
break;
}
case "cutoff": {
// 截断一个文件
break;
}
default:
System.out.println("未知命令");
break;
}
}
break;
如果用户当前还没有登陆,使用文件相关的命令时会直接提示。如果用户的Inode找不到,说明用户不存在,直接自动登出并提示用户。
之后开始正常的命令解析:
列出文件
case "list": {
// 列出当前所文件
var indexEntry = new IndexEntry();
var entryCount = homeInode.getSize() / indexEntry.byteCount();
var openedFilename = queryAllOpenedFilename(currentUser);
var inodeTable = simulator.readInodeTable();
simulator.readSomeIndexEntry(homeInode, 0, entryCount)
.forEach((f) -> {
System.out.print("文件名:" + f.getFileName());
if (openedFilename.contains(f.getFileName())) {
System.out.print(",已打开");
}
System.out.println(",大小" + inodeTable.get(f.getInodeIndex()).getSize() + "B");
});
break;
}
列出文件使用读取目录项的方法。同时为了标记已经打开的文件,还会查询该用户所有已经打开的文件名并加以标注。最后每个文件还会打印它的大小。
创建文件
case "create": {
// 创建一个空文件
var filename = "";
System.out.print("请输入文件名:");
filename = scanner.nextLine();
if (filename.isBlank()) {
System.out.println("文件名不可为空");
break;
}
if (simulator.containsFile(homeInode, filename)) {
System.out.println("文件已存在");
break;
}
var result = simulator.createFile(currentUser, filename);
if (result) {
System.out.println("创建成功");
} else {
System.out.println("创建失败");
}
break;
}
创建文件默认创建一个空白文件,不分配任何盘块。同样还是询问文件名,如果文件名已经存在或者为空,直接结束命令执行。通过检查则调用文件系统创建文件,并根据结果打印相关信息。
删除文件
case "delete": {
// 删除一个文件
var filename = "";
System.out.print("请输入文件名:");
filename = scanner.nextLine();
if (!simulator.containsFile(homeInode, filename)) {
System.out.println("文件不存在");
break;
}
if (queryOpenedFile(currentUser, filename) != null) {
System.out.println("文件已打开,请先关闭再删除");
break;
}
var result = simulator.deleteFile(currentUser, filename);
if (result) {
System.out.println("删除成功");
} else {
System.out.println("删除失败");
}
break;
}
删除文件也是一样,如果文件不存在,或者已经打开,则拒绝删除。否则调用文件系统删除文件。
打开文件
case "open": {
// 打开一个文件
var filename = "";
System.out.print("请输入文件名:");
filename = scanner.nextLine();
if (!simulator.containsFile(homeInode, filename)) {
System.out.println("文件不存在");
break;
}
if (queryOpenedFile(currentUser, filename) != null) {
System.out.println("文件已打开");
break;
}
var result = openFile(currentUser, filename);
if (result) {
System.out.println("文件打开成功");
} else {
System.out.println("文件打开失败");
}
break;
}
打开文件只允许打开已经存在且尚未打开的文件。相关工具方法如下
/**
* 打开的文件,((用户名,文件名),文件描述)
*/
private static final Map<Pair<String, String>, OpenedFile> openedFiles = new HashMap<>();
/**
* 打开文件
*/
public static boolean openFile(String username, String filename) {
var user = simulator.readUserTable().get(username);
if (user == null) {
return false;
}
var inodeTable = simulator.readInodeTable();
var homeInode = inodeTable.get(user.getHomeInodeIndex());
// 不存在的文件直接返回false
if (!simulator.containsFile(homeInode, filename)) {
return false;
}
// 如果已经打开也直接返回
if (openedFiles.containsKey(new Pair<>(username, filename))) {
return false;
}
openedFiles.put(new Pair<>(username, filename), new OpenedFile(simulator.findInodeByFilename(homeInode, filename)));
return true;
}
/**
* 关闭文件
*/
public static boolean closeFile(String username, String filename) {
// 如果没打开直接返回
if (!openedFiles.containsKey(new Pair<>(username, filename))) {
return false;
}
openedFiles.remove(new Pair<>(username, filename));
return true;
}
/**
* 通过用户名和文件名查询已打开的文件描述
*/
public static OpenedFile queryOpenedFile(String username, String filename) {
// 如果没打开直接返回
if (!openedFiles.containsKey(new Pair<>(username, filename))) {
return null;
}
return openedFiles.get(new Pair<>(username, filename));
}
/**
* 获取已经打开的文件名
*/
public static List<String> queryAllOpenedFilename(String username) {
return openedFiles.keySet().stream().filter(p -> p.getFirst().equals(username)).map(Pair::getSecond).collect(Collectors.toList());
}
打开文件的核心就是将打开的文件信息存储在内存中。打开的文件信息包括文件的Inode引用和一个当前读写指针的位置。后者将提供随机访问的特性。
关闭文件
case "close": {
// 关闭一个文件
var filename = "";
System.out.print("请输入文件名:");
filename = scanner.nextLine();
if (queryOpenedFile(currentUser, filename) == null) {
System.out.println("文件未打开");
break;
}
var result = closeFile(currentUser, filename);
if (result) {
System.out.println("文件关闭成功");
} else {
System.out.println("文件关闭失败");
}
break;
}
文件打开了就得关闭,要不然会造成内存泄漏。关闭只允许关闭已经打开的文件。操作上就是将对应的信息从内存中移除。
读文件
case "read": {
// 读一个文件
var filename = "";
System.out.print("请输入文件名:");
filename = scanner.nextLine();
var openedFile = queryOpenedFile(currentUser, filename);
if (openedFile == null) {
System.out.println("文件未打开");
break;
}
var delta = 0L;
System.out.println("当前文件描述符指针在" + openedFile.getPos());
System.out.println("文件大小" + openedFile.getInode().getSize() + "B");
if (openedFile.getPos() >= openedFile.getInode().getSize()) {
System.out.println("已到达文件尾。");
}
System.out.print("请输入相对偏移量:");
delta = scanner.nextLong();
scanner.nextLine();
// 更新偏移量
openedFile.setPos(openedFile.getPos() + delta);
System.out.println("移动后指针:" + openedFile.getPos());
var length = 0L;
System.out.print("请输入要读取的长度:");
length = scanner.nextLong();
scanner.nextLine();
if (length <= 0) {
System.out.println("长度必须大于0");
}
try {
var bytes = simulator.readFile(openedFile.getInode(), openedFile.getPos(), length);
System.out.println(new String(bytes, StandardCharsets.UTF_8));
openedFile.setPos(openedFile.getPos() + bytes.length);
System.out.println("读取后文件指针:" + openedFile.getPos());
} catch (Exception e) {
System.out.println("读取失败:" + e.getMessage());
}
break;
}
读文件要求只能读打开的文件,原因是未打开的文件没有读写指针,不知道从哪里开始读。读取的时候会根据提供的文件名寻找已打开的文件信息,并提示用户操作读写指针进行随机读写。
由于这个系统很简陋,所以唯一能读写的东西就是字符。在读取部分简单的包了个异常处理,因为文件系统那里会对传入参数进行检查,如果检查失败直接抛出异常崩溃JVM,这个异常处理要做的就是拦截异常,将其信息打印给用户,然后结束指令。
写文件
case "write": {
// 写一个文件
var filename = "";
System.out.print("请输入文件名:");
filename = scanner.nextLine();
var openedFile = queryOpenedFile(currentUser, filename);
if (openedFile == null) {
System.out.println("文件未打开");
break;
}
var delta = 0L;
System.out.println("当前文件描述符指针在" + openedFile.getPos());
System.out.println("文件大小" + openedFile.getInode().getSize() + "B");
if (openedFile.getPos() >= openedFile.getInode().getSize()) {
System.out.println("已到达文件尾");
}
System.out.print("请输入相对偏移量:");
delta = scanner.nextLong();
scanner.nextLine();
// 更新偏移量
openedFile.setPos(openedFile.getPos() + delta);
System.out.println("移动后指针:" + openedFile.getPos());
var length = 0L;
System.out.println("请在下方输入要写入的内容,单独一行'$EOF'表示结束:");
// 打印一个字符避免退格时回到已经写入的上一行
System.out.print(">");
var inputLine = scanner.nextLine();
try {
while (!inputLine.equals("$EOF")) {
if (length != 0) {
// 不是第一行,向前面行追加回车
inputLine = String.format("\n%s", inputLine);
}
var bytes = inputLine.getBytes(StandardCharsets.UTF_8);
var result = simulator.writeFile(openedFile.getInode(), openedFile.getPos() + length, bytes);
if (!result) {
System.out.println("写入失败!");
break;
}
// 更新长度
length += bytes.length;
// 读下一行
System.out.print(">");
inputLine = scanner.nextLine();
}
} catch (Exception e) {
System.out.println("写入失败:" + e.getMessage());
}
System.out.println("已写入" + length + "B");
openedFile.setPos(openedFile.getPos() + length);
System.out.println("当前文件指针:" + openedFile.getPos());
break;
}
写文件的交互与读文件的类似。写入支持多行,单行的$EOF
表示结束。如果中途写入失败,通常是指针越界,文件系统会直接抛出异常。这个异常同样会被异常处理拦截并转换成错误信息。
截断文件
case "cutoff": {
// 截断一个文件
var filename = "";
System.out.print("请输入文件名:");
filename = scanner.nextLine();
var openedFile = queryOpenedFile(currentUser, filename);
if (openedFile == null) {
System.out.println("文件未打开");
break;
}
System.out.println("文件大小" + openedFile.getInode().getSize() + "B");
if (openedFile.getPos() >= openedFile.getInode().getSize()) {
System.out.println("已到达文件尾");
}
var newSize = 0L;
System.out.print("请输入新的大小:");
newSize = scanner.nextLong();
scanner.nextLine();
if (newSize > openedFile.getInode().getSize()) {
System.out.println("新大小不能超过原来的大小");
break;
}
simulator.cutoffFile(openedFile.getInode(), newSize);
System.out.println("截断文件成功");
break;
}
就是询问文件的新大小,并调用文件系统的相关功能。
效果
未检测到数据文件,开始初始化。
已创建默认用户:root,密码:password
nobody@FSS> user login
请输入用户名:root
请输入用户密码:123
密码错误,请重试
nobody@FSS> user login
请输入用户名:root
请输入用户密码:password
登陆成功
root@FSS> file list
root@FSS> file create
请输入文件名:123
创建成功
root@FSS> file open
请输入文件名:123
文件打开成功
root@FSS> file list
文件名:123,已打开,大小0B
root@FSS> file write
请输入文件名:123
当前文件描述符指针在0
文件大小0B
已到达文件尾
请输入相对偏移量:1
移动后指针:1
请在下方输入要写入的内容,单独一行'$EOF'表示结束:
>123
写入失败:Offset must not bigger than size.
已写入0B
当前文件指针:1
root@FSS> file write
请输入文件名:123
当前文件描述符指针在1
文件大小0B
已到达文件尾
请输入相对偏移量:-1
移动后指针:0
请在下方输入要写入的内容,单独一行'$EOF'表示结束:
>1234567890
>abc
>
>$EOF
已写入15B
当前文件指针:15
root@FSS> file read
请输入文件名:123
当前文件描述符指针在15
文件大小15B
已到达文件尾。
请输入相对偏移量:-15
移动后指针:0
请输入要读取的长度:15
1234567890
abc
读取后文件指针:15
root@FSS> file close
请输入文件名:1234
文件未打开
root@FSS> file close
请输入文件名:123
文件关闭成功
root@FSS> exit
Bye!
这个效果还不错,至少我简单的测试了一下,没有意料之外的报错。后来请同学以「让这个系统崩溃」为目标可劲儿的尝试,最终看起来代码没有比较严重的bug,也没有与预期不相符的行为。
总结
今天程序顺利通过验机,这个系列也就此告一段落。这个实验在11月3号开始写,到12月3号写完,正好一个月。实话说这个程序虽然实用性不强,但是其中用到的许多设计想法都是我未曾在实用程序中实践过的。例如用户输入的条件不满足直接抛异常崩JVM,我大概永远不敢在重要的程序上这样做,通常有一个default会有用很多。另外一个例子就是整个Wrapped
的构造,平时写代码的时候别说字节数组了,Serializable
都很少用,直接一个Gson或者Jackson,反射就把这事儿做了,我也无须关心底层是怎么实现的(当然了,用了Gson打完包程序上百MB就是另一个故事了)。以及直接使用RandomAccessFile
像文件系统那样将数据定位到字节的精度去操作,这种深度还是我头一次用Java做。
无论如何,这个项目很顺利的完成了。本文作为这个系列的最后一篇文章,实现了人机交互界面,并最终让这个系统运行了起来。
附录:完整代码
package info.skyblond.os.exp.experiment3;
import info.skyblond.os.exp.experiment3.model.IndexEntry;
import info.skyblond.os.exp.experiment3.model.OpenedFile;
import info.skyblond.os.exp.experiment3.model.User;
import info.skyblond.os.exp.utils.Pair;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.stream.Collectors;
/**
* 文件系统模拟器的主入口
* 模仿一个简单的操作系统,利用文件系统进行用户鉴权、文件操作
*/
public class Exp3 {
private static final Simulator simulator = Simulator.getInstance();
/**
* 打开的文件,((用户名,文件名),文件描述)
*/
private static final Map<Pair<String, String>, OpenedFile> openedFiles = new HashMap<>();
/**
* 打开文件
*/
public static boolean openFile(String username, String filename) {
var user = simulator.readUserTable().get(username);
if (user == null) {
return false;
}
var inodeTable = simulator.readInodeTable();
var homeInode = inodeTable.get(user.getHomeInodeIndex());
// 不存在的文件直接返回false
if (!simulator.containsFile(homeInode, filename)) {
return false;
}
// 如果已经打开也直接返回
if (openedFiles.containsKey(new Pair<>(username, filename))) {
return false;
}
openedFiles.put(new Pair<>(username, filename), new OpenedFile(simulator.findInodeByFilename(homeInode, filename)));
return true;
}
/**
* 关闭文件
*/
public static boolean closeFile(String username, String filename) {
// 如果没打开直接返回
if (!openedFiles.containsKey(new Pair<>(username, filename))) {
return false;
}
openedFiles.remove(new Pair<>(username, filename));
return true;
}
/**
* 通过用户名和文件名查询已打开的文件描述
*/
public static OpenedFile queryOpenedFile(String username, String filename) {
// 如果没打开直接返回
if (!openedFiles.containsKey(new Pair<>(username, filename))) {
return null;
}
return openedFiles.get(new Pair<>(username, filename));
}
/**
* 获取已经打开的文件名
*/
public static List<String> queryAllOpenedFilename(String username) {
return openedFiles.keySet().stream().filter(p -> p.getFirst().equals(username)).map(Pair::getSecond).collect(Collectors.toList());
}
public static String getPromptLine(String username, String hostname) {
if (username.isBlank()) {
return "nobody@" + hostname + "> ";
} else {
return username + "@" + hostname + "> ";
}
}
public static void main(String[] args) {
var dataFile = new File("./data.bin");
simulator.setDataFile(dataFile);
if (!dataFile.exists()) {
System.out.println("未检测到数据文件,开始初始化。");
simulator.initDataFile();
System.out.println("已创建默认用户:root,密码:" + Simulator.ROOT_DEFAULT_PASSWORD);
}
var hostname = "FSS";
// var currentUser = "root"; // 默认root用户,调试用
var currentUser = "";
// prompt
System.out.print(getPromptLine(currentUser, hostname));
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String[] line = scanner.nextLine().split(" ");
if (line[0].equalsIgnoreCase("help")) {
// 帮助
System.out.println("user list \t\t 列出所有已知用户");
System.out.println("user create \t 创建新用户(仅限root)");
System.out.println("user delete \t 删除已有用户(仅限root)");
System.out.println("user login \t\t 登录(仅未登录时)");
System.out.println("user logout \t 登出(仅登录时)");
System.out.println("user chpasswd \t 修改密码(仅登录时)");
System.out.println("-".repeat(50));
System.out.println("file list \t\t 列出文件(仅登录时)");
System.out.println("file create \t 创建文件(仅登录时)");
System.out.println("file delete \t 删除文件(仅登录时)");
System.out.println("file open \t\t 打开文件(仅登录时)");
System.out.println("file close \t\t 关闭文件(仅登录时)");
System.out.println("file read \t\t 读文件(仅登录时)");
System.out.println("file write \t\t 写文件(仅登录时)");
System.out.println("file cutoff \t 截断文件(仅登录时)");
System.out.println("-".repeat(50));
System.out.println("help \t\t\t 打印帮助信息");
System.out.println("exit \t\t\t 退出系统");
} else if (line[0].equalsIgnoreCase("exit")) {
// 退出
System.out.println("Bye!");
return;
} else if (line.length == 2) {
switch (line[0].toLowerCase()) {
case "user":
switch (line[1].toLowerCase()) {
case "list": {
// 列出当前所有用户
var userTable = simulator.readUserTable();
var usernames = userTable.listUsername();
for (String username : usernames) {
System.out.println(username);
}
break;
}
case "create": {
// 创建
if (currentUser.equals("root")) {
var userTable = simulator.readUserTable();
var username = "";
System.out.print("请输入用户名:");
username = scanner.nextLine();
if (username.isBlank()) {
System.out.println("用户名不可为空");
break;
}
if (userTable.get(username) != null) {
System.out.println("用户已存在");
break;
}
System.out.print("请输入密码:");
var password = scanner.nextLine();
var result = simulator.createUser(username, password);
if (result) {
System.out.println("用户创建成功");
} else {
System.out.println("创建用户失败,请重试");
}
} else {
System.out.println("请使用root账户登录后重试");
}
break;
}
case "delete": {
// 删除
if (currentUser.equals("root")) {
var userTable = simulator.readUserTable();
var username = "";
System.out.print("请输入用户名:");
username = scanner.nextLine();
if (userTable.get(username) == null) {
System.out.println("用户不存在");
break;
}
var result = simulator.deleteUser(username);
if (result) {
// 删除成功回收打开的文件
for (String filename : queryAllOpenedFilename(username)) {
closeFile(username, filename);
}
System.out.println("用户删除成功");
} else {
System.out.println("删除用户失败,请重试");
}
} else {
System.out.println("请使用root账户登录后重试");
}
break;
}
case "login": {
// 登录
if (!currentUser.isBlank()) {
System.out.println("已登录,请先登出");
} else {
var userTable = simulator.readUserTable();
User user;
System.out.print("请输入用户名:");
var username = scanner.nextLine();
user = userTable.get(username);
if (user == null) {
System.out.println("找不到用户");
break;
}
System.out.print("请输入用户密码:");
var password = scanner.nextLine();
if (user.getPassword().equals(password)) {
currentUser = user.getUsername();
System.out.println("登陆成功");
} else {
System.out.println("密码错误,请重试");
}
}
break;
}
case "logout": {
// 登出
if (currentUser.isBlank()) {
System.out.println("请先登录");
} else {
currentUser = "";
}
break;
}
case "chpasswd": {
// 修改密码
if (currentUser.isBlank()) {
System.out.println("请先登录");
} else {
var userTable = simulator.readUserTable();
User user = userTable.get(currentUser);
if (user == null) {
System.out.println("未找到当前已登录用户信息,已自动登出,请重新登陆后尝试");
currentUser = "";
break;
} else {
var password = "";
System.out.print("请输入旧密码:");
password = scanner.nextLine();
if (!user.getPassword().equals(password)) {
System.out.println("旧密码错误,请重试");
break;
}
System.out.print("请输入新密码:");
password = scanner.nextLine();
var result = simulator.changePassword(user.getUsername(), password);
if (result) {
System.out.println("修改密码成功");
} else {
System.out.println("修改密码失败,请重试");
}
}
}
break;
}
default: {
System.out.println("未知命令");
break;
}
}
break;
case "file":
if (currentUser.isBlank()) {
System.out.println("请先登录");
} else {
var user = simulator.readUserTable().get(currentUser);
if (user == null) {
System.out.println("找不到当前登录用户的信息,已自动登出,请重新登陆后重试");
currentUser = "";
break;
}
var homeInode = simulator.readInodeTable().get(user.getHomeInodeIndex());
switch (line[1].toLowerCase()) {
case "list": {
// 列出当前所文件
var indexEntry = new IndexEntry();
var entryCount = homeInode.getSize() / indexEntry.byteCount();
var openedFilename = queryAllOpenedFilename(currentUser);
var inodeTable = simulator.readInodeTable();
simulator.readSomeIndexEntry(homeInode, 0, entryCount)
.forEach((f) -> {
System.out.print("文件名:" + f.getFileName());
if (openedFilename.contains(f.getFileName())) {
System.out.print(",已打开");
}
System.out.println(",大小" + inodeTable.get(f.getInodeIndex()).getSize() + "B");
});
break;
}
case "create": {
// 创建一个空文件
var filename = "";
System.out.print("请输入文件名:");
filename = scanner.nextLine();
if (filename.isBlank()) {
System.out.println("文件名不可为空");
break;
}
if (simulator.containsFile(homeInode, filename)) {
System.out.println("文件已存在");
break;
}
var result = simulator.createFile(currentUser, filename);
if (result) {
System.out.println("创建成功");
} else {
System.out.println("创建失败");
}
break;
}
case "delete": {
// 删除一个文件
var filename = "";
System.out.print("请输入文件名:");
filename = scanner.nextLine();
if (!simulator.containsFile(homeInode, filename)) {
System.out.println("文件不存在");
break;
}
if (queryOpenedFile(currentUser, filename) != null) {
System.out.println("文件已打开,请先关闭再删除");
break;
}
var result = simulator.deleteFile(currentUser, filename);
if (result) {
System.out.println("删除成功");
} else {
System.out.println("删除失败");
}
break;
}
case "open": {
// 打开一个文件
var filename = "";
System.out.print("请输入文件名:");
filename = scanner.nextLine();
if (!simulator.containsFile(homeInode, filename)) {
System.out.println("文件不存在");
break;
}
if (queryOpenedFile(currentUser, filename) != null) {
System.out.println("文件已打开");
break;
}
var result = openFile(currentUser, filename);
if (result) {
System.out.println("文件打开成功");
} else {
System.out.println("文件打开失败");
}
break;
}
case "close": {
// 关闭一个文件
var filename = "";
System.out.print("请输入文件名:");
filename = scanner.nextLine();
if (queryOpenedFile(currentUser, filename) == null) {
System.out.println("文件未打开");
break;
}
var result = closeFile(currentUser, filename);
if (result) {
System.out.println("文件关闭成功");
} else {
System.out.println("文件关闭失败");
}
break;
}
case "read": {
// 读一个文件
var filename = "";
System.out.print("请输入文件名:");
filename = scanner.nextLine();
var openedFile = queryOpenedFile(currentUser, filename);
if (openedFile == null) {
System.out.println("文件未打开");
break;
}
var delta = 0L;
System.out.println("当前文件描述符指针在" + openedFile.getPos());
System.out.println("文件大小" + openedFile.getInode().getSize() + "B");
if (openedFile.getPos() >= openedFile.getInode().getSize()) {
System.out.println("已到达文件尾。");
}
System.out.print("请输入相对偏移量:");
delta = scanner.nextLong();
scanner.nextLine();
// 更新偏移量
openedFile.setPos(openedFile.getPos() + delta);
System.out.println("移动后指针:" + openedFile.getPos());
var length = 0L;
System.out.print("请输入要读取的长度:");
length = scanner.nextLong();
scanner.nextLine();
if (length <= 0) {
System.out.println("长度必须大于0");
}
try {
var bytes = simulator.readFile(openedFile.getInode(), openedFile.getPos(), length);
System.out.println(new String(bytes, StandardCharsets.UTF_8));
openedFile.setPos(openedFile.getPos() + bytes.length);
System.out.println("读取后文件指针:" + openedFile.getPos());
} catch (Exception e) {
System.out.println("读取失败:" + e.getMessage());
}
break;
}
case "write": {
// 写一个文件
var filename = "";
System.out.print("请输入文件名:");
filename = scanner.nextLine();
var openedFile = queryOpenedFile(currentUser, filename);
if (openedFile == null) {
System.out.println("文件未打开");
break;
}
var delta = 0L;
System.out.println("当前文件描述符指针在" + openedFile.getPos());
System.out.println("文件大小" + openedFile.getInode().getSize() + "B");
if (openedFile.getPos() >= openedFile.getInode().getSize()) {
System.out.println("已到达文件尾");
}
System.out.print("请输入相对偏移量:");
delta = scanner.nextLong();
scanner.nextLine();
// 更新偏移量
openedFile.setPos(openedFile.getPos() + delta);
System.out.println("移动后指针:" + openedFile.getPos());
var length = 0L;
System.out.println("请在下方输入要写入的内容,单独一行'$EOF'表示结束:");
// 打印一个字符避免退格时回到已经写入的上一行
System.out.print(">");
var inputLine = scanner.nextLine();
try {
while (!inputLine.equals("$EOF")) {
if (length != 0) {
// 不是第一行,向前面行追加回车
inputLine = String.format("\n%s", inputLine);
}
var bytes = inputLine.getBytes(StandardCharsets.UTF_8);
var result = simulator.writeFile(openedFile.getInode(), openedFile.getPos() + length, bytes);
if (!result) {
System.out.println("写入失败!");
break;
}
// 更新长度
length += bytes.length;
// 读下一行
System.out.print(">");
inputLine = scanner.nextLine();
}
} catch (Exception e) {
System.out.println("写入失败:" + e.getMessage());
}
System.out.println("已写入" + length + "B");
openedFile.setPos(openedFile.getPos() + length);
System.out.println("当前文件指针:" + openedFile.getPos());
break;
}
case "cutoff": {
// 截断一个文件
var filename = "";
System.out.print("请输入文件名:");
filename = scanner.nextLine();
var openedFile = queryOpenedFile(currentUser, filename);
if (openedFile == null) {
System.out.println("文件未打开");
break;
}
System.out.println("文件大小" + openedFile.getInode().getSize() + "B");
if (openedFile.getPos() >= openedFile.getInode().getSize()) {
System.out.println("已到达文件尾");
}
var newSize = 0L;
System.out.print("请输入新的大小:");
newSize = scanner.nextLong();
scanner.nextLine();
if (newSize > openedFile.getInode().getSize()) {
System.out.println("新大小不能超过原来的大小");
break;
}
simulator.cutoffFile(openedFile.getInode(), newSize);
System.out.println("截断文件成功");
break;
}
default:
System.out.println("未知命令");
break;
}
}
break;
default:
System.out.println("未知命令");
break;
}
} else {
System.out.println("命令格式有误");
}
System.out.print(getPromptLine(currentUser, hostname));
}
}
}
-全文完-
【代码札记】Java模拟二级文件系统 终 由 天空 Blond 采用 知识共享 署名 - 非商业性使用 - 相同方式共享 4.0 国际 许可协议进行许可。
本许可协议授权之外的使用权限可以从 https://skyblond.info/about.html 处获得。