基于java语言模拟实现本地的快速文件搜索神器。首先windows下的桌面搜索软件是遍历式查找,速度较慢。如果想要提高速度,我采用的是通过连接数据库,在查找之前将文件的目录信息经过全盘符遍历存入数据库中,待查找的时候便从数据库中查找即可。具体实现程序见访达实现具体代码
所有首先我们在启动该程序时,首先要完成和数据库的连接工作,为用户提供相应的 建立索引、帮助提醒、模糊文件名搜索、准确文件类型搜索、退出操作。
和数据库建立连接
利用阿里提供的数据库连接池Druid,new DruidDataSource().setDriverClassName(“h2”) -setUrl
public static DataSource dataSource(){
if(dataSource == null){
synchronized (DataSourceFactory.class){
if(dataSource == null){
dataSource = new DruidDataSource();
dataSource.setDriverClassName("org.h2.Driver");
//采用h2的嵌入式数据库,数据库以本地文件方式存储
//获取当前工厂路径
dataSource.setUrl("jdbc:h2:"+EverythingPlusConfig.getInstance().getH2IndexPath());
//Druid数据库连接池的可配置参数
dataSource.setValidationQuery("select now()");
//Or dataSource.setTestWhileIdle(false);
}
}
}
return dataSource;
}
执行初始化数据库命令
我将详细的数据库初始化建表语句存到资源文件中。然后通过IO流读取,执行。jdbc编程操作流程:首先加载数据库驱动,然后建立数据库连接,创建数据操作命令,执行数据操作,拿到返回数集操作,然后关闭结果集,关闭命令,关闭连接。
private void initComponent(){
//数据源对象
DataSource dataSource = DataSourceFactory.dataSource();
//检查数据库
// initOrResetDatabase();
//业务层对象
FileIndexDao fileIndexDao = new FileIndexDaoImpl(dataSource);
this.fileSearch = new FileSearchImpl(fileIndexDao);
this.fileScan = new FileScanImpl();
// this.fileScan.interceptor(new FilePrintInterceptor());//调试需要,发布不需要
this.fileScan.interceptor(new FileIndexInterceptor(fileIndexDao));
this.thingClearInterceptor = new ThingClearInterceptor(fileIndexDao);
this.backgroundClearThread = new Thread(this.thingClearInterceptor);
this.backgroundClearThread.setName("Thread-Thing-Clear");
this.backgroundClearThread.setDaemon(true);
this.fileWatch = new FileWatchImpl(fileIndexDao);
}
public static void initDatabase(){
//获取数据源,获取SQL语句,获取数据库连接和命令执行sql
DataSource dataSource = DataSourceFactory.dataSource();
//不采取读取绝对路径下文件。读取classpath路径下的文件
try(InputStream in = DataSourceFactory.class.getClassLoader().getResourceAsStream("everything_plus.sql");){
if (in == null) {
throw new RuntimeException("Not read init database script please check in");
}
StringBuilder sqlBuilder = new StringBuilder();
try(BufferedReader reader = new BufferedReader(new InputStreamReader(in));){
String line ;
while ((line=reader.readLine())!=null){
if(!line.startsWith("--")){
sqlBuilder.append(line);
}
}
} catch (IOException e) {
e.printStackTrace();
}
String sql = sqlBuilder.toString();
//1.获取数据库连接
Connection connection = dataSource.getConnection();
//2.创建命令
PreparedStatement statement =connection.prepareStatement(sql);
//3.执行命令
statement.execute();
connection.close();
statement.close();
} catch (SQLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
获得根盘符加入为课搜索路径便于搜索
//1.获取文件系统
FileSystem fileSystem = FileSystems.getDefault();
//2.遍历目录
Iterable<Path> iterable = fileSystem.getRootDirectories();
iterable.forEach(new Consumer<Path>() {
public void accept(Path path) {
EverythingPlusConfig.config.getIncludePath().add(path.toString());
}
});
可以由用户启动时或者默认设置不搜索路径
由于windows或者linux下不搜索的路径是不同的,所以要获取当前系统名称,然后根据系统选择要排除的路径
//排除目录
String osName = System.getProperty("os.name");
if(osName.startsWith("Windows")){
config.getExcludePath().add("C:\\Windows");
config.getExcludePath().add("C:\\Program Files(x86)");
config.getExcludePath().add("C:\\Program Files");
config.getExcludePath().add("C:\\ProgramData");
}else{
config.getExcludePath().add("/tmp");
config.getExcludePath().add("/etc");
config.getExcludePath().add("/root");
}
建立索引过程,遍历盘符,将众文件加入到数据库中
由于建立索引是一个比较复杂的过程,也是一个比较耗费事件的过程,所以应采用多线程另外启动一个线程,经行该操作。由于该操作就是一个遍历数据将文件路径一条条的加入数据库的过程,所以在进行之前我们必须要初始化数据库,保证数据的不重复性。而且由于启动多线程,我们不能够一个两个线程同时写入操作一个数据库,所以我使用了flag标志位来避免同一时间的多次创建索引。
public void buildIndex(){
if(flag == false) {
flag =true;
initOrResetDatabase();
Set<String> directories = EverythingPlusConfig.getInstance().getIncludePath();
if (this.executorService == null) {
this.executorService = Executors.newFixedThreadPool(directories.size(), new ThreadFactory() {
private final AtomicInteger threadId = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("Thread-Scan-" + threadId.getAndIncrement());
return thread;
}
});
}
CountDownLatch countDownLatch = new CountDownLatch(directories.size());
for (String path : directories) {
this.executorService.submit(() -> {
EverythingPlusManger.this.fileScan.index(path);
//当前任务完成 countdown 值-1
countDownLatch.countDown();
});
}
//阻塞直到任务完成
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Bulid index complete");
flag = false;
}else {
System.out.println("you can not repeat index ");
}
}
由于我们支持排除路径,但是从根盘符等可执行路径下去可能会遇到不支持搜索的路径。所以我们有一个路径排除的过程。
public void index(String path) {
File file = new File(path);
if(file.isFile()){
if(config.getExcludePath().contains(file.getParent())|| this.config.getExcludePath().contains(file.getName())) {
return;
} else{
}
}else{
File[] files = file.listFiles();
if(files!=null){
for(File f:files){
index(f.getAbsolutePath());
}
}
}
//file ->thing 写入
for(FileInterceptor interceptor :this.interceptors){
interceptor.apply(file);
}
}
然后将路径转换为存储该文件路径的文件的信息节点才能够存入数据库
public final class FileConvertThing {
public static Thing convert(File file){
Thing thing = new Thing();
thing.setName(file.getName());
thing.setPath(file.getAbsolutePath());
thing.setDepth(computerFileDepth(file));
thing.setFileType(computerFileType(file));
return thing;
}
private static int computerFileDepth(File file){
int depth;
String[] segments = file.getAbsolutePath().split("\\\\");
depth = segments.length;
return depth;
}
private static FileType computerFileType(File file){
if(file.isDirectory()){
return FileType.OTHER;
}
String fileName = file.getName();
int index = fileName.lastIndexOf(".");
if(index!=-1 && index <fileName.length() -1 ){
String extend = file.getName().substring(index+1);
return FileType.lookup(extend);
}else {
return FileType.OTHER;
}
}
}
重点在文件类型的转换
public enum FileType {
IMG(new String[]{"png", "jpeg", "jpe", "gif"}),
DOC(new String[]{"doc", "docx", "pdf", "ppt", "txt","pptx"}),
BIN(new String[]{"exe", "sh", "jar", "msi"}),
ARCHIVDE("zip","rar"),
OTHER(new String[]{"*"});
private Set<String> extend = new HashSet<>();
FileType(String ... extend){
this.extend.addAll(Arrays.asList(extend));
}
public static FileType lookup(String extend){
for(FileType fileType:FileType.values()){
if(fileType.extend.contains(extend)){
return fileType;
}
}
return OTHER;
}
public static FileType lookBYName(String name){
//根据类型名获取文件类型对象
for(FileType fileType:FileType.values()){
if(fileType.name().equals(name)){
return fileType;
}
}
return OTHER;
}
}
搜索
搜索的要点就是如何根据用户的要求拼接成一个完成了查询语句。首先我们支持模糊匹配,这里就要用到“% xxx %”,我们还支持文本类型的准确搜索,所以我们要将类型转为全大写,然后去匹配。判断属于那种类型。搜索出那种准确类型的文件。
List<Thing> things = new ArrayList<>();
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try{
//1.获取数据库连接
connection = dataSource.getConnection();
//2.准备数据库语言
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append("select name,path,depth,file_type from FILE_INDEX ");
sqlBuilder.append(" where ");
sqlBuilder.append(" name like '%").append(condition.getName()).append("%' ");
if(condition.getFileType()!=null){
sqlBuilder.append(" and file_type = '")
.append(condition.getFileType().toUpperCase()).append("' ");
}
sqlBuilder.append(" order by depth ")
.append(condition.getOrderByAsc().equals(true) ? " asc " : " desc ");
sqlBuilder.append(" limit ")
.append(condition.getLimit())
.append(" offset 0 ");
System.out.println(sqlBuilder);
statement =connection.prepareStatement(sqlBuilder.toString());
//name:like , filetype : = ,limit :limit offset ,orderbyAsc:order by
resultSet = statement.executeQuery();
while (resultSet.next()){
Thing thing = new Thing();
thing.setName(resultSet.getString("name"));
thing.setPath(resultSet.getString("path"));
thing.setDepth(resultSet.getInt("depth"));
String fileType =resultSet.getString("file_type");
thing.setFileType(FileType.lookBYName(fileType));
things.add(thing);
}
}catch (SQLException e){
e.printStackTrace();
}
finally {
releaseResource(resultSet,statement,connection);
return things;
}
删除过期文件
我们搜索出的文件一定还存在吗?不一定。当我们判断其不存在的时候,在不把这条信息返回给用户的同时,还要删除数据库中的数据。我们通过先将数据加入到队列中,然后在早期启动着清理线程监听着经过一个短间隔后一起删除。
public List<Thing> search(Condition condition){
return this.fileSearch.search(condition).stream().filter(thing -> {
String path = thing.getPath();
File f = new File(path);
boolean flag = f.exists();
if(!flag){
thingClearInterceptor.apply(thing);
}
return flag;
}).collect(Collectors.toList());
}
....
public void run(){
while(true){
Thing thing = this.queue.poll();
if(thing!=null){
fileIndexDao.delete(thing);
}
//TODO批量删除
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
文件监听
启动文件监听线程,File monitor 随时监听系统中是否有文件的创建,修改,删除。等
首先加入包
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
然后自定义类实现FileWatch、FileAlterationListener接口。然后继续监听
public void monitor(HandlePath handlePath) {
//监控includepath是一组集合
for(String path:handlePath.getIncludePath()){
FileAlterationObserver observer = new FileAlterationObserver(path, pathname -> {
String currentPath = pathname.getAbsolutePath();
for(String excludePath:handlePath.getExcludePath()){
if(excludePath.startsWith(currentPath)){
return false;
}
}
return true;
});
observer.addListener(this);
this.monitor.addObserver(observer);
}
}
//启动文件系统监听
public void startFileSystemMonitor(){
EverythingPlusConfig config = EverythingPlusConfig.getInstance();
HandlePath handlePath = new HandlePath();
handlePath.setIncludePath(config.getIncludePath());
handlePath.setExcludePath(config.getExcludePath());
this.fileWatch.monitor(handlePath);
// System.out.println("文件系统监控启动");
new Thread(() -> {
// System.out.println("文件系统监控启动");
fileWatch.start();
}).start();
}