基于[提交](f853f03bac)修改的版本
This commit is contained in:
parent
5e2fbfe1f9
commit
63790e4c51
|
|
@ -5,6 +5,9 @@
|
|||
<groovy codeStyle="LEGACY" />
|
||||
</component>
|
||||
<component name="EntryPointsManager">
|
||||
<list size="1">
|
||||
<item index="0" class="java.lang.String" itemvalue="javafx.fxml.FXML" />
|
||||
</list>
|
||||
<writeAnnotations>
|
||||
<writeAnnotation name="lombok.Getter" />
|
||||
</writeAnnotations>
|
||||
|
|
|
|||
146
build.gradle
146
build.gradle
|
|
@ -4,12 +4,16 @@ buildscript {
|
|||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'io.franzbecker.gradle-lombok' version '3.0.0'
|
||||
id 'application'
|
||||
id 'org.openjfx.javafxplugin' version '0.1.0'
|
||||
id 'org.beryx.jlink' version '2.26.0'
|
||||
// 不再使用模块插件,移除 org.javamodularity.moduleplugin
|
||||
// id 'org.javamodularity.moduleplugin' version '1.8.12'
|
||||
// 你可以根据需要保留 jlink 插件,但推荐取消模块化后暂时不用它
|
||||
id 'org.beryx.jlink' version '2.26.0' apply false
|
||||
}
|
||||
|
||||
group = project_group
|
||||
|
|
@ -23,102 +27,122 @@ ext {
|
|||
junitVersion = '5.10.0'
|
||||
}
|
||||
|
||||
// 指定 JDK 版本
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
}
|
||||
}
|
||||
|
||||
sourceCompatibility = '17'
|
||||
targetCompatibility = '17'
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.release = 17 // 明确指定Java版本
|
||||
options.release = 17
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
// 平台判定,自动添加 JavaFX 依赖的 classifier
|
||||
def osName = org.gradle.internal.os.OperatingSystem.current()
|
||||
def javafxPlatform
|
||||
if (osName.isWindows()) {
|
||||
javafxPlatform = 'win'
|
||||
} else if (osName.isMacOsX()) {
|
||||
javafxPlatform = 'mac'
|
||||
} else if (osName.isLinux()) {
|
||||
javafxPlatform = 'linux'
|
||||
} else {
|
||||
throw new GradleException("Unsupported OS for JavaFX: $osName")
|
||||
}
|
||||
|
||||
def javafxVersion = '17.0.6'
|
||||
|
||||
dependencies {
|
||||
// JavaFX 必须带平台后缀,classpath 模式必须
|
||||
implementation "org.openjfx:javafx-controls:$javafxVersion:$javafxPlatform"
|
||||
implementation "org.openjfx:javafx-fxml:$javafxVersion:$javafxPlatform"
|
||||
|
||||
// 你项目的其他依赖(老库走classpath)
|
||||
implementation 'ch.qos.logback:logback-classic:1.5.6'
|
||||
implementation 'org.apache.logging.log4j:log4j-slf4j2-impl:2.20.0'
|
||||
implementation 'commons-cli:commons-cli:1.9.0'
|
||||
implementation 'com.alibaba:easyexcel:4.0.3'
|
||||
implementation 'org.apache.pdfbox:pdfbox:3.0.5'
|
||||
implementation 'com.github.albfernandez:javadbf:1.14.1'
|
||||
implementation 'org.apache.poi:poi-ooxml:5.4.1'
|
||||
implementation 'com.intellij:annotations:12.0'
|
||||
|
||||
compileOnly 'org.projectlombok:lombok:1.18.38'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.38'
|
||||
|
||||
testCompileOnly 'org.projectlombok:lombok:1.18.38'
|
||||
testAnnotationProcessor 'org.projectlombok:lombok:1.18.38'
|
||||
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
||||
}
|
||||
|
||||
application {
|
||||
mainModule = 'top.r3944realms.docchecktoolrefactored'
|
||||
// 取消模块化,去掉 mainModule
|
||||
mainClass = 'top.r3944realms.docchecktoolrefactored.Main'
|
||||
}
|
||||
|
||||
javafx {
|
||||
version = '17.0.6'
|
||||
version = javafxVersion
|
||||
// 这里的 modules 只为插件识别,实际classpath方式运行时不起作用
|
||||
modules = ['javafx.controls', 'javafx.fxml']
|
||||
}
|
||||
|
||||
processResources {
|
||||
// exclude 'logback.xml'
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Logback classic (included slf4j &)
|
||||
implementation 'ch.qos.logback:logback-classic:1.5.6'
|
||||
implementation 'org.apache.logging.log4j:log4j-slf4j2-impl:2.20.0'
|
||||
|
||||
// Lombok
|
||||
compileOnly("org.projectlombok:lombok:1.18.38")
|
||||
annotationProcessor("org.projectlombok:lombok:1.18.38")
|
||||
|
||||
testCompileOnly("org.projectlombok:lombok:1.18.38")
|
||||
testAnnotationProcessor("org.projectlombok:lombok:1.18.38")
|
||||
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}")
|
||||
}
|
||||
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
configurations.configureEach {
|
||||
exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl'
|
||||
}
|
||||
}
|
||||
// 可选:添加 testJar 任务
|
||||
tasks.register('testJar', Jar) {
|
||||
from sourceSets.test.output
|
||||
}
|
||||
|
||||
// 创建日志目录任务
|
||||
tasks.register('createLogDir') {
|
||||
doLast {
|
||||
mkdir "${projectDir}/logs"
|
||||
}
|
||||
|
||||
}
|
||||
// 打包sourcesJar任务
|
||||
tasks.register('sourcesJar', Jar) {
|
||||
dependsOn classes
|
||||
|
||||
from sourceSets.main.allSource
|
||||
}
|
||||
// 打包javadocJar任务
|
||||
tasks.register('javadocJar', Jar) {
|
||||
dependsOn javadoc
|
||||
|
||||
from javadoc.destinationDir
|
||||
}
|
||||
|
||||
// 解决javadoc打包乱码
|
||||
javadoc {
|
||||
options {
|
||||
encoding "UTF-8"
|
||||
charSet 'UTF-8'
|
||||
author true
|
||||
version true
|
||||
title "DG_LAB"
|
||||
processResources.dependsOn createLogDir
|
||||
|
||||
// 其他自定义任务按需保留或调整
|
||||
tasks.register('runCli', JavaExec) {
|
||||
group = 'application'
|
||||
description = 'Run the application in CLI mode'
|
||||
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
mainClass = 'top.r3944realms.docchecktoolrefactored.Main'
|
||||
|
||||
// 默认参数,可以覆盖
|
||||
args '--cli', '-v'
|
||||
|
||||
// 可以添加更多默认参数
|
||||
if (project.hasProperty('cliArgs')) {
|
||||
args project.property('cliArgs').split()
|
||||
}
|
||||
}
|
||||
|
||||
// 可选:创建生成可执行JAR的任务
|
||||
tasks.register('buildCliJar', Jar) {
|
||||
group = 'build'
|
||||
description = 'Builds a standalone JAR for CLI mode'
|
||||
|
||||
jlink {
|
||||
imageZip = project.file("${buildDir}/distributions/app-${javafx.platform.classifier}.zip")
|
||||
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
|
||||
launcher {
|
||||
name = 'DocCheckTool'
|
||||
jvmArgs = ['-Dlogback.configurationFile=./config/logback.xml'] // 支持外部配置
|
||||
manifest {
|
||||
attributes 'Main-Class': 'top.r3944realms.docchecktoolrefactored.Main'
|
||||
}
|
||||
mergedModule {
|
||||
requires 'java.logging'
|
||||
requires 'java.xml'
|
||||
}
|
||||
}
|
||||
|
||||
jlinkZip {
|
||||
group = 'distribution'
|
||||
from {
|
||||
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
|
||||
} with jar
|
||||
|
||||
archiveBaseName = 'doc-check-tool-cli'
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
processResources.dependsOn createLogDir
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
project_group ='top.r3944realms.docchecktoolrefacored'
|
||||
project_version = '1.0-SNAPSHOT'
|
||||
project_group =top.r3944realms.docchecktoolrefacored
|
||||
project_version = 1.0-SNAPSHOT
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
module top.r3944realms.docchecktoolrefactored {
|
||||
requires javafx.graphics;
|
||||
requires javafx.controls;
|
||||
requires javafx.fxml;
|
||||
requires static lombok;
|
||||
requires org.slf4j;
|
||||
|
||||
opens top.r3944realms.docchecktoolrefactored to javafx.fxml;
|
||||
opens top.r3944realms.docchecktoolrefactored.ui to javafx.fxml;
|
||||
opens top.r3944realms.docchecktoolrefactored.ui.module to javafx.fxml;
|
||||
opens top.r3944realms.docchecktoolrefactored.deprecated to javafx.fxml;
|
||||
|
||||
exports top.r3944realms.docchecktoolrefactored to javafx.graphics;
|
||||
exports top.r3944realms.docchecktoolrefactored.ui to javafx.fxml;
|
||||
exports top.r3944realms.docchecktoolrefactored.ui.module to javafx.fxml;
|
||||
exports top.r3944realms.docchecktoolrefactored.deprecated to javafx.graphics;
|
||||
|
||||
exports top.r3944realms.docchecktoolrefactored.core ;
|
||||
exports top.r3944realms.docchecktoolrefactored.io.scanner;
|
||||
exports top.r3944realms.docchecktoolrefactored.io.reader;
|
||||
exports top.r3944realms.docchecktoolrefactored.model;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package top.r3944realms.docchecktoolrefactored;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.stage.Stage;
|
||||
import top.r3944realms.docchecktoolrefactored.ui.SceneManager;
|
||||
|
||||
public class JavaFxApplication extends Application {
|
||||
@Override
|
||||
public void init() throws Exception {
|
||||
super.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Stage primaryStage) throws Exception {
|
||||
SceneManager.init(primaryStage);
|
||||
SceneManager.switchLoginView();
|
||||
primaryStage.show();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,35 +1,36 @@
|
|||
package top.r3944realms.docchecktoolrefactored;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.stage.Stage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.ui.SceneManager;
|
||||
import top.r3944realms.docchecktoolrefactored.util.StringUtil;
|
||||
import top.r3944realms.docchecktoolrefactored.cil.CliProcessor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The type Main.
|
||||
*/
|
||||
@Slf4j
|
||||
public class Main extends Application {
|
||||
@Override
|
||||
public void init() throws Exception {
|
||||
super.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Stage primaryStage) throws Exception {
|
||||
SceneManager.init(primaryStage);
|
||||
SceneManager.switchLoginView();
|
||||
primaryStage.show();
|
||||
}
|
||||
public class Main {
|
||||
|
||||
/**
|
||||
* The entry point of application.
|
||||
*
|
||||
* @param args the input arguments
|
||||
*/
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
public static void main(String[] args) {
|
||||
log.info(StringUtil.NO_BUG);
|
||||
launch(args);
|
||||
// log.info(StringUtil.NO_BUG);
|
||||
// 检查是否有 --cli 参数
|
||||
List<String> list = Arrays.asList(args);
|
||||
if (list.contains("--cli")) {
|
||||
// CLI 模式
|
||||
List<String> strList = new ArrayList<>();
|
||||
list.stream().filter(i -> !i.equals("--cli")).forEach(strList::add);
|
||||
new CliProcessor().process(strList.toArray(new String[0]));
|
||||
} else {
|
||||
// GUI 模式
|
||||
JavaFxApplication.launch(JavaFxApplication.class, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,440 @@
|
|||
package top.r3944realms.docchecktoolrefactored.cil;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.cli.*;
|
||||
import top.r3944realms.docchecktoolrefactored.core.DuplicateFinder;
|
||||
import top.r3944realms.docchecktoolrefactored.core.FileHashCalculator;
|
||||
import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
|
||||
import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner;
|
||||
import top.r3944realms.docchecktoolrefactored.model.DuplicateGroup;
|
||||
import top.r3944realms.docchecktoolrefactored.util.FileUtil;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.text.ParseException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 命令行处理器,负责解析参数和执行重复文件检查
|
||||
*/
|
||||
@Slf4j
|
||||
public class CliProcessor {
|
||||
// 参数定义
|
||||
private static final String HELP_OPTION = "h";
|
||||
private static final String DIRECTORY_OPTION = "d";
|
||||
private static final String PROGRESS_OPTION = "p";
|
||||
private static final String MIN_SIZE_OPTION = "m";
|
||||
private static final String MAX_SIZE_OPTION = "M";
|
||||
private static final String EXCLUDE_OPTION = "e";
|
||||
private static final String FORMAT_OPTION = "f";
|
||||
private static final String OUTPUT_OPTION = "o";
|
||||
private static final String QUIET_OPTION = "q";
|
||||
private static final String SORT_OPTION = "s";
|
||||
private static final String VERSION_OPTION = "v";
|
||||
|
||||
// 支持的输出格式
|
||||
private enum OutputFormat {
|
||||
TEXT, JSON, CSV
|
||||
}
|
||||
|
||||
// 支持的排序方式
|
||||
private enum SortBy {
|
||||
SIZE, COUNT, PATH
|
||||
}
|
||||
|
||||
// 配置选项
|
||||
private boolean showProgress = false;
|
||||
private boolean quietMode = false;
|
||||
private OutputFormat outputFormat = OutputFormat.TEXT;
|
||||
private SortBy sortBy = SortBy.SIZE;
|
||||
private Path outputFile = null;
|
||||
private long minSize = 0;
|
||||
private Long maxSize = null;
|
||||
private List<String> excludeExtensions = List.of();
|
||||
|
||||
/**
|
||||
* 处理命令行参数并执行相应操作
|
||||
* @param args 命令行参数
|
||||
*/
|
||||
public void process(String[] args) {
|
||||
Options options = createOptions();
|
||||
CommandLineParser parser = new DefaultParser();
|
||||
|
||||
try {
|
||||
CommandLine cmd = parser.parse(options, args);
|
||||
|
||||
// 处理帮助选项
|
||||
if (cmd.hasOption(HELP_OPTION)) {
|
||||
printHelp(options);
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理版本选项
|
||||
if (cmd.hasOption(VERSION_OPTION)) {
|
||||
printVersion();
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析参数
|
||||
parseOptions(cmd);
|
||||
|
||||
// 验证目录参数
|
||||
if (!cmd.hasOption(DIRECTORY_OPTION)) {
|
||||
throw new IllegalArgumentException("Directory option (-d) is required");
|
||||
}
|
||||
Path scanDir = Paths.get(cmd.getOptionValue(DIRECTORY_OPTION));
|
||||
|
||||
// 执行扫描
|
||||
List<DuplicateGroup> duplicates = scanForDuplicates(scanDir);
|
||||
|
||||
// 输出结果
|
||||
outputResults(duplicates);
|
||||
|
||||
} catch (ParseException e) {
|
||||
System.err.println("Error: " + e.getMessage());
|
||||
printHelp(options);
|
||||
System.exit(1);
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing CLI command", e);
|
||||
System.err.println("Error: " + e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建命令行选项
|
||||
*/
|
||||
private Options createOptions() {
|
||||
Options options = new Options();
|
||||
|
||||
// 基本选项
|
||||
options.addOption(Option.builder(HELP_OPTION)
|
||||
.longOpt("help")
|
||||
.desc("Show this help message")
|
||||
.build());
|
||||
options.addOption(Option.builder(VERSION_OPTION)
|
||||
.longOpt("version")
|
||||
.desc("Show version information")
|
||||
.build());
|
||||
options.addOption(Option.builder(DIRECTORY_OPTION)
|
||||
.longOpt("directory")
|
||||
.hasArg()
|
||||
.argName("PATH")
|
||||
.desc("Directory to scan for duplicates")
|
||||
.build());
|
||||
options.addOption(Option.builder(PROGRESS_OPTION)
|
||||
.longOpt("progress")
|
||||
.desc("Show progress bar during processing")
|
||||
.build());
|
||||
options.addOption(Option.builder(QUIET_OPTION)
|
||||
.longOpt("quiet")
|
||||
.desc("Quiet mode, only show errors")
|
||||
.build());
|
||||
|
||||
// 过滤选项
|
||||
options.addOption(Option.builder(MIN_SIZE_OPTION)
|
||||
.longOpt("min-size")
|
||||
.hasArg()
|
||||
.argName("BYTES")
|
||||
.desc("Minimum file size to consider (in bytes)")
|
||||
.build());
|
||||
options.addOption(Option.builder(MAX_SIZE_OPTION)
|
||||
.longOpt("max-size")
|
||||
.hasArg()
|
||||
.argName("BYTES")
|
||||
.desc("Maximum file size to consider (in bytes)")
|
||||
.build());
|
||||
options.addOption(Option.builder(EXCLUDE_OPTION)
|
||||
.longOpt("exclude")
|
||||
.hasArg()
|
||||
.argName("EXTENSIONS")
|
||||
.desc("Comma-separated list of file extensions to exclude")
|
||||
.build());
|
||||
|
||||
// 输出控制选项
|
||||
options.addOption(Option.builder(FORMAT_OPTION)
|
||||
.longOpt("format")
|
||||
.hasArg()
|
||||
.argName("FORMAT")
|
||||
.desc("Output format (text/json/csv), default: text")
|
||||
.build());
|
||||
options.addOption(Option.builder(OUTPUT_OPTION)
|
||||
.longOpt("output")
|
||||
.hasArg()
|
||||
.argName("FILE")
|
||||
.desc("Write output to file instead of stdout")
|
||||
.build());
|
||||
options.addOption(Option.builder(SORT_OPTION)
|
||||
.longOpt("sort")
|
||||
.hasArg()
|
||||
.argName("CRITERIA")
|
||||
.desc("Sort results by (size/count/path), default: size")
|
||||
.build());
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析命令行选项
|
||||
*/
|
||||
private void parseOptions(CommandLine cmd) throws ParseException {
|
||||
showProgress = cmd.hasOption(PROGRESS_OPTION);
|
||||
quietMode = cmd.hasOption(QUIET_OPTION);
|
||||
|
||||
// 解析输出格式
|
||||
if (cmd.hasOption(FORMAT_OPTION)) {
|
||||
try {
|
||||
outputFormat = OutputFormat.valueOf(cmd.getOptionValue(FORMAT_OPTION).toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("Invalid output format. Allowed values: text, json, csv");
|
||||
}
|
||||
}
|
||||
|
||||
// 解析排序方式
|
||||
if (cmd.hasOption(SORT_OPTION)) {
|
||||
try {
|
||||
sortBy = SortBy.valueOf(cmd.getOptionValue(SORT_OPTION).toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("Invalid sort criteria. Allowed values: size, count, path");
|
||||
}
|
||||
}
|
||||
|
||||
// 解析输出文件
|
||||
if (cmd.hasOption(OUTPUT_OPTION)) {
|
||||
outputFile = Paths.get(cmd.getOptionValue(OUTPUT_OPTION));
|
||||
}
|
||||
|
||||
// 解析大小限制
|
||||
if (cmd.hasOption(MIN_SIZE_OPTION)) {
|
||||
try {
|
||||
minSize = Long.parseLong(cmd.getOptionValue(MIN_SIZE_OPTION));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid min-size value: must be a number");
|
||||
}
|
||||
}
|
||||
|
||||
if (cmd.hasOption(MAX_SIZE_OPTION)) {
|
||||
try {
|
||||
maxSize = Long.parseLong(cmd.getOptionValue(MAX_SIZE_OPTION));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid max-size value: must be a number");
|
||||
}
|
||||
}
|
||||
|
||||
// 解析排除的扩展名
|
||||
if (cmd.hasOption(EXCLUDE_OPTION)) {
|
||||
excludeExtensions = Arrays.asList(cmd.getOptionValue(EXCLUDE_OPTION).split(","));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行重复文件扫描
|
||||
*/
|
||||
private List<DuplicateGroup> scanForDuplicates(Path scanDir) throws IOException {
|
||||
if (!quietMode) {
|
||||
System.out.println("Scanning directory: " + scanDir.toAbsolutePath());
|
||||
}
|
||||
|
||||
FileScanner scanner = new RobustParallelScanner(20);
|
||||
FileHashCalculator calculator = FileHashCalculator.defaultInstance();
|
||||
DuplicateFinder finder = new DuplicateFinder(scanner, calculator, showProgress);
|
||||
|
||||
List<DuplicateGroup> duplicates = finder.findDuplicates(scanDir);
|
||||
|
||||
// 应用过滤
|
||||
duplicates = filterResults(duplicates);
|
||||
|
||||
// 应用排序
|
||||
duplicates = sortResults(duplicates);
|
||||
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤结果
|
||||
*/
|
||||
private List<DuplicateGroup> filterResults(List<DuplicateGroup> groups) {
|
||||
return groups.stream()
|
||||
.filter(group -> group.size() >= minSize)
|
||||
.filter(group -> maxSize == null || group.size() <= maxSize)
|
||||
.filter(group -> excludeExtensions.stream()
|
||||
.noneMatch(ext -> group.fileMetas().stream()
|
||||
.anyMatch(file -> file.getPath().toString().toLowerCase().endsWith(ext.toLowerCase()))))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 排序结果
|
||||
*/
|
||||
private List<DuplicateGroup> sortResults(List<DuplicateGroup> groups) {
|
||||
return switch (sortBy) {
|
||||
case SIZE -> groups.stream()
|
||||
.sorted((g1, g2) -> Long.compare(g2.size(), g1.size()))
|
||||
.collect(Collectors.toList());
|
||||
case COUNT -> groups.stream()
|
||||
.sorted((g1, g2) -> Integer.compare(g2.fileMetas().size(), g1.fileMetas().size()))
|
||||
.collect(Collectors.toList());
|
||||
case PATH -> groups.stream()
|
||||
.sorted(Comparator.comparing(g -> g.fileMetas().get(0).getPath()))
|
||||
.collect(Collectors.toList());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出结果
|
||||
*/
|
||||
private void outputResults(List<DuplicateGroup> duplicates) throws IOException {
|
||||
if (quietMode && duplicates.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String outputContent = switch (outputFormat) {
|
||||
case JSON -> formatAsJson(duplicates);
|
||||
case CSV -> formatAsCsv(duplicates);
|
||||
default -> formatAsText(duplicates);
|
||||
};
|
||||
|
||||
if (outputFile != null) {
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(outputFile)) {
|
||||
writer.write(outputContent);
|
||||
}
|
||||
if (!quietMode) {
|
||||
System.out.println("Results written to: " + outputFile.toAbsolutePath());
|
||||
}
|
||||
} else {
|
||||
System.out.println(outputContent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本格式输出
|
||||
*/
|
||||
private String formatAsText(List<DuplicateGroup> duplicates) {
|
||||
if (duplicates.isEmpty()) {
|
||||
return "No duplicate files found.";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(String.format("\nFound %d groups of duplicate files:\n", duplicates.size()));
|
||||
|
||||
AtomicInteger groupNum = new AtomicInteger(1);
|
||||
duplicates.forEach(group -> {
|
||||
sb.append(String.format("\nGroup %d:\n", groupNum.getAndIncrement()));
|
||||
sb.append(String.format("Hash: %s\n", group.hash()));
|
||||
sb.append(String.format("Size: %s\n", FileUtil.humanReadableByteCount(group.size())));
|
||||
sb.append("Files:\n");
|
||||
group.fileMetas().forEach(file -> sb.append(String.format(" %s\n", file.getPath())));
|
||||
});
|
||||
|
||||
// 添加统计信息
|
||||
long totalWastedSpace = duplicates.stream()
|
||||
.mapToLong(group -> group.size() * (group.fileMetas().size() - 1))
|
||||
.sum();
|
||||
sb.append(String.format("\nTotal wasted space: %s\n",
|
||||
FileUtil.humanReadableByteCount(totalWastedSpace)));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON格式输出
|
||||
*/
|
||||
private String formatAsJson(List<DuplicateGroup> duplicates) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("{\n");
|
||||
|
||||
// 基本信息
|
||||
sb.append(" \"duplicate_groups\": [\n");
|
||||
|
||||
// 各组数据
|
||||
duplicates.forEach(group -> {
|
||||
sb.append(" {\n");
|
||||
sb.append(String.format(" \"hash\": \"%s\",\n", group.hash()));
|
||||
sb.append(String.format(" \"size\": %d,\n", group.size()));
|
||||
sb.append(String.format(" \"size_human\": \"%s\",\n",
|
||||
FileUtil.humanReadableByteCount(group.size())));
|
||||
sb.append(" \"files\": [\n");
|
||||
|
||||
group.fileMetas().forEach(file ->
|
||||
sb.append(String.format(" \"%s\",\n", file.getPath())));
|
||||
|
||||
// 移除最后一个逗号
|
||||
if (!group.fileMetas().isEmpty()) {
|
||||
sb.delete(sb.length() - 2, sb.length());
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
sb.append(" ]\n");
|
||||
sb.append(" },\n");
|
||||
});
|
||||
|
||||
// 移除最后一个逗号
|
||||
if (!duplicates.isEmpty()) {
|
||||
sb.delete(sb.length() - 2, sb.length());
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
sb.append(" ],\n");
|
||||
|
||||
// 统计信息
|
||||
long totalWastedSpace = duplicates.stream()
|
||||
.mapToLong(group -> group.size() * (group.fileMetas().size() - 1))
|
||||
.sum();
|
||||
sb.append(" \"stats\": {\n");
|
||||
sb.append(String.format(" \"total_groups\": %d,\n", duplicates.size()));
|
||||
sb.append(String.format(" \"total_files\": %d,\n",
|
||||
duplicates.stream().mapToInt(group -> group.fileMetas().size()).sum()));
|
||||
sb.append(String.format(" \"total_wasted_space\": %d,\n", totalWastedSpace));
|
||||
sb.append(String.format(" \"total_wasted_space_human\": \"%s\"\n",
|
||||
FileUtil.humanReadableByteCount(totalWastedSpace)));
|
||||
sb.append(" }\n");
|
||||
|
||||
sb.append("}\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* CSV格式输出
|
||||
*/
|
||||
private String formatAsCsv(List<DuplicateGroup> duplicates) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("group,hash,size,size_human,file_path\n");
|
||||
|
||||
AtomicInteger groupNum = new AtomicInteger(1);
|
||||
duplicates.forEach(group -> {
|
||||
String groupPrefix = groupNum.getAndIncrement() + "," +
|
||||
group.hash() + "," +
|
||||
group.size() + "," +
|
||||
FileUtil.humanReadableByteCount(group.size());
|
||||
|
||||
group.fileMetas().forEach(file ->
|
||||
sb.append(groupPrefix).append(",").append(file.getPath()).append("\n"));
|
||||
});
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印帮助信息
|
||||
*/
|
||||
private void printHelp(Options options) {
|
||||
HelpFormatter formatter = new HelpFormatter();
|
||||
formatter.printHelp("doc-check-tool", options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印版本信息
|
||||
*/
|
||||
private void printVersion() {
|
||||
Package pkg = getClass().getPackage();
|
||||
String version = pkg.getImplementationVersion();
|
||||
System.out.println("Doc Check Tool " + (version != null ? version : "DEV"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
package top.r3944realms.docchecktoolrefactored.core;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public class AddressFileComparator {
|
||||
|
||||
public enum CompareMode {
|
||||
PAGE_LEVEL, // 页面级比较
|
||||
FILE_LEVEL // 文件级比较
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static class ComparisonResult {
|
||||
private final int physicalRecordsCount; // 物理文件记录数
|
||||
private final int logicalRecordsCount; // 逻辑文件记录数
|
||||
private final List<String> forwardComparisonResults; // 正向比较文件名缺失结果
|
||||
private final List<String> backwardComparisonResults; // 反向比较文件名缺失结果
|
||||
private final List<String> pathMismatchResults; // 路径不一致的结果
|
||||
private final List<String> pageCountMismatchResults; // 页数不一致的结果(仅文件级比较使用)
|
||||
|
||||
public ComparisonResult(int physicalRecordsCount,
|
||||
int logicalRecordsCount,
|
||||
List<String> forwardResults,
|
||||
List<String> backwardResults,
|
||||
List<String> pathMismatchResults,
|
||||
List<String> pageCountMismatchResults) {
|
||||
this.physicalRecordsCount = physicalRecordsCount;
|
||||
this.logicalRecordsCount = logicalRecordsCount;
|
||||
this.forwardComparisonResults = forwardResults;
|
||||
this.backwardComparisonResults = backwardResults;
|
||||
this.pathMismatchResults = pathMismatchResults;
|
||||
this.pageCountMismatchResults = pageCountMismatchResults;
|
||||
}
|
||||
|
||||
// 为向后兼容保留原来的构造函数
|
||||
public ComparisonResult(int physicalRecordsCount,
|
||||
int logicalRecordsCount,
|
||||
List<String> forwardResults,
|
||||
List<String> backwardResults,
|
||||
List<String> pathMismatchResults) {
|
||||
this(physicalRecordsCount, logicalRecordsCount, forwardResults,
|
||||
backwardResults, pathMismatchResults, new ArrayList<>());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public ComparisonResult compareFiles(String physicalAddressFilePath, String logicalAddressFilePath) {
|
||||
return compareFiles(physicalAddressFilePath, logicalAddressFilePath, CompareMode.PAGE_LEVEL);
|
||||
}
|
||||
|
||||
public ComparisonResult compareFiles(String physicalAddressFilePath, String logicalAddressFilePath, CompareMode compareMode) {
|
||||
List<String[]> physicalRecords = readCSV(physicalAddressFilePath);
|
||||
List<String[]> logicalRecords = readCSV(logicalAddressFilePath);
|
||||
|
||||
// 记录读取的行数(不包括标题行)
|
||||
int physicalCount = physicalRecords.size();
|
||||
int logicalCount = logicalRecords.size();
|
||||
|
||||
log.info("读取物理地址文件记录数: {}", physicalCount);
|
||||
log.info("读取逻辑地址文件记录数: {}", logicalCount);
|
||||
|
||||
List<String> forwardComparisonResults = new ArrayList<>(); // 物理文件在逻辑文件中未找到
|
||||
List<String> backwardComparisonResults = new ArrayList<>(); // 逻辑文件在物理文件中未找到
|
||||
List<String> pathMismatchResults = new ArrayList<>(); // 文件名相同但路径不一致
|
||||
List<String> pageCountMismatchResults = new ArrayList<>(); // 文件名和路径相同但页数不一致(仅文件级)
|
||||
|
||||
// 正向比较:遍历物理文件,检查是否在逻辑文件中存在
|
||||
for (String[] physicalRecord : physicalRecords) {
|
||||
// 确保数据行有足够列数
|
||||
if (compareMode == CompareMode.PAGE_LEVEL && physicalRecord.length < 2) continue;
|
||||
if (compareMode == CompareMode.FILE_LEVEL && physicalRecord.length < 3) continue;
|
||||
|
||||
String physicalFileName = physicalRecord[0];
|
||||
String physicalAddress = physicalRecord[1];
|
||||
String physicalPageCount = compareMode == CompareMode.FILE_LEVEL ? physicalRecord[2] : null;
|
||||
|
||||
boolean found = false;
|
||||
for (String[] logicalRecord : logicalRecords) {
|
||||
// 确保数据行有足够列数
|
||||
if (compareMode == CompareMode.PAGE_LEVEL && logicalRecord.length < 2) continue;
|
||||
if (compareMode == CompareMode.FILE_LEVEL && logicalRecord.length < 3) continue;
|
||||
|
||||
String logicalFileName = logicalRecord[0];
|
||||
String logicalAddress = logicalRecord[1];
|
||||
String logicalPageCount = compareMode == CompareMode.FILE_LEVEL ? logicalRecord[2] : null;
|
||||
|
||||
if (physicalFileName.equals(logicalFileName)) {
|
||||
found = true;
|
||||
// 文件名相同,比较路径
|
||||
if (!physicalAddress.equals(logicalAddress)) {
|
||||
pathMismatchResults.add("文件名=" + physicalFileName +
|
||||
", 物理地址=" + physicalAddress +
|
||||
", 逻辑地址=" + logicalAddress);
|
||||
}
|
||||
// 如果是文件级比较且路径相同,再比较页数
|
||||
else if (compareMode == CompareMode.FILE_LEVEL &&
|
||||
physicalPageCount != null && logicalPageCount != null &&
|
||||
!physicalPageCount.equals(logicalPageCount)) {
|
||||
pageCountMismatchResults.add("文件名=" + physicalFileName +
|
||||
", 物理地址=" + physicalAddress +
|
||||
", 逻辑地址=" + logicalAddress +
|
||||
", 物理页数=" + physicalPageCount +
|
||||
", 逻辑页数=" + logicalPageCount);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果在逻辑文件中未找到该物理文件记录
|
||||
if (!found) {
|
||||
String result = "文件名=" + physicalFileName + ", 物理地址=" + physicalAddress;
|
||||
if (compareMode == CompareMode.FILE_LEVEL && physicalPageCount != null) {
|
||||
result += ", 物理页数=" + physicalPageCount;
|
||||
}
|
||||
forwardComparisonResults.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
// 反向比较:遍历逻辑文件,检查是否在物理文件中存在
|
||||
for (String[] logicalRecord : logicalRecords) {
|
||||
// 确保数据行有足够列数
|
||||
if (compareMode == CompareMode.PAGE_LEVEL && logicalRecord.length < 2) continue;
|
||||
if (compareMode == CompareMode.FILE_LEVEL && logicalRecord.length < 3) continue;
|
||||
|
||||
String logicalFileName = logicalRecord[0];
|
||||
String logicalAddress = logicalRecord[1];
|
||||
String logicalPageCount = compareMode == CompareMode.FILE_LEVEL ? logicalRecord[2] : null;
|
||||
|
||||
boolean found = false;
|
||||
for (String[] physicalRecord : physicalRecords) {
|
||||
// 确保数据行有足够列数
|
||||
if (compareMode == CompareMode.PAGE_LEVEL && physicalRecord.length < 2) continue;
|
||||
if (compareMode == CompareMode.FILE_LEVEL && physicalRecord.length < 3) continue;
|
||||
|
||||
String physicalFileName = physicalRecord[0];
|
||||
|
||||
if (logicalFileName.equals(physicalFileName)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果在物理文件中未找到该逻辑文件记录
|
||||
if (!found) {
|
||||
String result = "文件名=" + logicalFileName + ", 逻辑地址=" + logicalAddress;
|
||||
if (compareMode == CompareMode.FILE_LEVEL && logicalPageCount != null) {
|
||||
result += ", 逻辑页数=" + logicalPageCount;
|
||||
}
|
||||
backwardComparisonResults.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
// 将比较结果记录到日志中
|
||||
logComparisonResults(physicalCount, logicalCount, forwardComparisonResults,
|
||||
backwardComparisonResults, pathMismatchResults, pageCountMismatchResults, compareMode);
|
||||
|
||||
return new ComparisonResult(
|
||||
physicalCount,
|
||||
logicalCount,
|
||||
forwardComparisonResults,
|
||||
backwardComparisonResults,
|
||||
pathMismatchResults,
|
||||
pageCountMismatchResults
|
||||
);
|
||||
}
|
||||
|
||||
private List<String[]> readCSV(String filePath) {
|
||||
List<String[]> records = new ArrayList<>();
|
||||
|
||||
try {
|
||||
File file = new File(filePath);
|
||||
if (!file.exists()) {
|
||||
log.error("CSV文件不存在: {}", filePath);
|
||||
return records;
|
||||
}
|
||||
|
||||
BufferedReader reader = new BufferedReader(new FileReader(file));
|
||||
String line;
|
||||
boolean isFirstLine = true;
|
||||
|
||||
while ((line = reader.readLine()) != null) {
|
||||
// 跳过首行(标题行)
|
||||
if (isFirstLine) {
|
||||
isFirstLine = false;
|
||||
continue;
|
||||
}
|
||||
// 按逗号分隔CSV行数据
|
||||
String[] data = line.split(",");
|
||||
records.add(data);
|
||||
}
|
||||
|
||||
reader.close();
|
||||
log.info("成功读取CSV文件,共 {} 行记录", records.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("读取CSV文件时出错: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
private void logComparisonResults(int physicalCount, int logicalCount,
|
||||
List<String> forwardResults, List<String> backwardResults,
|
||||
List<String> pathMismatchResults, List<String> pageCountMismatchResults,
|
||||
CompareMode compareMode) {
|
||||
log.info("=== 文件比较结果 ===");
|
||||
log.info("物理地址文件记录数: {}", physicalCount);
|
||||
log.info("逻辑地址文件记录数: {}", logicalCount);
|
||||
|
||||
if (pathMismatchResults.isEmpty()) {
|
||||
log.info("没有路径错误");
|
||||
} else {
|
||||
log.info("文件名相同但路径不一致的记录数量: {}", pathMismatchResults.size());
|
||||
for (String result : pathMismatchResults) {
|
||||
log.info("\t{}", result);
|
||||
}
|
||||
}
|
||||
|
||||
if (compareMode == CompareMode.FILE_LEVEL) {
|
||||
if (pageCountMismatchResults.isEmpty()) {
|
||||
log.info("没有页数错误");
|
||||
} else {
|
||||
log.info("文件名和路径相同但页数不一致的记录数量: {}", pageCountMismatchResults.size());
|
||||
for (String result : pageCountMismatchResults) {
|
||||
log.info("\t{}", result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (forwardResults.isEmpty()) {
|
||||
log.info("没有物理存在而逻辑不存在的文件");
|
||||
} else {
|
||||
log.info("物理文件在逻辑文件中未找到的记录数量: {}", forwardResults.size());
|
||||
for (String result : forwardResults) {
|
||||
log.info("\t{}", result);
|
||||
}
|
||||
}
|
||||
|
||||
if (backwardResults.isEmpty()) {
|
||||
log.info("没有逻辑存在而物理不存在的文件");
|
||||
} else {
|
||||
log.info("逻辑文件在物理文件中未找到的记录数量: {}", backwardResults.size());
|
||||
for (String result : backwardResults) {
|
||||
log.info("\t{}", result);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("=== 比较完成 ===");
|
||||
}
|
||||
|
||||
// 为向后兼容保留原来的日志方法
|
||||
private void logComparisonResults(int physicalCount, int logicalCount,
|
||||
List<String> forwardResults, List<String> backwardResults,
|
||||
List<String> pathMismatchResults) {
|
||||
logComparisonResults(physicalCount, logicalCount, forwardResults, backwardResults,
|
||||
pathMismatchResults, new ArrayList<>(), CompareMode.PAGE_LEVEL);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package top.r3944realms.docchecktoolrefactored.core;
|
||||
|
||||
import org.intellij.lang.annotations.MagicConstant;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public interface AddressFileGenerator {
|
||||
/**
|
||||
* 页面级
|
||||
*/
|
||||
int PAGE_TYPE = 1;
|
||||
/**
|
||||
* 文件级
|
||||
*/
|
||||
int FILE_TYPE = 2;
|
||||
|
||||
/**
|
||||
* 回调接口
|
||||
*/
|
||||
interface Callback {
|
||||
void onProgress(String message);
|
||||
void onSuccess(String outputPath);
|
||||
void onError(String errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成地址文件
|
||||
*
|
||||
* @param folderPath 文件夹路径
|
||||
* @param outputFile 输出文件
|
||||
* @param folderType 文件夹类型 ({@link AddressFileGenerator#PAGE_TYPE "页面级"} 或 {@link AddressFileGenerator#FILE_TYPE "文件级"})
|
||||
* @param callback {@link Callback 回调接口}
|
||||
*/
|
||||
void generateAddressFile(
|
||||
String folderPath,
|
||||
File outputFile,
|
||||
@MagicConstant(intValues = {PAGE_TYPE, FILE_TYPE}) int folderType,
|
||||
Callback callback
|
||||
);
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import java.nio.file.Files;
|
|||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
|
|
@ -39,8 +40,19 @@ public class DuplicateFinder {
|
|||
// 第一阶段:按文件大小分组
|
||||
Map<Long, List<FileMetadata>> sizeGroups = groupFilesBySize(rootDir);
|
||||
|
||||
// 计算需要处理的总文件数(大小分组中可能有重复的文件)
|
||||
int totalFilesToProcess = sizeGroups.values().stream()
|
||||
.filter(group -> group.size() > 1)
|
||||
.mapToInt(List::size)
|
||||
.sum();
|
||||
|
||||
if (totalFilesToProcess == 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 第二阶段:对可能重复的文件计算哈希
|
||||
Map<String, List<FileMetadata>> hashGroups = new ConcurrentHashMap<>();
|
||||
AtomicInteger processedFiles = new AtomicInteger(0);
|
||||
|
||||
sizeGroups.values().parallelStream()
|
||||
.filter(group -> group.size() > 1) // 只处理可能重复的文件
|
||||
|
|
@ -49,12 +61,19 @@ public class DuplicateFinder {
|
|||
String hash = hashCalculator.calculateHash(file.getPath());
|
||||
file.setHash(hash);
|
||||
hashGroups.computeIfAbsent(hash, k -> new ArrayList<>()).add(file);
|
||||
// 更新进度
|
||||
int current = processedFiles.incrementAndGet();
|
||||
if (enableProgress) {
|
||||
printProgress("Calculating hashes", current, totalFilesToProcess);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// 记录错误但继续处理其他文件
|
||||
log.error("Failed to calculate file's hash: {}, {}", file.getPath(), e.getMessage());
|
||||
}
|
||||
}));
|
||||
|
||||
if (enableProgress) {
|
||||
System.out.println(); // 完成进度条后换行
|
||||
}
|
||||
// 第三阶段:构建结果
|
||||
return hashGroups.values().stream()
|
||||
.filter(group -> group.size() > 1)
|
||||
|
|
@ -75,7 +94,11 @@ public class DuplicateFinder {
|
|||
FileScanner.ProgressAwareListener listener = new FileScanner.ProgressAwareListener() {
|
||||
@Override
|
||||
public void onProgressUpdate(int current, int total) {
|
||||
log.info("Scanning progress: {}/{} ", current, total);
|
||||
if (enableProgress) {
|
||||
printProgress("Scanning files", current, total);
|
||||
} else {
|
||||
log.info("Scanning progress: {} / {} ", current, total);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -101,4 +124,24 @@ public class DuplicateFinder {
|
|||
fileScanner.scan(rootDir, listener);
|
||||
return sizeGroups;
|
||||
}
|
||||
/**
|
||||
* 打印进度条
|
||||
* @param phase 当前阶段描述
|
||||
* @param current 当前进度
|
||||
* @param total 总进度
|
||||
*/
|
||||
private void printProgress(String phase, int current, int total) {
|
||||
int width = 50; // 进度条宽度
|
||||
float percent = (float) current / total;
|
||||
int progress = (int) (width * percent);
|
||||
|
||||
String progressBar = String.format("\r%s: [%-" + width + "s] %3d%% %d/%d",
|
||||
phase,
|
||||
"=".repeat(progress),
|
||||
(int) (percent * 100),
|
||||
current,
|
||||
total);
|
||||
|
||||
System.out.print(progressBar);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,31 @@ public interface FileHashCalculator {
|
|||
*/
|
||||
String calculateHash(Path file) throws IOException;
|
||||
|
||||
/**
|
||||
* 计算文件部分hash值方法
|
||||
* @param file 要计算的文件路径
|
||||
* @param partialChunkSize 分块大小
|
||||
* @return 文件的哈希值字符串
|
||||
*/
|
||||
String calculatePartialHash(Path file, int partialChunkSize) throws IOException;
|
||||
|
||||
/**
|
||||
* 计算文件部分hash值方法(默认)
|
||||
* @param file 要计算的文件路径
|
||||
* @return 文件的哈希值字符串
|
||||
*/
|
||||
default String calculatePartialHash(Path file) throws IOException {
|
||||
return calculatePartialHash(file, getPartialSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* 块大小
|
||||
* @return 块大小
|
||||
*/
|
||||
default int getPartialSize() {
|
||||
return 4096;// 4 * 1024
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认实现使用MD5
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
package top.r3944realms.docchecktoolrefactored.core;
|
||||
|
||||
|
||||
import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
|
||||
import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class HashFileGenerator {
|
||||
|
||||
public interface ProgressListener {
|
||||
void onProgressUpdate(int current, int total);
|
||||
}
|
||||
|
||||
public void generateHashFile(List<Path> directories, Path outputFile, ProgressListener listener) throws IOException, InterruptedException {
|
||||
List<Path> allFiles = new ArrayList<>();
|
||||
|
||||
// 扫描所有目录中的文件
|
||||
for (Path directory : directories) {
|
||||
if (!Files.isDirectory(directory)) {
|
||||
throw new IllegalArgumentException("指定路径不是有效目录: " + directory);
|
||||
}
|
||||
|
||||
List<Path> files = new ArrayList<>();
|
||||
CompletableFuture<Void> scanFuture = new CompletableFuture<>();
|
||||
|
||||
// 使用 RobustParallelScanner 扫描文件
|
||||
try (RobustParallelScanner scanner = new RobustParallelScanner(10)) {
|
||||
scanner.scan(directory, new FileScanner.FileScanListener() {
|
||||
@Override
|
||||
public void onFileFound(Path file) {
|
||||
files.add(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScanComplete() {
|
||||
scanFuture.complete(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Path path, Exception e) {
|
||||
System.err.println("Error scanning path: " + path + " - " + e.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
// 等待扫描完成
|
||||
scanFuture.join();
|
||||
allFiles.addAll(files);
|
||||
}
|
||||
}
|
||||
|
||||
// 计算每个文件的哈希值
|
||||
List<String[]> hashResults = new ArrayList<>();
|
||||
AtomicInteger processedFiles = new AtomicInteger(0);
|
||||
int totalFiles = allFiles.size();
|
||||
|
||||
allFiles.parallelStream().forEach(file -> {
|
||||
try {
|
||||
String hash = new MD5HashCalculator().calculatePartialHash(file);
|
||||
String[] result = {file.getFileName().toString(), hash};
|
||||
synchronized (hashResults) {
|
||||
hashResults.add(result);
|
||||
}
|
||||
int processed = processedFiles.incrementAndGet();
|
||||
if (listener != null) {
|
||||
listener.onProgressUpdate(processed, totalFiles);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("无法计算该文件哈希值: " + file + " - " + e.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
// 写入结果到文件
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile.toFile()))) {
|
||||
writer.write("文件名,哈希值");
|
||||
writer.newLine();
|
||||
for (String[] result : hashResults) {
|
||||
writer.write(result[0] + "," + result[1]);
|
||||
writer.newLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package top.r3944realms.docchecktoolrefactored.core;
|
||||
|
||||
import top.r3944realms.docchecktoolrefactored.io.reader.CatalogFileReader;
|
||||
import top.r3944realms.docchecktoolrefactored.io.reader.CatalogFileReaderFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
public class LogicalAddressFileGenerator implements AddressFileGenerator {
|
||||
@Override
|
||||
public void generateAddressFile(String catalogFilePath, File outputFile, int folderType, Callback callback) {
|
||||
callback.onProgress("正在生成逻辑地址文件...");
|
||||
try {
|
||||
// 使用工厂模式创建相应的文件读取器
|
||||
List<Record> records = readCatalogFile(catalogFilePath);
|
||||
|
||||
// 过滤掉 可能的 输出文件记录 -- 注2: 避免将输出文件作为输入处理
|
||||
String outputFileName = outputFile.getName();
|
||||
records = records.stream()
|
||||
.filter(record -> !record.archiveCode.equalsIgnoreCase(outputFileName))
|
||||
.toList();
|
||||
try (PrintWriter writer = new PrintWriter(outputFile, StandardCharsets.UTF_8)) {
|
||||
if (folderType == PAGE_TYPE) {
|
||||
// 页面级逻辑:为每页生成一行数据
|
||||
generatePageLevelFile(writer, records);
|
||||
} else if (folderType == FILE_TYPE) {
|
||||
// 文件级逻辑:每个档案只生成一行数据
|
||||
generateFileLevelFile(writer, records);
|
||||
} else {
|
||||
throw new IllegalArgumentException("不支持的文件夹类型: " + folderType);
|
||||
}
|
||||
}
|
||||
|
||||
callback.onSuccess("逻辑地址文件生成成功: " + outputFile.getAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
callback.onError("生成逻辑地址文件时出错: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 生成页面级逻辑地址文件
|
||||
*/
|
||||
private void generatePageLevelFile(PrintWriter writer, List<Record> records) {
|
||||
// 写入CSV头部
|
||||
writer.println("逻辑文件名,逻辑地址");
|
||||
|
||||
// 处理每条记录
|
||||
for (Record record : records) {
|
||||
String archiveCode = record.archiveCode;
|
||||
int page = record.page;
|
||||
|
||||
// 为每页生成一行数据
|
||||
for (int i = 1; i <= page; i++) {
|
||||
// 生成逻辑文件名
|
||||
String logicalFileName = String.format("%s-%04d", archiveCode, i);
|
||||
|
||||
// 生成逻辑地址
|
||||
String logicalAddress = generatePageLevelLogicalAddress(archiveCode, i);
|
||||
|
||||
// 写入CSV行
|
||||
writer.printf("%s,%s%n", logicalFileName, logicalAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件级逻辑地址文件
|
||||
*/
|
||||
private void generateFileLevelFile(PrintWriter writer, List<Record> records) {
|
||||
// 写入CSV头部(包含页数列)
|
||||
writer.println("逻辑文件名,逻辑地址,页数");
|
||||
|
||||
// 处理每条记录
|
||||
for (Record record : records) {
|
||||
String archiveCode = record.archiveCode;
|
||||
int page = record.page;
|
||||
|
||||
// 生成逻辑地址(不包含页数)
|
||||
String logicalAddress = generateFileLevelLogicalAddress(archiveCode);
|
||||
|
||||
// 写入CSV行,包含页数
|
||||
writer.printf("%s,%s,%d%n", /* 逻辑文件名(就是档号)*/ archiveCode, logicalAddress, page);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据档号生成文件级逻辑地址(不包含页数)
|
||||
*
|
||||
* @param archiveCode 档号内容
|
||||
* @return 文件级逻辑地址
|
||||
*/
|
||||
private String generateFileLevelLogicalAddress(String archiveCode) {
|
||||
StringBuilder address = csvStringBuilder(archiveCode);
|
||||
|
||||
// 添加最后的档号部分
|
||||
if (!address.isEmpty()) {
|
||||
address.append("\\").append(archiveCode);
|
||||
}
|
||||
|
||||
return address.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据档号生成逻辑地址
|
||||
*
|
||||
* @param archiveCode 档号内容
|
||||
* @param pageNumber 页码
|
||||
* @return 逻辑地址
|
||||
*/
|
||||
private String generatePageLevelLogicalAddress(String archiveCode, int pageNumber) {
|
||||
StringBuilder address = csvStringBuilder(archiveCode);
|
||||
|
||||
// 添加页码部分,同样去掉第一个
|
||||
if (!address.isEmpty()) {
|
||||
address.append("\\");
|
||||
}
|
||||
address.append(String.format("%s-%04d", archiveCode, pageNumber));
|
||||
|
||||
return address.toString();
|
||||
}
|
||||
|
||||
private StringBuilder csvStringBuilder(String archiveCode) {
|
||||
StringBuilder address = new StringBuilder();
|
||||
String[] parts = archiveCode.split("-");
|
||||
|
||||
StringBuilder path = new StringBuilder();
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
if (i > 0) {
|
||||
path.append("-");
|
||||
}
|
||||
path.append(parts[i]);
|
||||
// 去掉第一个 \
|
||||
if (address.isEmpty()) {
|
||||
address.append(path);
|
||||
} else {
|
||||
address.append("\\").append(path);
|
||||
}
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取目录文件
|
||||
*
|
||||
* @param filePath 文件路径
|
||||
* @return 记录列表
|
||||
* @throws Exception 读取异常
|
||||
*/
|
||||
private List<Record> readCatalogFile(String filePath) throws Exception {
|
||||
// 使用工厂模式创建相应的文件读取器
|
||||
CatalogFileReader reader = CatalogFileReaderFactory.createReader(filePath);
|
||||
return reader.readCatalogFile(filePath);
|
||||
}
|
||||
public record Record(String archiveCode, int page) {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package top.r3944realms.docchecktoolrefactored.core;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
|
|
@ -29,6 +30,69 @@ public class MD5HashCalculator implements FileHashCalculator {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <h1>读取文件头部:</h1>
|
||||
* <ul>
|
||||
* <li>读取文件的前partialChunkSize字节(如果文件大小小于这个值,则读取整个文件)</li>
|
||||
* <li>这些字节会被添加到MD5计算中</li>
|
||||
* </ul>
|
||||
* <h1>读取文件中间部分:</h1>
|
||||
* <ul>
|
||||
* <li>只有当文件大小大于partialChunkSize * 2时才会执行</li>
|
||||
* <li>从文件中间位置前后各取partialChunkSize/2字节(总共partialChunkSize字节)</li>
|
||||
* <li>这些字节会被追加到MD5计算中 </li>
|
||||
* </ul>
|
||||
* <h1>读取文件尾部:</h1>
|
||||
* <ul>
|
||||
* <li>只有当文件大小大于partialChunkSize时才会执行</li>
|
||||
* <li>读取文件最后的partialChunkSize字节</li>
|
||||
* <li>这些字节会被追加到MD5计算中</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Override
|
||||
public String calculatePartialHash(Path file, int partialChunkSize) throws IOException {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
|
||||
try (var channel = Files.newByteChannel(file)) {
|
||||
long size = channel.size();
|
||||
|
||||
// Read head
|
||||
if (size > 0) {
|
||||
int chunkSize = (int) Math.min(partialChunkSize, size);
|
||||
ByteBuffer buffer = ByteBuffer.allocate(chunkSize);
|
||||
channel.read(buffer);
|
||||
buffer.flip();
|
||||
digest.update(buffer);
|
||||
}
|
||||
|
||||
// Read middle
|
||||
if (size > partialChunkSize * 2L) {
|
||||
long midPos = size / 2 - partialChunkSize / 2;
|
||||
channel.position(midPos);
|
||||
ByteBuffer buffer = ByteBuffer.allocate(partialChunkSize);
|
||||
channel.read(buffer);
|
||||
buffer.flip();
|
||||
digest.update(buffer);
|
||||
}
|
||||
|
||||
// Read tail
|
||||
if (size > partialChunkSize) {
|
||||
long endPos = size - partialChunkSize;
|
||||
channel.position(endPos);
|
||||
ByteBuffer buffer = ByteBuffer.allocate(partialChunkSize);
|
||||
channel.read(buffer);
|
||||
buffer.flip();
|
||||
digest.update(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
return bytesToHex(digest.digest());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("MD5算法不可用", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,241 @@
|
|||
package top.r3944realms.docchecktoolrefactored.core;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@Slf4j
|
||||
public class PhysicalAddressFileGenerator implements AddressFileGenerator {
|
||||
@Override
|
||||
public void generateAddressFile(String folderPath, File outputFile, int folderType, Callback callback) {
|
||||
callback.onProgress("正在生成物理地址文件...");
|
||||
try {
|
||||
File rootFolder = new File(folderPath);
|
||||
if (!rootFolder.exists() || !rootFolder.isDirectory()) {
|
||||
callback.onError("所选路径不存在或不是一个有效的文件夹。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存输出文件的绝对路径,用于后续比较
|
||||
String outputFilePath = outputFile.getAbsolutePath();
|
||||
|
||||
// 写入CSV文件
|
||||
try (PrintWriter writer = new PrintWriter(outputFile, StandardCharsets.UTF_8)) {
|
||||
if (folderType == AddressFileGenerator.PAGE_TYPE) {
|
||||
// 页面级逻辑:处理所有图片文件
|
||||
writer.println("物理文件名,物理地址");
|
||||
processPageLevelFolder(rootFolder, writer, outputFilePath);
|
||||
} else if (folderType == AddressFileGenerator.FILE_TYPE) {
|
||||
// 文件级逻辑:处理PDF文件
|
||||
writer.println("物理文件名,物理地址,页数");
|
||||
processFileLevelFolder(rootFolder, writer, outputFilePath);
|
||||
} else {
|
||||
throw new IllegalArgumentException("不支持的文件夹类型: " + folderType);
|
||||
}
|
||||
}
|
||||
|
||||
callback.onSuccess("物理地址文件生成成功: " + outputFile.getAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
callback.onError("生成物理地址文件时出错: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 处理页面级文件夹及其内部文件
|
||||
*
|
||||
* @param folder 文件夹
|
||||
* @param writer PrintWriter对象
|
||||
* @param outputFilePath 输出文件的绝对路径
|
||||
*/
|
||||
private void processPageLevelFolder(File folder, PrintWriter writer, String outputFilePath) {
|
||||
// 获取该文件夹下的所有非隐藏文件和文件夹
|
||||
File[] filesAndFolders = folder.listFiles(file -> !file.isHidden());
|
||||
|
||||
if (filesAndFolders != null) {
|
||||
for (File file : filesAndFolders) {
|
||||
// 跳过输出文件本身,避免将生成的CSV文件也作为数据处理
|
||||
if (file.getAbsolutePath().equals(outputFilePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.isFile()) {
|
||||
String fileName = file.getName();
|
||||
// 只处理图片文件,跳过其他类型的文件
|
||||
if (!isImageFile(fileName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 移除文件扩展名
|
||||
String fileNameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'));
|
||||
|
||||
// 格式化文件名,确保最后一个部分是4位数字
|
||||
String formattedFileName = formatFileName(fileNameWithoutExt);
|
||||
|
||||
// 生成物理地址路径
|
||||
String physicalAddress = generatePhysicalAddress(file.getAbsolutePath(), formattedFileName);
|
||||
|
||||
// 写入CSV行
|
||||
writer.printf("%s,%s%n", formattedFileName, physicalAddress);
|
||||
} else if (file.isDirectory()) {
|
||||
// 递归处理子文件夹
|
||||
processPageLevelFolder(file, writer, outputFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文件级文件夹(处理PDF文件)
|
||||
*
|
||||
* @param folder 文件夹
|
||||
* @param writer PrintWriter对象
|
||||
* @param outputFilePath 输出文件的绝对路径
|
||||
*/
|
||||
private void processFileLevelFolder(File folder, PrintWriter writer, String outputFilePath) {
|
||||
// 获取该文件夹下的所有非隐藏文件和文件夹
|
||||
File[] filesAndFolders = folder.listFiles(file -> !file.isHidden());
|
||||
|
||||
if (filesAndFolders != null) {
|
||||
for (File file : filesAndFolders) {
|
||||
// 跳过输出文件本身,避免将生成的CSV文件也作为数据处理
|
||||
if (file.getAbsolutePath().equals(outputFilePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.isFile()) {
|
||||
String fileName = file.getName();
|
||||
// 只处理PDF文件
|
||||
if (isPdfFile(fileName)) {
|
||||
// 移除文件扩展名
|
||||
String fileNameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'));
|
||||
|
||||
// 生成物理地址路径(使用与页面级相同的逻辑)
|
||||
String physicalAddress = generatePhysicalAddress(file.getAbsolutePath(), fileNameWithoutExt);
|
||||
|
||||
// 获取PDF页数
|
||||
int pageCount = getPdfPageCount(file);
|
||||
|
||||
// 写入CSV行
|
||||
writer.printf("%s,%s,%d%n", fileNameWithoutExt, physicalAddress, pageCount);
|
||||
}
|
||||
} else if (file.isDirectory()) {
|
||||
// 递归处理子文件夹
|
||||
processFileLevelFolder(file, writer, outputFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取PDF文件的页数
|
||||
*
|
||||
* @param pdfFile PDF文件
|
||||
* @return 页数
|
||||
*/
|
||||
private int getPdfPageCount(File pdfFile) {
|
||||
try {
|
||||
// 使用Apache PDFBox库获取PDF页数
|
||||
PDDocument document = Loader.loadPDF(pdfFile);
|
||||
int pageCount = document.getNumberOfPages();
|
||||
document.close();
|
||||
return pageCount;
|
||||
} catch (Exception e) {
|
||||
log.warn("无法获取PDF文件页数: {}", pdfFile.getAbsolutePath(), e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断是否为PDF文件
|
||||
*
|
||||
* @param fileName 文件名
|
||||
* @return 是否为PDF文件
|
||||
*/
|
||||
private boolean isPdfFile(String fileName) {
|
||||
return fileName.toLowerCase().endsWith(".pdf");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为图片文件
|
||||
*
|
||||
* @param fileName 文件名
|
||||
* @return 是否为图片文件
|
||||
*/
|
||||
private boolean isImageFile(String fileName) {
|
||||
String lowerFileName = fileName.toLowerCase();
|
||||
return lowerFileName.endsWith(".jpg") || lowerFileName.endsWith(".jpeg") ||
|
||||
lowerFileName.endsWith(".png") || lowerFileName.endsWith(".bmp") ||
|
||||
lowerFileName.endsWith(".gif") || lowerFileName.endsWith(".tiff");
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件名,确保最后一个部分是4位数字
|
||||
*
|
||||
* @param fileName 文件名(不含扩展名)
|
||||
* @return 格式化后的文件名
|
||||
*/
|
||||
private String formatFileName(String fileName) {
|
||||
int lastDashIndex = fileName.lastIndexOf('-');
|
||||
if (lastDashIndex != -1 && lastDashIndex < fileName.length() - 1) {
|
||||
String lastPart = fileName.substring(lastDashIndex + 1);
|
||||
// 检查是否为数字
|
||||
if (lastPart.matches("\\d+")) {
|
||||
// 格式化为4位数字
|
||||
int number = Integer.parseInt(lastPart);
|
||||
String formattedLastPart = String.format("%04d", number);
|
||||
return fileName.substring(0, lastDashIndex + 1) + formattedLastPart;
|
||||
}
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成物理地址路径
|
||||
*
|
||||
* @param absolutePath 文件的绝对路径
|
||||
* @param fileName 文件名(不含扩展名)
|
||||
* @return 物理地址路径
|
||||
*/
|
||||
private String generatePhysicalAddress(String absolutePath, String fileName) {
|
||||
// 获取文件名第一个'-'前面的内容(如9027)
|
||||
int firstDashIndex = fileName.indexOf('-');
|
||||
String prefix = firstDashIndex != -1 ? fileName.substring(0, firstDashIndex) : fileName;
|
||||
|
||||
// 构建从根路径开始的相对路径
|
||||
String relativePath = absolutePath.replace('\\', '/');
|
||||
String[] pathParts = relativePath.split("/");
|
||||
|
||||
StringBuilder resultPath = new StringBuilder();
|
||||
boolean foundPrefix = false;
|
||||
|
||||
for (int i = 0; i < pathParts.length; i++) {
|
||||
if (!foundPrefix) {
|
||||
// 检查当前部分是否包含prefix
|
||||
if (pathParts[i].contains(prefix)) {
|
||||
foundPrefix = true;
|
||||
resultPath.append(pathParts[i]);
|
||||
}
|
||||
} else {
|
||||
// 去掉文件类型的后缀(如.jpg)
|
||||
if (i == pathParts.length - 1) {
|
||||
String fileNameWithoutExt = pathParts[i].substring(0, pathParts[i].lastIndexOf('.'));
|
||||
resultPath.append("/").append(fileNameWithoutExt);
|
||||
} else {
|
||||
resultPath.append("/").append(pathParts[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果结果路径为空,返回空字符串
|
||||
if (resultPath.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 将路径中的 / 替换为 \(适用于 Windows 系统)
|
||||
return resultPath.toString().replace('/', '\\');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package top.r3944realms.docchecktoolrefactored.deprecated;
|
||||
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
public abstract class BaseFunctionPanel extends Pane {
|
||||
protected DocCheckToolMainFrame mainFrame;
|
||||
protected TextArea resultTextArea;
|
||||
protected static final Color PRIMARY_COLOR = Color.rgb(66, 133, 244);
|
||||
protected static final Color SECONDARY_COLOR = Color.rgb(30, 169, 80);
|
||||
protected static final Color BACKGROUND_COLOR = Color.rgb(245, 245, 245);
|
||||
protected static final Color BORDER_COLOR = Color.rgb(222, 226, 230);
|
||||
|
||||
public BaseFunctionPanel(DocCheckToolMainFrame mainFrame) {
|
||||
this.mainFrame = mainFrame;
|
||||
setStyle("-fx-background-color: rgb(245,245,245);");
|
||||
initComponents();
|
||||
}
|
||||
|
||||
protected abstract void initComponents();
|
||||
|
||||
public void showStatusMessage(String message) {
|
||||
if (resultTextArea != null) {
|
||||
resultTextArea.appendText(message + "\n");
|
||||
resultTextArea.positionCaret(resultTextArea.getText().length());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
package top.r3944realms.docchecktoolrefactored.deprecated;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class DocCheckToolMainFrame {
|
||||
private StackPane cardPanel;
|
||||
private static final Color PRIMARY_COLOR = Color.rgb(66, 133, 244);
|
||||
private static final Color BACKGROUND_COLOR = Color.rgb(245, 245, 245);
|
||||
|
||||
public void show(Stage primaryStage) {
|
||||
primaryStage.setTitle("淮阴区数字化档案检查验收系统");
|
||||
primaryStage.setWidth(1200);
|
||||
primaryStage.setHeight(600);
|
||||
primaryStage.setResizable(false);
|
||||
|
||||
// Create top button panel
|
||||
HBox topButtonPanel = new HBox(10);
|
||||
topButtonPanel.setPadding(new Insets(10));
|
||||
topButtonPanel.setAlignment(Pos.CENTER_LEFT);
|
||||
topButtonPanel.setStyle("-fx-background-color: rgb(245,245,245);");
|
||||
|
||||
String[] buttonTexts = {"1 查重复文件", "2 查遗漏、存储路径和命名规范", "3 查质量",
|
||||
"4 元数据", "5 查数据挂接", "6 查工作文件", "7 查存储载体"};
|
||||
|
||||
for (String text : buttonTexts) {
|
||||
Button btn = new Button(text);
|
||||
btn.setFont(Font.font("Microsoft YaHei", 14));
|
||||
btn.setTextFill(Color.WHITE);
|
||||
btn.setStyle("-fx-background-color: rgb(66,133,244); -fx-padding: 5 15 5 15;");
|
||||
btn.setOnAction(e -> switchPanel(text));
|
||||
topButtonPanel.getChildren().add(btn);
|
||||
}
|
||||
|
||||
// Initialize card panel
|
||||
cardPanel = new StackPane();
|
||||
cardPanel.setStyle("-fx-background-color: rgb(245,245,245);");
|
||||
|
||||
// Add all function panels to card panel
|
||||
cardPanel.getChildren().addAll(
|
||||
new DuplicateCheckPanel(this),
|
||||
new PathCheckPanel(this),
|
||||
new OtherFunctionPanel(" 3 查质量"),
|
||||
new OtherFunctionPanel("4 元数据"),
|
||||
new OtherFunctionPanel("5 查数据挂接"),
|
||||
new OtherFunctionPanel("6 查工作文件"),
|
||||
new OtherFunctionPanel("7 查存储载体")
|
||||
);
|
||||
|
||||
// Initially show the first panel
|
||||
switchPanel("1 查重复文件");
|
||||
|
||||
// Create main layout
|
||||
BorderPane root = new BorderPane();
|
||||
root.setTop(topButtonPanel);
|
||||
root.setCenter(cardPanel);
|
||||
root.setStyle("-fx-background-color: rgb(245,245,245);");
|
||||
|
||||
primaryStage.setScene(new Scene(root));
|
||||
primaryStage.show();
|
||||
}
|
||||
|
||||
private void switchPanel(String panelName) {
|
||||
for (javafx.scene.Node node : cardPanel.getChildren()) {
|
||||
node.setVisible(false);
|
||||
}
|
||||
|
||||
int index = switch (panelName) {
|
||||
case "1 查重复文件" -> 0;
|
||||
case "2 查遗漏、存储路径和命名规范" -> 1;
|
||||
case "3 查质量" -> 2;
|
||||
case "4 元数据" -> 3;
|
||||
case "5 查数据挂接" -> 4;
|
||||
case "6 查工作文件" -> 5;
|
||||
case "7 查存储载体" -> 6;
|
||||
default -> 0;
|
||||
};
|
||||
|
||||
cardPanel.getChildren().get(index).setVisible(true);
|
||||
}
|
||||
|
||||
public void showStatusMessage(String message) {
|
||||
for (javafx.scene.Node node : cardPanel.getChildren()) {
|
||||
if (node instanceof BaseFunctionPanel && node.isVisible()) {
|
||||
((BaseFunctionPanel) node).showStatusMessage(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void showErrorMessage(String message) {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("错误");
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
showStatusMessage("【错误】" + message);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
package top.r3944realms.docchecktoolrefactored.deprecated;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.FontWeight;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.*;
|
||||
|
||||
public class DuplicateCheckPanel extends BaseFunctionPanel {
|
||||
private TextField folderPathTextField;
|
||||
private TextArea feedbackTextArea;
|
||||
private Set<String> duplicateFolders = new HashSet<>();
|
||||
|
||||
public DuplicateCheckPanel(DocCheckToolMainFrame mainFrame) {
|
||||
super(mainFrame);
|
||||
initComponents();
|
||||
}
|
||||
|
||||
protected void initComponents() {
|
||||
// Main layout with padding
|
||||
this.setPadding(new Insets(20));
|
||||
|
||||
// Folder selection components
|
||||
HBox folderSelectBox = new HBox(10);
|
||||
folderSelectBox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
Label loadFolderLabel = new Label("载入文件夹");
|
||||
loadFolderLabel.setFont(Font.font("Microsoft YaHei", 14));
|
||||
loadFolderLabel.setTextFill(Color.DARKGRAY);
|
||||
|
||||
folderPathTextField = new TextField();
|
||||
folderPathTextField.setEditable(false);
|
||||
folderPathTextField.setFont(Font.font("Microsoft YaHei", 14));
|
||||
folderPathTextField.setStyle("-fx-background-color: white; -fx-border-color: #CCCCCC; -fx-border-width: 1; -fx-padding: 5 10 5 10;");
|
||||
HBox.setHgrow(folderPathTextField, Priority.ALWAYS);
|
||||
|
||||
Button selectFolderButton = new Button("选择文件夹");
|
||||
selectFolderButton.setFont(Font.font("Microsoft YaHei", 14));
|
||||
selectFolderButton.setTextFill(Color.WHITE);
|
||||
selectFolderButton.setStyle("-fx-background-color: #4285F4; -fx-padding: 5 15 5 15;");
|
||||
selectFolderButton.setOnAction(e -> {
|
||||
DirectoryChooser directoryChooser = new DirectoryChooser();
|
||||
directoryChooser.setTitle("选择要检查的文件夹");
|
||||
File selectedFolder = directoryChooser.showDialog(new Stage());
|
||||
if (selectedFolder != null) {
|
||||
folderPathTextField.setText(selectedFolder.getAbsolutePath());
|
||||
}
|
||||
});
|
||||
|
||||
folderSelectBox.getChildren().addAll(loadFolderLabel, folderPathTextField, selectFolderButton);
|
||||
|
||||
Button startCheckButton = new Button("开始检查");
|
||||
startCheckButton.setFont(Font.font("Microsoft YaHei", FontWeight.BOLD, 14));
|
||||
startCheckButton.setTextFill(Color.WHITE);
|
||||
startCheckButton.setStyle("-fx-background-color: #4285F4; -fx-padding: 5 20 5 20;");
|
||||
startCheckButton.setOnAction(e -> {
|
||||
String folderPath = folderPathTextField.getText();
|
||||
if (!folderPath.isEmpty()) {
|
||||
feedbackTextArea.setText("");
|
||||
File folder = new File(folderPath);
|
||||
duplicateFolders.clear();
|
||||
checkDuplicateFiles(folder);
|
||||
if (duplicateFolders.isEmpty()) {
|
||||
feedbackTextArea.setText("反馈结果:\n如无重复文件,则反馈:无重复文件;\n如有重复文件,则在此反馈档号。\n\n当前:无重复文件");
|
||||
} else {
|
||||
StringBuilder result = new StringBuilder("反馈结果:\n如无重复文件,则反馈:无重复文件;\n如有重复文件,则在此反馈档号。\n\n当前:");
|
||||
for (String folderName : duplicateFolders) {
|
||||
result.append("\n").append(folderName);
|
||||
}
|
||||
feedbackTextArea.setText(result.toString());
|
||||
}
|
||||
} else {
|
||||
mainFrame.showErrorMessage("请先选择一个文件夹!");
|
||||
}
|
||||
});
|
||||
|
||||
// Feedback area
|
||||
feedbackTextArea = new TextArea();
|
||||
feedbackTextArea.setEditable(false);
|
||||
feedbackTextArea.setFont(Font.font("Microsoft YaHei", 14));
|
||||
feedbackTextArea.setText("反馈结果:\n如无重复文件,则反馈:无重复文件;\n如有重复文件,则在此反馈档号。");
|
||||
feedbackTextArea.setStyle("-fx-background-color: white; -fx-border-color: #CCCCCC; -fx-border-width: 1; -fx-padding: 10;");
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane(feedbackTextArea);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setFitToHeight(true);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
|
||||
|
||||
// Main layout
|
||||
VBox mainBox = new VBox(10);
|
||||
mainBox.getChildren().addAll(folderSelectBox, startCheckButton, scrollPane);
|
||||
this.getChildren().add(mainBox);
|
||||
|
||||
// Set result text area reference
|
||||
resultTextArea = feedbackTextArea;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively checks for duplicate fileMetadata in the specified folder
|
||||
*/
|
||||
private void checkDuplicateFiles(File folder) {
|
||||
Map<String, List<File>> fileHashMap = new HashMap<>();
|
||||
if (folder.isDirectory()) {
|
||||
File[] files = folder.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
checkDuplicateFiles(file);
|
||||
} else {
|
||||
try {
|
||||
String md5Hash = calculateMD5(file);
|
||||
fileHashMap.computeIfAbsent(md5Hash, k -> new ArrayList<>()).add(file);
|
||||
} catch (IOException | NoSuchAlgorithmException ex) {
|
||||
ex.printStackTrace();
|
||||
mainFrame.showErrorMessage("计算文件哈希值出错:" + ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (List<File> fileList : fileHashMap.values()) {
|
||||
if (fileList.size() > 1) {
|
||||
for (File file : fileList) {
|
||||
File parentFolder = file.getParentFile();
|
||||
if (parentFolder != null) {
|
||||
duplicateFolders.add(parentFolder.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the MD5 hash of a file
|
||||
*/
|
||||
private String calculateMD5(File file) throws IOException, NoSuchAlgorithmException {
|
||||
MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = fis.read(buffer)) != -1) {
|
||||
digest.update(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
byte[] hashBytes = digest.digest();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : hashBytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
package top.r3944realms.docchecktoolrefactored.deprecated;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Font;
|
||||
|
||||
public class OtherFunctionPanel extends BaseFunctionPanel {
|
||||
private String title;
|
||||
|
||||
public OtherFunctionPanel(String title) {
|
||||
super(null);
|
||||
this.title = title;
|
||||
initComponents();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initComponents() {
|
||||
// Set padding
|
||||
this.setPadding(new Insets(50));
|
||||
|
||||
//Create tip label
|
||||
Label tipLabel = new Label("【" + "" +"】功能界面待完善..."+title);
|
||||
tipLabel.setFont(Font.font("Microsoft YaHei", 28));
|
||||
tipLabel.setTextFill(javafx.scene.paint.Color.DARKGRAY);
|
||||
|
||||
// Use VBox for vertical layout and center alignment
|
||||
VBox container = new VBox(tipLabel);
|
||||
container.setAlignment(Pos.CENTER);
|
||||
|
||||
// Add to the panel
|
||||
this.getChildren().add(container);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,287 +0,0 @@
|
|||
package top.r3944realms.docchecktoolrefactored.deprecated;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.FontWeight;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class PathCheckPanel extends BaseFunctionPanel {
|
||||
private TextField physicalFolderPathTextField;
|
||||
private TextField logicalFolderPathTextField;
|
||||
private TextArea checkResultTextArea;
|
||||
private Path physicalFolderPath;
|
||||
private Path logicalFolderPath;
|
||||
private List<String> physicalPaths = new ArrayList<>();
|
||||
private List<String> logicalPaths = new ArrayList<>();
|
||||
|
||||
public PathCheckPanel(DocCheckToolMainFrame mainFrame) {
|
||||
super(mainFrame);
|
||||
initComponents();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initComponents() {
|
||||
// Main layout with spacing
|
||||
VBox mainLayout = new VBox(15);
|
||||
mainLayout.setPadding(new Insets(20));
|
||||
|
||||
// Physical path selection
|
||||
TitledPane physicalPathPane = createPathPane(
|
||||
"物理存储路径",
|
||||
"选择物理文件夹",
|
||||
"生成物理存储路径文件",
|
||||
this::selectPhysicalFolder,
|
||||
this::generatePhysicalPathFile
|
||||
);
|
||||
physicalFolderPathTextField = (TextField) ((HBox) physicalPathPane.getContent()).getChildren().get(0);
|
||||
|
||||
// Logical path selection
|
||||
TitledPane logicalPathPane = createPathPane(
|
||||
"逻辑存储路径",
|
||||
"选择逻辑文件夹",
|
||||
"生成逻辑存储路径文件",
|
||||
this::selectLogicalFolder,
|
||||
this::generateLogicalPathFile
|
||||
);
|
||||
logicalFolderPathTextField = (TextField) ((HBox) logicalPathPane.getContent()).getChildren().get(0);
|
||||
|
||||
// Check buttons
|
||||
HBox buttonBox = new HBox(15);
|
||||
buttonBox.setAlignment(Pos.CENTER);
|
||||
|
||||
Button checkFilesButton = new Button("检查文件");
|
||||
stylePrimaryButton(checkFilesButton, 16);
|
||||
checkFilesButton.setOnAction(e -> checkFiles());
|
||||
|
||||
Button checkDirButton = new Button("检查目录");
|
||||
checkDirButton.setFont(Font.font("Microsoft YaHei", FontWeight.BOLD, 16));
|
||||
checkDirButton.setTextFill(Color.WHITE);
|
||||
checkDirButton.setStyle("-fx-background-color: #ef4444; -fx-border-color: #dc2626; -fx-border-width: 2; -fx-padding: 8 30 8 30;");
|
||||
checkDirButton.setOnAction(e -> checkDirectory());
|
||||
|
||||
buttonBox.getChildren().addAll(checkFilesButton, checkDirButton);
|
||||
|
||||
// Result area
|
||||
checkResultTextArea = new TextArea();
|
||||
checkResultTextArea.setEditable(false);
|
||||
checkResultTextArea.setFont(Font.font("Microsoft YaHei", 14));
|
||||
checkResultTextArea.setText("检查结果将显示在这里...\n请先选择文件夹并生成路径文件");
|
||||
checkResultTextArea.setStyle("-fx-background-color: white; -fx-border-color: #dee2e6; -fx-border-width: 1; -fx-padding: 10;");
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane(checkResultTextArea);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setFitToHeight(true);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
|
||||
|
||||
// Assemble main layout
|
||||
VBox pathPanes = new VBox(15, physicalPathPane, logicalPathPane);
|
||||
mainLayout.getChildren().addAll(pathPanes, scrollPane, buttonBox);
|
||||
this.getChildren().add(mainLayout);
|
||||
}
|
||||
|
||||
private TitledPane createPathPane(String title, String selectBtnText, String generateBtnText,
|
||||
Runnable selectAction, Runnable generateAction) {
|
||||
TextField pathField = new TextField();
|
||||
pathField.setEditable(false);
|
||||
pathField.setFont(Font.font("Microsoft YaHei", 14));
|
||||
pathField.setStyle("-fx-background-color: white; -fx-border-color: #dee2e6; -fx-border-width: 1; -fx-padding: 5 10 5 10;");
|
||||
|
||||
Button selectButton = new Button(selectBtnText);
|
||||
stylePrimaryButton(selectButton, 14);
|
||||
selectButton.setOnAction(e -> selectAction.run());
|
||||
|
||||
Button generateButton = new Button(generateBtnText);
|
||||
stylePrimaryButton(generateButton, 14);
|
||||
generateButton.setDisable(true);
|
||||
generateButton.setOnAction(e -> generateAction.run());
|
||||
|
||||
HBox content = new HBox(10, pathField, selectButton, generateButton);
|
||||
content.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
TitledPane pane = new TitledPane(title, content);
|
||||
pane.setFont(Font.font("Microsoft YaHei", 14));
|
||||
pane.setStyle("-fx-border-color: #dee2e6; -fx-border-width: 1;");
|
||||
|
||||
return pane;
|
||||
}
|
||||
|
||||
private void stylePrimaryButton(Button button, double fontSize) {
|
||||
button.setFont(Font.font("Microsoft YaHei", FontWeight.BOLD, fontSize));
|
||||
button.setTextFill(Color.WHITE);
|
||||
button.setStyle("-fx-background-color: #4285f4; -fx-border-color: #3b71ca; -fx-border-width: 2; -fx-padding: 8 30 8 30;");
|
||||
}
|
||||
|
||||
private void selectPhysicalFolder() {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.setTitle("选择物理存储文件夹");
|
||||
File selectedFolder = chooser.showDialog(new Stage());
|
||||
if (selectedFolder != null) {
|
||||
physicalFolderPathTextField.setText(selectedFolder.getAbsolutePath());
|
||||
physicalFolderPath = selectedFolder.toPath();
|
||||
|
||||
try {
|
||||
collectFilePaths(physicalFolderPath, physicalPaths);
|
||||
showStatusMessage("已收集 " + physicalPaths.size() + " 个物理文件路径");
|
||||
enableGenerateButton(physicalFolderPathTextField, true);
|
||||
} catch (IOException e) {
|
||||
mainFrame.showErrorMessage("读取物理文件夹失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void selectLogicalFolder() {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.setTitle("选择逻辑存储文件夹");
|
||||
File selectedFolder = chooser.showDialog(new Stage());
|
||||
if (selectedFolder != null) {
|
||||
logicalFolderPathTextField.setText(selectedFolder.getAbsolutePath());
|
||||
logicalFolderPath = selectedFolder.toPath();
|
||||
|
||||
try {
|
||||
collectFilePaths(logicalFolderPath, logicalPaths);
|
||||
showStatusMessage("已收集 " + logicalPaths.size() + " 个逻辑文件路径");
|
||||
enableGenerateButton(logicalFolderPathTextField, true);
|
||||
} catch (IOException e) {
|
||||
mainFrame.showErrorMessage("读取逻辑文件夹失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void enableGenerateButton(TextField pathField, boolean enable) {
|
||||
// Implementation would find the generate button and enable it
|
||||
// This depends on your exact UI structure
|
||||
}
|
||||
|
||||
private void generatePhysicalPathFile() {
|
||||
if (physicalFolderPath == null || physicalPaths.isEmpty()) {
|
||||
mainFrame.showErrorMessage("请先选择有效的物理文件夹!");
|
||||
return;
|
||||
}
|
||||
|
||||
Path outputPath = Paths.get("物理存储路径文件.txt");
|
||||
writePathsToFile(physicalPaths, outputPath);
|
||||
showStatusMessage("物理存储路径文件已生成:" + outputPath.toAbsolutePath());
|
||||
}
|
||||
|
||||
private void generateLogicalPathFile() {
|
||||
if (logicalFolderPath == null || logicalPaths.isEmpty()) {
|
||||
mainFrame.showErrorMessage("请先选择有效的逻辑文件夹!");
|
||||
return;
|
||||
}
|
||||
|
||||
Path outputPath = Paths.get("逻辑存储路径文件.txt");
|
||||
writePathsToFile(logicalPaths, outputPath);
|
||||
showStatusMessage("逻辑存储路径文件已生成:" + outputPath.toAbsolutePath());
|
||||
}
|
||||
|
||||
// The following methods remain largely unchanged as they deal with file operations:
|
||||
private void collectFilePaths(Path folderPath, List<String> paths) throws IOException {
|
||||
paths.clear();
|
||||
try (Stream<Path> walk = Files.walk(folderPath)) {
|
||||
paths.addAll(walk
|
||||
.filter(Files::isRegularFile)
|
||||
.map(p -> folderPath.relativize(p).toString())
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
|
||||
private void writePathsToFile(List<String> paths, Path outputPath) {
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputPath.toFile()))) {
|
||||
for (String path : paths) {
|
||||
writer.write(path);
|
||||
writer.newLine();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
mainFrame.showErrorMessage("写入文件失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void checkFiles() {
|
||||
if (physicalPaths.isEmpty() || logicalPaths.isEmpty()) {
|
||||
mainFrame.showErrorMessage("请先选择并生成物理和逻辑路径文件!");
|
||||
return;
|
||||
}
|
||||
|
||||
clearResultArea();
|
||||
showStatusMessage("正在进行文件检查...");
|
||||
|
||||
Set<String> missingInPhysical = new HashSet<>();
|
||||
for (String logicalPath : logicalPaths) {
|
||||
if (!physicalPaths.contains(logicalPath)) {
|
||||
missingInPhysical.add(logicalPath);
|
||||
}
|
||||
}
|
||||
|
||||
displayCheckResult("文件检查", logicalPaths.size(), missingInPhysical, null);
|
||||
}
|
||||
|
||||
private void checkDirectory() {
|
||||
if (physicalPaths.isEmpty() || logicalPaths.isEmpty()) {
|
||||
mainFrame.showErrorMessage("请先选择并生成物理和逻辑路径文件!");
|
||||
return;
|
||||
}
|
||||
|
||||
clearResultArea();
|
||||
showStatusMessage("正在进行目录检查...");
|
||||
|
||||
Set<String> missingInLogical = new HashSet<>();
|
||||
for (String physicalPath : physicalPaths) {
|
||||
if (!logicalPaths.contains(physicalPath)) {
|
||||
missingInLogical.add(physicalPath);
|
||||
}
|
||||
}
|
||||
|
||||
displayCheckResult("目录检查", physicalPaths.size(), null, missingInLogical);
|
||||
}
|
||||
|
||||
private void displayCheckResult(String checkType, int totalCount,
|
||||
Set<String> missingInPhysical, Set<String> missingInLogical) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append("=== ").append(checkType).append(" ===\n");
|
||||
result.append("共检查 ").append(totalCount).append(" 个").append(checkType.equals("文件检查") ? "文件" : "目录项").append("\n\n");
|
||||
|
||||
if (checkType.equals("文件检查")) {
|
||||
if (missingInPhysical.isEmpty()) {
|
||||
result.append("✓ 未发现问题:所有逻辑文件在物理存储中均存在\n");
|
||||
} else {
|
||||
result.append("! 发现 ").append(missingInPhysical.size()).append(" 个问题:\n");
|
||||
missingInPhysical.forEach(path -> result.append(" - 物理存储中缺失文件:").append(path).append("\n"));
|
||||
}
|
||||
} else {
|
||||
if (missingInLogical.isEmpty()) {
|
||||
result.append("✓ 未发现问题:所有物理文件在逻辑目录中均存在\n");
|
||||
} else {
|
||||
result.append("! 发现 ").append(missingInLogical.size()).append(" 个问题:\n");
|
||||
missingInLogical.forEach(path -> result.append(" - 逻辑目录中缺失文件:").append(path).append("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
checkResultTextArea.appendText(result.toString());
|
||||
checkResultTextArea.appendText("\n\n");
|
||||
showStatusMessage(checkType + "完成");
|
||||
}
|
||||
|
||||
private void clearResultArea() {
|
||||
checkResultTextArea.clear();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package top.r3944realms.docchecktoolrefactored.io.reader;
|
||||
|
||||
|
||||
import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 目录文件读取器接口
|
||||
*/
|
||||
public interface CatalogFileReader {
|
||||
/**
|
||||
* 读取目录文件
|
||||
*
|
||||
* @param filePath 文件路径
|
||||
* @return 记录列表
|
||||
* @throws Exception 读取异常
|
||||
*/
|
||||
List<LogicalAddressFileGenerator.Record> readCatalogFile(String filePath) throws Exception;
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package top.r3944realms.docchecktoolrefactored.io.reader;
|
||||
|
||||
/**
|
||||
* 目录文件读取器工厂类
|
||||
*/
|
||||
public class CatalogFileReaderFactory {
|
||||
/**
|
||||
* 根据文件扩展名创建相应的文件读取器
|
||||
*
|
||||
* @param filePath 文件路径
|
||||
* @return 文件读取器实例
|
||||
*/
|
||||
public static CatalogFileReader createReader(String filePath) {
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
throw new IllegalArgumentException("文件路径不能为空");
|
||||
}
|
||||
|
||||
String lowerPath = filePath.toLowerCase();
|
||||
if (lowerPath.endsWith(".xlsx") || lowerPath.endsWith(".xls")) {
|
||||
return new ExcelFileReader();
|
||||
} else if (lowerPath.endsWith(".xml")) {
|
||||
return new XmlFileReader();
|
||||
} else if (lowerPath.endsWith(".dbf")) {
|
||||
return new DbfFileReader();
|
||||
} else {
|
||||
throw new IllegalArgumentException("不支持的文件格式: " + filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package top.r3944realms.docchecktoolrefactored.io.reader;
|
||||
|
||||
import com.linuxense.javadbf.DBFReader;
|
||||
import com.linuxense.javadbf.DBFRow;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
|
||||
import top.r3944realms.docchecktoolrefactored.util.FileUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* DBF文件读取器实现
|
||||
*/
|
||||
@Slf4j
|
||||
public class DbfFileReader implements CatalogFileReader {
|
||||
// 常量定义(避免硬编码)
|
||||
private static final String FIELD_ARCHIVE_CODE = "档号";
|
||||
private static final String FIELD_PAGE = "页数";
|
||||
|
||||
@Override
|
||||
public List<LogicalAddressFileGenerator.Record> readCatalogFile(String filePath) throws Exception {
|
||||
|
||||
List<LogicalAddressFileGenerator.Record> records = new ArrayList<>();
|
||||
File file = FileUtil.fileCheckAndGet(filePath, "dbf");
|
||||
try (
|
||||
FileInputStream fis = new FileInputStream(file);
|
||||
DBFReader reader = new DBFReader(fis)
|
||||
) {
|
||||
int fieldCount = reader.getFieldCount();
|
||||
log.debug("开始读取DBF文件: {}, DBF文件字段数: {}",filePath, fieldCount);
|
||||
|
||||
// 查找"档号"和"页数"字段的索引
|
||||
int archiveCodeIndex = -1;
|
||||
int pageIndex = -1;
|
||||
for (int i = 0; i < fieldCount; i++) {
|
||||
if (archiveCodeIndex != -1 && pageIndex != -1) {
|
||||
log.debug("已找到所需字段,跳出循环,档号: {}, 页数: {}", archiveCodeIndex, pageIndex);
|
||||
break;
|
||||
}
|
||||
String fieldName = reader.getField(i).getName();
|
||||
log.debug("发现字段: {}", fieldName);
|
||||
if (FIELD_ARCHIVE_CODE.equals(fieldName)) {
|
||||
archiveCodeIndex = i;
|
||||
log.debug("找到‘档号’字段,索引: {}", archiveCodeIndex);
|
||||
} else if (FIELD_PAGE.equals(fieldName)) {
|
||||
pageIndex = i;
|
||||
log.debug("找到‘页数’字段,索引: {}", pageIndex);
|
||||
}
|
||||
|
||||
}
|
||||
if (archiveCodeIndex == -1 || pageIndex == -1) {
|
||||
log.error("未找到必要字段,档号: {}, 页数: {}",
|
||||
archiveCodeIndex == -1 ? "未找到" : archiveCodeIndex,
|
||||
pageIndex == -1 ? "未找到" : pageIndex
|
||||
);
|
||||
throw new IllegalArgumentException(
|
||||
String.format("DBF文件缺少必要字段: %s=%s, %s=%s",
|
||||
FIELD_ARCHIVE_CODE, archiveCodeIndex == -1,
|
||||
FIELD_PAGE, pageIndex == -1
|
||||
)
|
||||
);
|
||||
}
|
||||
int validRecords = 0;
|
||||
int skippedRecords = 0;
|
||||
DBFRow row;
|
||||
while ((row = reader.nextRow()) != null) {
|
||||
String archiveCode =
|
||||
Optional.ofNullable(row.getObject(archiveCodeIndex))
|
||||
.map(Object::toString)
|
||||
.map(String::trim)
|
||||
.orElse("");
|
||||
int page =
|
||||
Optional.ofNullable(row.getObject(pageIndex))
|
||||
.map(i -> {
|
||||
try {
|
||||
if (i instanceof Number) {
|
||||
return ((Number) i).intValue();
|
||||
} else {
|
||||
return Integer.parseInt(i.toString().trim());
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("无法将页数值转换为整数: {}", i);
|
||||
return 0;
|
||||
}
|
||||
}).orElse(0);
|
||||
|
||||
// 只有当档号不为空且页数大于0时才添加记录
|
||||
if (!archiveCode.isEmpty() && page > 0) {
|
||||
records.add(new LogicalAddressFileGenerator.Record(archiveCode, page));
|
||||
validRecords++;
|
||||
log.debug("读取有效记录 - 档号: {}, 页数: {}", archiveCode, page);
|
||||
} else {
|
||||
skippedRecords++;
|
||||
if (!archiveCode.isEmpty() || page > 0) {
|
||||
log.debug("跳过无效记录 - 档号: {}, 页数: {}", archiveCode, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("DBF文件读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords);
|
||||
} catch (IOException e) {
|
||||
log.error("读取DBF文件失败: {}", filePath, e);
|
||||
throw new UncheckedIOException("DBF文件读取异常", e);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
package top.r3944realms.docchecktoolrefactored.io.reader;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
|
||||
import top.r3944realms.docchecktoolrefactored.util.FileUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public class ExcelFileReader implements CatalogFileReader {
|
||||
// 常量定义(避免硬编码)
|
||||
private static final String FIELD_ARCHIVE_CODE = "档号";
|
||||
private static final String FIELD_PAGE = "页数";
|
||||
|
||||
@Override
|
||||
public List<LogicalAddressFileGenerator.Record> readCatalogFile(String filePath) throws Exception {
|
||||
File file = FileUtil.fileCheckAndGet(filePath, "xlsx", "xls");
|
||||
boolean isXlsx = file.getName().endsWith(".xlsx");
|
||||
return readExcelFile(file, isXlsx);
|
||||
}
|
||||
private List<LogicalAddressFileGenerator.Record> readExcelFile(File file, boolean isXlsx) throws Exception {
|
||||
List<LogicalAddressFileGenerator.Record> records = new ArrayList<>();
|
||||
log.debug("开始解析Excel文件,格式: {}", isXlsx ? "xlsx" : "xls");
|
||||
try (FileInputStream fis = new FileInputStream(file);
|
||||
Workbook workbook = isXlsx ? new XSSFWorkbook(fis) : new HSSFWorkbook(fis)) {
|
||||
// 获取第一个工作表
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
log.debug("读取工作表: {}", sheet.getSheetName());
|
||||
|
||||
// 获取标题行
|
||||
Row headerRow = sheet.getRow(0);
|
||||
if (headerRow == null) {
|
||||
log.error("Excel文件缺少标题行");
|
||||
throw new IllegalArgumentException("Excel文件缺少标题行");
|
||||
}
|
||||
// 查找"档号"和"页数"列的索引
|
||||
int archiveCodeIndex = -1;
|
||||
int pageIndex = -1;
|
||||
log.debug("开始查找'档号'和'页数'列的索引");
|
||||
boolean foundExactMatch = false;
|
||||
for (Cell cell : headerRow) {
|
||||
String cellValue = getCellValueAsString(cell).trim();
|
||||
if (FIELD_ARCHIVE_CODE.equals(cellValue)) {
|
||||
archiveCodeIndex = cell.getColumnIndex();
|
||||
log.debug("找到'档号'列,索引: {}", archiveCodeIndex);
|
||||
} else if (FIELD_PAGE.equals(cellValue)) {
|
||||
pageIndex = cell.getColumnIndex();
|
||||
foundExactMatch = true;
|
||||
log.debug("找到精确匹配'页数'列,索引: {}", pageIndex);
|
||||
}
|
||||
}
|
||||
// 如果没有精确匹配,进行模糊查找
|
||||
if (!foundExactMatch) {
|
||||
for (Cell cell : headerRow) {
|
||||
String cellValue = getCellValueAsString(cell).trim();
|
||||
if (cellValue.contains(FIELD_PAGE)) {
|
||||
pageIndex = cell.getColumnIndex();
|
||||
log.debug("找到模糊匹配'页数'列,索引: {}", pageIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 检查是否找到必需的列
|
||||
if (archiveCodeIndex == -1 || pageIndex == -1) {
|
||||
log.error("未找到必要字段,档号: {}, 页数: {}",
|
||||
archiveCodeIndex == -1 ? "未找到" : archiveCodeIndex,
|
||||
pageIndex == -1 ? "未找到" : pageIndex
|
||||
);
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Excel文件缺少必要字段: %s=%s, %s=%s",
|
||||
FIELD_ARCHIVE_CODE, archiveCodeIndex == -1,
|
||||
FIELD_PAGE, pageIndex == -1
|
||||
)
|
||||
);
|
||||
}
|
||||
// 从第二行开始读取数据(跳过标题行)
|
||||
int totalRows = sheet.getLastRowNum();
|
||||
int validRecords = 0;
|
||||
int skippedRecords = 0;
|
||||
|
||||
for (int i = 1; i <= totalRows; i++) {
|
||||
Row row = sheet.getRow(i);
|
||||
if (row == null) {
|
||||
skippedRecords++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 读取档号
|
||||
Cell archiveCodeCell = row.getCell(archiveCodeIndex);
|
||||
String archiveCode = "";
|
||||
if (archiveCodeCell != null) {
|
||||
archiveCode = getCellValueAsString(archiveCodeCell).trim();
|
||||
}
|
||||
|
||||
// 读取页数
|
||||
Cell pageCell = row.getCell(pageIndex);
|
||||
int page = 0;
|
||||
if (pageCell != null) {
|
||||
page = getCellValueAsInteger(pageCell);
|
||||
}
|
||||
|
||||
// 验证数据有效性
|
||||
if (archiveCode.isEmpty()) {
|
||||
throw new IllegalArgumentException("第" + ( i + 1 ) + "行档号为空,停止处理");
|
||||
}
|
||||
if (page <= 0) {
|
||||
throw new IllegalArgumentException("第" + ( i + 1 ) + "行页数无效: " + page + ",停止处理");
|
||||
}
|
||||
|
||||
// 只有数据有效时才添加记录
|
||||
records.add(new LogicalAddressFileGenerator.Record(archiveCode, page));
|
||||
validRecords++;
|
||||
log.debug("读取有效记录 - 档号: {}, 页数: {}", archiveCode, page);
|
||||
}
|
||||
|
||||
log.info("数据读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords);
|
||||
} catch (Exception e) {
|
||||
log.error("读取Excel文件时发生错误: {}", e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
return records;
|
||||
}
|
||||
/**
|
||||
* 获取单元格值作为字符串
|
||||
* @param cell 单元格
|
||||
* @return 字符串值
|
||||
*/
|
||||
private String getCellValueAsString(Cell cell) {
|
||||
if (cell == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
switch (cell.getCellType()) {
|
||||
case STRING:
|
||||
return cell.getStringCellValue();
|
||||
case NUMERIC:
|
||||
// 判断是否为日期格式
|
||||
if (DateUtil.isCellDateFormatted(cell)) {
|
||||
return cell.getDateCellValue().toString();
|
||||
} else {
|
||||
// 数值转换为字符串,避免科学计数法
|
||||
double numericValue = cell.getNumericCellValue();
|
||||
if (numericValue == Math.floor(numericValue)) {
|
||||
return String.valueOf((int) numericValue);
|
||||
} else {
|
||||
return String.valueOf(numericValue);
|
||||
}
|
||||
}
|
||||
case BOOLEAN:
|
||||
return String.valueOf(cell.getBooleanCellValue());
|
||||
case FORMULA:
|
||||
try {
|
||||
return cell.getStringCellValue();
|
||||
} catch (Exception e) {
|
||||
return cell.getCellFormula();
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单元格值作为整数
|
||||
* @param cell 单元格
|
||||
* @return 整数值
|
||||
*/
|
||||
private int getCellValueAsInteger(Cell cell) {
|
||||
if (cell == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (cell.getCellType()) {
|
||||
case NUMERIC:
|
||||
return (int) Math.round(cell.getNumericCellValue());
|
||||
case STRING:
|
||||
String stringValue = cell.getStringCellValue().trim();
|
||||
if (!stringValue.isEmpty()) {
|
||||
return Integer.parseInt(stringValue);
|
||||
}
|
||||
return 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("无法将单元格值转换为整数: {}", cell);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
package top.r3944realms.docchecktoolrefactored.io.reader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* The interface File reader.
|
||||
*/
|
||||
public interface FileReader {
|
||||
/**
|
||||
* 读取文件内容
|
||||
*
|
||||
* @param file 要读取的文件
|
||||
* @return 文件内容字节数组 byte [ ]
|
||||
* @throws IOException the io exception
|
||||
*/
|
||||
byte[] readFully(Path file) throws IOException;
|
||||
|
||||
/**
|
||||
* 流式读取文件内容
|
||||
*
|
||||
* @param file 要读取的文件
|
||||
* @param consumer 内容消费者
|
||||
* @throws IOException the io exception
|
||||
*/
|
||||
void readStreaming(Path file, ByteBufferConsumer consumer) throws IOException;
|
||||
|
||||
/**
|
||||
* The interface Byte buffer consumer.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface ByteBufferConsumer {
|
||||
/**
|
||||
* Consume.
|
||||
*
|
||||
* @param buffer the buffer
|
||||
* @param isLast the is last
|
||||
* @throws IOException the io exception
|
||||
*/
|
||||
void consume(ByteBuffer buffer, boolean isLast) throws IOException;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
package top.r3944realms.docchecktoolrefactored.io.reader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.MappedByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
/**
|
||||
* The type Memory mapped file reader.
|
||||
*/
|
||||
public class MemoryMappedFileReader implements FileReader {
|
||||
private static final long MAX_MMAP_SIZE = 1 << 30; // 1GB
|
||||
|
||||
@Override
|
||||
public byte[] readFully(Path file) throws IOException {
|
||||
long size = Files.size(file);
|
||||
if (size > MAX_MMAP_SIZE) {
|
||||
throw new IOException("File too large for memory mapping: " + file);
|
||||
}
|
||||
|
||||
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) {
|
||||
MappedByteBuffer buffer = channel.map(
|
||||
FileChannel.MapMode.READ_ONLY, 0, size);
|
||||
byte[] bytes = new byte[(int) size];
|
||||
buffer.get(bytes);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readStreaming(Path file, ByteBufferConsumer consumer) throws IOException {
|
||||
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) {
|
||||
long size = channel.size();
|
||||
long position = 0;
|
||||
long chunkSize = Math.min(size, 8 * 1024 * 1024); // 8MB chunks
|
||||
|
||||
while (position < size) {
|
||||
long remaining = size - position;
|
||||
long currentChunk = Math.min(remaining, chunkSize);
|
||||
|
||||
MappedByteBuffer buffer = channel.map(
|
||||
FileChannel.MapMode.READ_ONLY, position, currentChunk);
|
||||
consumer.consume(buffer, position + currentChunk >= size);
|
||||
position += currentChunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
package top.r3944realms.docchecktoolrefactored.io.reader;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
|
||||
import top.r3944realms.docchecktoolrefactored.util.FileUtil;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public class XmlFileReader implements CatalogFileReader {
|
||||
// 可配置的元素名列表,按优先级排序
|
||||
private static final List<String> RECORD_TAG_CANDIDATES =
|
||||
Arrays.asList("row", "record", "data", "item", "档案");
|
||||
|
||||
private static final List<String> ARCHIVE_CODE_TAG_CANDIDATES =
|
||||
Arrays.asList("档号", "dangan", "fileNo");
|
||||
|
||||
private static final List<String> PAGE_COUNT_TAG_CANDIDATES =
|
||||
Arrays.asList("页数", "pages", "pageCount");
|
||||
|
||||
|
||||
@Override
|
||||
public List<LogicalAddressFileGenerator.Record> readCatalogFile(String filePath) throws Exception {
|
||||
File file = FileUtil.fileCheckAndGet(filePath, "xml");
|
||||
List<LogicalAddressFileGenerator.Record> records = new ArrayList<>();
|
||||
|
||||
try {
|
||||
// 创建安全配置的DocumentBuilder
|
||||
DocumentBuilder builder = createSecureDocumentBuilder();
|
||||
|
||||
// 解析XML文件
|
||||
Document doc = builder.parse(file);
|
||||
doc.getDocumentElement().normalize();
|
||||
|
||||
log.debug("开始解析XML文件: {}, 根元素: {}",
|
||||
file.getName(), doc.getDocumentElement().getNodeName());
|
||||
// 查找记录元素
|
||||
NodeList recordNodes = findAllRecordNodes(doc);
|
||||
log.debug("找到 {} 个潜在记录节点", recordNodes.getLength());
|
||||
|
||||
// 解析每个记录元素
|
||||
int validCount = 0;
|
||||
int invalidCount = 0;
|
||||
|
||||
for (int i = 0; i < recordNodes.getLength(); i++) {
|
||||
Node node = recordNodes.item(i);
|
||||
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||
Element recordElement = (Element) node;
|
||||
LogicalAddressFileGenerator.Record record = parseRecordElement(recordElement);
|
||||
|
||||
if (record != null) {
|
||||
records.add(record);
|
||||
validCount++;
|
||||
log.debug("解析到有效记录: {}", record);
|
||||
} else {
|
||||
invalidCount++;
|
||||
log.debug("跳过无效记录节点");
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("XML解析完成 - 有效记录: {}, 无效记录: {}", validCount, invalidCount);
|
||||
} catch (Exception e) {
|
||||
log.error("解析XML文件失败: {}", filePath, e);
|
||||
throw new Exception("解析XML文件失败: " + filePath, e);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
@SuppressWarnings("HttpUrlsUsage")
|
||||
private static DocumentBuilder createSecureDocumentBuilder() throws ParserConfigurationException {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
// 安全配置防止XXE攻击
|
||||
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
||||
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
||||
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
||||
factory.setXIncludeAware(false);
|
||||
factory.setExpandEntityReferences(false);
|
||||
return factory.newDocumentBuilder();
|
||||
}
|
||||
private NodeList findAllRecordNodes(Document doc) {
|
||||
// 尝试所有可能的记录标签
|
||||
for (String tagName : RECORD_TAG_CANDIDATES) {
|
||||
NodeList nodes = doc.getElementsByTagName(tagName);
|
||||
if (nodes.getLength() > 0) {
|
||||
log.debug("使用标签名 '{}' 找到 {} 个记录节点", tagName, nodes.getLength());
|
||||
return nodes;
|
||||
}
|
||||
}
|
||||
log.warn("未找到任何记录节点,尝试的标签名: {}", RECORD_TAG_CANDIDATES);
|
||||
return new EmptyNodeList(); // 返回空节点列表而不是null
|
||||
}
|
||||
|
||||
private LogicalAddressFileGenerator.Record parseRecordElement(Element recordElement) {
|
||||
String archiveCode = findFirstNonEmptyTextContent(recordElement, ARCHIVE_CODE_TAG_CANDIDATES);
|
||||
String pageStr = findFirstNonEmptyTextContent(recordElement, PAGE_COUNT_TAG_CANDIDATES);
|
||||
|
||||
if (archiveCode == null || archiveCode.isEmpty()) {
|
||||
log.debug("记录缺少档号字段");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
int page = pageStr != null ? Integer.parseInt(pageStr.trim()) : 0;
|
||||
if (page > 0) {
|
||||
return new LogicalAddressFileGenerator.Record(archiveCode, page);
|
||||
} else {
|
||||
log.debug("无效的页数值: {}", pageStr);
|
||||
return null;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("页数字段格式错误: {}", pageStr);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String findFirstNonEmptyTextContent(Element element, List<String> tagNames) {
|
||||
for (String tag : tagNames) {
|
||||
NodeList nodes = element.getElementsByTagName(tag);
|
||||
if (nodes.getLength() > 0) {
|
||||
String content = nodes.item(0).getTextContent();
|
||||
if (content != null && !content.trim().isEmpty()) {
|
||||
return content.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 空节点列表实现
|
||||
private static class EmptyNodeList implements NodeList {
|
||||
@Override public Node item(int index) { return null; }
|
||||
@Override public int getLength() { return 0; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -7,9 +7,7 @@ import java.nio.file.Files;
|
|||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package top.r3944realms.docchecktoolrefactored.model;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.annotation.write.style.ColumnWidth;
|
||||
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
|
||||
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ContentRowHeight(20)
|
||||
@HeadRowHeight(25)
|
||||
@ColumnWidth(25)
|
||||
public class RecordData {
|
||||
@ExcelProperty(value = "档号")
|
||||
private String archivalCode;
|
||||
|
||||
@ExcelProperty(value = "页数")
|
||||
private Integer page;
|
||||
}
|
||||
|
|
@ -7,12 +7,19 @@ import javafx.scene.control.TextArea;
|
|||
import javafx.scene.control.TextField;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.Stage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.core.DuplicateFinder;
|
||||
import top.r3944realms.docchecktoolrefactored.core.FileHashCalculator;
|
||||
import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
|
||||
import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner;
|
||||
import top.r3944realms.docchecktoolrefactored.ui.task.DuplicateDocumentDetectionTask;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* The type Duplicate document pane controller.
|
||||
*/
|
||||
@Slf4j
|
||||
public class DuplicateDocumentPaneController {
|
||||
@FXML private TextArea result1B;
|
||||
@FXML private TextField loadFolder1TF;
|
||||
|
|
@ -39,6 +46,39 @@ public class DuplicateDocumentPaneController {
|
|||
* @param actionEvent the action event
|
||||
*/
|
||||
@FXML void onStart(ActionEvent actionEvent) {
|
||||
// 触发异步逻辑 -> 调用
|
||||
log.info("用户点击了开始查重按钮");
|
||||
String folderPath = loadFolder1TF.getText();
|
||||
if (folderPath == null || folderPath.trim().isEmpty()) {
|
||||
log.warn("未选择文件夹,无法进行查重");
|
||||
result1B.setText("请选择要检查的文件夹。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建并启动后台任务
|
||||
DuplicateDocumentDetectionTask task = new DuplicateDocumentDetectionTask(folderPath);
|
||||
|
||||
// 绑定任务的消息到结果文本区域
|
||||
task.messageProperty().addListener((observable, oldValue, newValue) -> {
|
||||
result1B.setText(newValue);
|
||||
});
|
||||
|
||||
// 当任务完成时显示完整结果
|
||||
task.setOnSucceeded(e -> {
|
||||
result1B.setText(task.getValue());
|
||||
log.info("查重任务完成,结果如下:{}", task.getValue());
|
||||
});
|
||||
|
||||
// 处理任务失败情况
|
||||
task.setOnFailed(e -> {
|
||||
Throwable exception = task.getException();
|
||||
result1B.setText("检测过程中发生错误: " + exception.getMessage());
|
||||
log.error("error", exception);
|
||||
log.info("查重任务失败,错误信息: {}", exception.getMessage());
|
||||
});
|
||||
|
||||
// 在新线程中执行任务
|
||||
Thread thread = new Thread(task);
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,23 @@ import javafx.scene.control.Button;
|
|||
import javafx.scene.control.ChoiceBox;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.stage.FileChooser;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.core.AddressFileComparator;
|
||||
import top.r3944realms.docchecktoolrefactored.core.AddressFileGenerator;
|
||||
import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
|
||||
import top.r3944realms.docchecktoolrefactored.core.PhysicalAddressFileGenerator;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
|
||||
/**
|
||||
* The type Path check pane controller.
|
||||
*/
|
||||
//TODO: 应该交给Platform:runLater;
|
||||
@Slf4j
|
||||
public class PathCheckPaneController implements Initializable {
|
||||
@FXML private ChoiceBox<Mode> loadFolderType2CB;
|
||||
@FXML private TextArea result2TA;
|
||||
|
|
@ -24,6 +34,14 @@ public class PathCheckPaneController implements Initializable {
|
|||
@FXML private Button selectJPGFolder2B;
|
||||
@FXML private Button generateLogicalAddress2B;
|
||||
@FXML private Button generatePhysicalAddress2B;
|
||||
// 存储生成的文件路径
|
||||
private String logicalAddressFilePath = null;
|
||||
private String physicalAddressFilePath = null;
|
||||
|
||||
|
||||
// 逻辑地址文件生成器实例
|
||||
private final LogicalAddressFileGenerator generator = new LogicalAddressFileGenerator();
|
||||
private final PhysicalAddressFileGenerator paGenerator = new PhysicalAddressFileGenerator();
|
||||
|
||||
/**
|
||||
* On select lc.
|
||||
|
|
@ -31,7 +49,30 @@ public class PathCheckPaneController implements Initializable {
|
|||
* @param actionEvent the action event
|
||||
*/
|
||||
@FXML void onSelectLC(ActionEvent actionEvent) {
|
||||
log.info("用户点击了选择目录文件按钮");
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("选择目录文件");
|
||||
|
||||
// 设置文件过滤器,只允许DBF、XML、xlsx、xls格式
|
||||
|
||||
FileChooser.ExtensionFilter xlsxFilter = new FileChooser.ExtensionFilter("Excel Files (*.xlsx)", "*.xlsx");
|
||||
FileChooser.ExtensionFilter xlsFilter = new FileChooser.ExtensionFilter("Excel Files (*.xls)", "*.xls");
|
||||
FileChooser.ExtensionFilter dbfFilter = new FileChooser.ExtensionFilter("DBF Files (*.dbf)", "*.dbf");
|
||||
FileChooser.ExtensionFilter xmlFilter = new FileChooser.ExtensionFilter("XML Files (*.xml)", "*.xml");
|
||||
|
||||
fileChooser.getExtensionFilters().addAll( xlsxFilter, xlsFilter,dbfFilter, xmlFilter);
|
||||
|
||||
// 显示文件选择对话框
|
||||
File selectedFile = fileChooser.showOpenDialog(selectLoadCatalog2B.getScene().getWindow());
|
||||
|
||||
// 如果选择了文件,则将文件路径显示在loadCatalog2TF上
|
||||
if (selectedFile != null) {
|
||||
loadCatalog2TF.setText(selectedFile.getAbsolutePath());
|
||||
log.info("选择的目录文件路径为:{}", selectedFile.getAbsolutePath());
|
||||
}else{
|
||||
log.warn("用户未选择任何文件夹");
|
||||
result2TA.setText("未选择任何文件夹,请重新选择。");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -40,7 +81,22 @@ public class PathCheckPaneController implements Initializable {
|
|||
* @param actionEvent the action event
|
||||
*/
|
||||
@FXML void onSelectJPGF(ActionEvent actionEvent) {
|
||||
javafx.stage.DirectoryChooser directoryChooser = new javafx.stage.DirectoryChooser();
|
||||
// 正确获取当前选中的值
|
||||
Mode selectedMode = loadFolderType2CB.getValue();
|
||||
if (selectedMode == Mode.PAGE_TYPE) {
|
||||
directoryChooser.setTitle("选择页面级文件夹");
|
||||
} else if (selectedMode == Mode.FILE_TYPE) {
|
||||
directoryChooser.setTitle("选择文件级文件夹");
|
||||
}
|
||||
log.info("用户选择的模式为:{}", selectedMode);
|
||||
|
||||
File selectedDirectory = directoryChooser.showDialog(selectJPGFolder2B.getScene().getWindow());
|
||||
|
||||
if (selectedDirectory != null) {
|
||||
loadJPGFolder2TF.setText(selectedDirectory.getAbsolutePath());
|
||||
log.info("选择的{}文件夹路径为:{}", selectedMode,selectedDirectory.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -49,7 +105,62 @@ public class PathCheckPaneController implements Initializable {
|
|||
* @param actionEvent the action event
|
||||
*/
|
||||
@FXML void onGenerateLA(ActionEvent actionEvent) {
|
||||
log.info("用户点击了生成逻辑地址文件按钮");
|
||||
String filePath = loadCatalog2TF.getText();
|
||||
if (filePath.isEmpty()) {
|
||||
result2TA.setText("请先选择目录文件。");
|
||||
return;
|
||||
}
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("选择保存逻辑地址文件的位置");
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv"));
|
||||
fileChooser.setInitialFileName("逻辑地址文件.csv");
|
||||
|
||||
File outputFile = fileChooser.showSaveDialog(generateLogicalAddress2B.getScene().getWindow());
|
||||
if (outputFile == null) {
|
||||
result2TA.setText("未选择保存位置");
|
||||
log.warn("用户未选择任何文件");
|
||||
return;
|
||||
}
|
||||
|
||||
// 正确处理文件扩展名 - 只有在没有.csv扩展名时才添加
|
||||
final File finalOutputFile;
|
||||
if (!outputFile.getName().toLowerCase().endsWith(".csv")) {
|
||||
finalOutputFile = new File(outputFile.getAbsolutePath() + ".csv");
|
||||
} else {
|
||||
finalOutputFile = outputFile;
|
||||
}
|
||||
|
||||
// 保存生成的文件路径
|
||||
logicalAddressFilePath = finalOutputFile.getAbsolutePath();
|
||||
log.info("选择的输出文件路径: {}", logicalAddressFilePath);
|
||||
// 创建后台任务来处理文件生成
|
||||
Thread backgroundThread = new Thread(() -> {
|
||||
// 获取当前选择的文件夹类型
|
||||
Mode selectedMode = loadFolderType2CB.getValue();
|
||||
int folderType = (selectedMode == Mode.PAGE_TYPE) ? AddressFileGenerator.PAGE_TYPE : AddressFileGenerator.FILE_TYPE;
|
||||
|
||||
generator.generateAddressFile(filePath, finalOutputFile, folderType, new LogicalAddressFileGenerator.Callback() {
|
||||
@Override
|
||||
public void onProgress(String message) {
|
||||
javafx.application.Platform.runLater(() -> result2TA.setText(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(String outputPath) {
|
||||
javafx.application.Platform.runLater(() -> result2TA.setText(outputPath));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String errorMessage) {
|
||||
javafx.application.Platform.runLater(() -> result2TA.setText(errorMessage));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
backgroundThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -58,20 +169,152 @@ public class PathCheckPaneController implements Initializable {
|
|||
* @param actionEvent the action event
|
||||
*/
|
||||
@FXML void onGeneratePA(ActionEvent actionEvent) {
|
||||
String folderPath = loadJPGFolder2TF.getText();
|
||||
if (folderPath.isEmpty()) {
|
||||
result2TA.setText("请先选择文件夹。");
|
||||
return;
|
||||
}
|
||||
File folder = new File(folderPath);
|
||||
if(!folder.exists() || !folder.isDirectory()) {
|
||||
result2TA.setText("所选路径不存在或不是一个有效的文件夹。");
|
||||
return;
|
||||
}
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("选择保存物理地址文件的位置");
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv"));
|
||||
fileChooser.setInitialFileName("物理地址文件.csv");
|
||||
// 使用当前窗口作为父窗口显示文件选择对话框
|
||||
File outputFile = fileChooser.showSaveDialog(selectJPGFolder2B.getScene().getWindow());
|
||||
|
||||
if (outputFile == null) {
|
||||
result2TA.setText("未选择保存位置");
|
||||
return;
|
||||
}
|
||||
|
||||
// 正确处理文件扩展名 - 只有在没有.csv扩展名时才添加
|
||||
final File finalOutputFile;
|
||||
if (!outputFile.getName().toLowerCase().endsWith(".csv")) {
|
||||
finalOutputFile = new File(outputFile.getAbsolutePath() + ".csv");
|
||||
} else {
|
||||
finalOutputFile = outputFile;
|
||||
}
|
||||
|
||||
// 保存生成的文件路径
|
||||
physicalAddressFilePath = finalOutputFile.getAbsolutePath();
|
||||
|
||||
// 创建后台任务来处理文件生成
|
||||
Thread backgroundThread = new Thread(() -> {
|
||||
// 获取当前选择的文件夹类型
|
||||
Mode selectedMode = loadFolderType2CB.getValue();
|
||||
int folderType = (selectedMode == Mode.PAGE_TYPE) ? AddressFileGenerator.PAGE_TYPE : AddressFileGenerator.FILE_TYPE;
|
||||
|
||||
paGenerator.generateAddressFile(folderPath, finalOutputFile, folderType, new AddressFileGenerator.Callback() {
|
||||
@Override
|
||||
public void onProgress(String message) {
|
||||
javafx.application.Platform.runLater(() -> result2TA.setText(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(String outputPath) {
|
||||
javafx.application.Platform.runLater(() -> result2TA.setText(outputPath));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String errorMessage) {
|
||||
javafx.application.Platform.runLater(() -> result2TA.setText(errorMessage));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
backgroundThread.start();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* On start.
|
||||
*
|
||||
* @param actionEvent the action event
|
||||
*/
|
||||
@FXML void onStart(ActionEvent actionEvent) {
|
||||
@FXML
|
||||
void onStart(ActionEvent actionEvent) {
|
||||
log.info("用户点击了开始对比按钮");
|
||||
|
||||
// 检查是否已生成两个文件
|
||||
if (logicalAddressFilePath == null || physicalAddressFilePath == null) {
|
||||
result2TA.setText("请先生成逻辑地址文件和物理地址文件。");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("逻辑地址文件路径为:{}", logicalAddressFilePath);
|
||||
log.info("物理地址文件路径为:{}", physicalAddressFilePath);
|
||||
|
||||
// 使用新创建的核心类进行文件比较
|
||||
AddressFileComparator comparator = new AddressFileComparator();
|
||||
AddressFileComparator.ComparisonResult result = comparator.compareFiles(physicalAddressFilePath, logicalAddressFilePath);
|
||||
|
||||
// 显示比对结果
|
||||
StringBuilder resultText = new StringBuilder();
|
||||
|
||||
// 显示读取的行数
|
||||
resultText.append("读取物理地址文件记录数: ").append(result.getPhysicalRecordsCount()).append("\n");
|
||||
resultText.append("读取逻辑地址文件记录数: ").append(result.getLogicalRecordsCount()).append("\n\n");
|
||||
|
||||
// 显示路径不一致的结果
|
||||
if (!result.getPathMismatchResults().isEmpty()) {
|
||||
resultText.append("文件名相同但路径不一致的记录数量: ").append(result.getPathMismatchResults().size()).append("\n");
|
||||
for (String mismatch : result.getPathMismatchResults()) {
|
||||
resultText.append("\t").append(mismatch).append("\n");
|
||||
}
|
||||
resultText.append("\n");
|
||||
} else {
|
||||
resultText.append("没有路径错误\n\n");
|
||||
}
|
||||
|
||||
// 显示物理文件在逻辑文件中未找到的结果
|
||||
if (!result.getForwardComparisonResults().isEmpty()) {
|
||||
resultText.append("物理文件在逻辑文件中未找到的记录数量: ").append(result.getForwardComparisonResults().size()).append("\n");
|
||||
for (String forward : result.getForwardComparisonResults()) {
|
||||
resultText.append("\t").append(forward).append("\n");
|
||||
}
|
||||
resultText.append("\n");
|
||||
} else {
|
||||
resultText.append("没有物理存在而逻辑不存在的文件\n\n");
|
||||
}
|
||||
|
||||
// 显示逻辑文件在物理文件中未找到的结果
|
||||
if (!result.getBackwardComparisonResults().isEmpty()) {
|
||||
resultText.append("逻辑文件在物理文件中未找到的记录数量: ").append(result.getBackwardComparisonResults().size()).append("\n");
|
||||
for (String backward : result.getBackwardComparisonResults()) {
|
||||
resultText.append("\t").append(backward).append("\n");
|
||||
}
|
||||
} else {
|
||||
resultText.append("没有逻辑存在而物理不存在的文件\n");
|
||||
}
|
||||
|
||||
// 如果所有结果都为空,则显示一致信息
|
||||
if (result.getPathMismatchResults().isEmpty() &&
|
||||
result.getForwardComparisonResults().isEmpty() &&
|
||||
result.getBackwardComparisonResults().isEmpty()) {
|
||||
resultText = new StringBuilder("所有文件比对一致,无差异。\n");
|
||||
resultText.append("读取物理地址文件记录数: ").append(result.getPhysicalRecordsCount()).append("\n");
|
||||
resultText.append("读取逻辑地址文件记录数: ").append(result.getLogicalRecordsCount()).append("\n");
|
||||
}
|
||||
|
||||
result2TA.setText(resultText.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle resourceBundle) {
|
||||
loadFolderType2CB.setValue(Mode.PAGE_TYPE);
|
||||
loadFolderType2CB.getItems().addAll(Mode.values());
|
||||
}
|
||||
|
||||
|
|
@ -82,11 +325,11 @@ public class PathCheckPaneController implements Initializable {
|
|||
/**
|
||||
* Jpg mode.
|
||||
*/
|
||||
JPG("jpg"),
|
||||
PAGE_TYPE("jpg"),
|
||||
/**
|
||||
* Pdf mode.
|
||||
*/
|
||||
PDF("pdf");
|
||||
FILE_TYPE("pdf");
|
||||
/**
|
||||
* The Id.
|
||||
*/
|
||||
|
|
@ -97,3 +340,5 @@ public class PathCheckPaneController implements Initializable {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package top.r3944realms.docchecktoolrefactored.ui.module;
|
||||
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.TextField;
|
||||
|
||||
|
|
@ -12,5 +13,14 @@ public class ProjectInfoPaneController {
|
|||
@FXML private TextField totalCatalogNumberTF;
|
||||
@FXML private TextField fileCategoriesTF;
|
||||
@FXML private TextField fileYearTF;
|
||||
@FXML
|
||||
void onReset(ActionEvent event) {
|
||||
// 清空所有文本字段
|
||||
projectNameTF.clear();
|
||||
fileYearTF.clear();
|
||||
fileCategoriesTF.clear();
|
||||
totalCatalogNumberTF.clear();
|
||||
AcceptanceTimeTF.clear();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,254 @@
|
|||
// StorageCarrierPaneController.java
|
||||
package top.r3944realms.docchecktoolrefactored.ui.module;
|
||||
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.core.HashFileGenerator;
|
||||
import top.r3944realms.docchecktoolrefactored.core.MD5HashCalculator;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
//TODO: 应该交给Platform:runLater;
|
||||
@Slf4j
|
||||
public class StorageCarrierPaneController {
|
||||
|
||||
@FXML
|
||||
private Button caculateHash7B;
|
||||
|
||||
@FXML
|
||||
private Button generateHashFile7B;
|
||||
|
||||
@FXML
|
||||
private TextField loadCompressedFile;
|
||||
|
||||
@FXML
|
||||
private TextField loadDigitalOutcomes;
|
||||
|
||||
@FXML
|
||||
private TextArea result7TA;
|
||||
|
||||
@FXML
|
||||
private Button selectLoadCompressedFile7B;
|
||||
|
||||
@FXML
|
||||
private Button selectLoadDigitalOutcomes7B;
|
||||
|
||||
@FXML
|
||||
private Button clearSelectedFoldersButton;
|
||||
|
||||
|
||||
|
||||
|
||||
@FXML
|
||||
void onSelectLD(ActionEvent event) {
|
||||
log.info("用户点击选择文件夹按钮");
|
||||
DirectoryChooser directoryChooser = new DirectoryChooser();
|
||||
directoryChooser.setTitle("选择要检查的文件夹(页面级文件夹和文件级文件夹等不包括目录文件夹)");
|
||||
|
||||
File selectedFolder = directoryChooser.showDialog(new Stage());
|
||||
if (selectedFolder != null) {
|
||||
String currentText = loadDigitalOutcomes.getText();
|
||||
String folderPath = selectedFolder.getAbsolutePath();
|
||||
|
||||
// 如果当前文本框为空,直接设置;否则追加路径
|
||||
if (currentText == null || currentText.isEmpty()) {
|
||||
loadDigitalOutcomes.setText(folderPath);
|
||||
} else {
|
||||
// 检查是否已经添加过该路径,避免重复
|
||||
String[] existingPaths = currentText.split(File.pathSeparator);
|
||||
boolean alreadyExists = false;
|
||||
for (String path : existingPaths) {
|
||||
if (path.equals(folderPath)) {
|
||||
alreadyExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadyExists) {
|
||||
loadDigitalOutcomes.setText(currentText + File.pathSeparator + folderPath);
|
||||
}
|
||||
}
|
||||
log.info("用户选择了文件夹: {}", selectedFolder.getAbsolutePath());
|
||||
} else {
|
||||
log.info("用户取消了文件夹选择");
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
void onClearSelectedFolders(ActionEvent event) {
|
||||
log.info("用户点击清除已选择文件夹按钮");
|
||||
loadDigitalOutcomes.setText("");
|
||||
result7TA.setText("已清除所有已选择的文件夹");
|
||||
log.info("已清除所有已选择的文件夹");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@FXML
|
||||
void onSelectLC(ActionEvent event) {
|
||||
log.info("用户点击选择RAR文件按钮");
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("选择一个 .rar 文件");
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("RAR Files", "*.rar"));
|
||||
File selectedFile = fileChooser.showOpenDialog(new Stage());
|
||||
if (selectedFile != null) {
|
||||
loadCompressedFile.setText(selectedFile.getAbsolutePath());
|
||||
log.info("用户选择了RAR文件: {}", selectedFile.getAbsolutePath());
|
||||
} else {
|
||||
log.info("用户取消了RAR文件选择");
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
void onCaculateHash(ActionEvent event) {
|
||||
log.info("开始计算RAR文件的MD5哈希值");
|
||||
String filePath = loadCompressedFile.getText();
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
log.warn("未选择RAR文件,无法计算哈希值");
|
||||
result7TA.setText("请先选择一个 .rar 文件");
|
||||
return;
|
||||
}
|
||||
|
||||
File file = new File(filePath);
|
||||
if (!file.exists() || !file.isFile() || !filePath.endsWith(".rar")) {
|
||||
log.warn("选择的文件无效或不是RAR文件: {}", filePath);
|
||||
result7TA.setText("所选文件不存在或不是一个有效的 .rar 文件");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("开始计算文件哈希值: {}", filePath);
|
||||
MD5HashCalculator hashCalculator = new MD5HashCalculator();
|
||||
String hashResult = hashCalculator.calculateHash(file.toPath());
|
||||
result7TA.setText("计算结果:\n" + hashResult);
|
||||
log.info("文件哈希值计算完成: {}", hashResult);
|
||||
} catch (IOException e) {
|
||||
log.error("计算文件哈希值时出错: {}", filePath, e);
|
||||
result7TA.setText("计算哈希值时出错: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
void onGenerateHF(ActionEvent event) {
|
||||
log.info("开始生成哈希列表文件");
|
||||
String folderPathsText = loadDigitalOutcomes.getText();
|
||||
if (folderPathsText == null || folderPathsText.isEmpty()) {
|
||||
log.warn("未选择文件夹,无法生成哈希列表文件");
|
||||
result7TA.setText("请先选择一个文件夹");
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析多个文件夹路径
|
||||
String[] folderPaths = folderPathsText.split(File.pathSeparator);
|
||||
List<File> folders = new ArrayList<>();
|
||||
|
||||
for (String path : folderPaths) {
|
||||
File folder = new File(path.trim());
|
||||
if (folder.exists() && folder.isDirectory()) {
|
||||
folders.add(folder);
|
||||
} else {
|
||||
log.warn("选择的路径无效或不是文件夹: {}", path);
|
||||
result7TA.setText("所选路径不存在或不是一个有效的文件夹: " + path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("选择保存哈希列表文件的位置");
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv"));
|
||||
|
||||
// 设置默认文件名
|
||||
fileChooser.setInitialFileName("哈希值列表文件.csv");
|
||||
|
||||
// 使用当前窗口作为父窗口显示文件选择对话框
|
||||
File outputFile = fileChooser.showSaveDialog(selectLoadDigitalOutcomes7B.getScene().getWindow());
|
||||
|
||||
if (outputFile == null) {
|
||||
log.info("用户取消了文件保存操作");
|
||||
result7TA.setText("未选择保存位置");
|
||||
return;
|
||||
}
|
||||
|
||||
// 正确处理文件扩展名 - 只有在没有.csv扩展名时才添加
|
||||
final File finalOutputFile;
|
||||
if (!outputFile.getName().toLowerCase().endsWith(".csv")) {
|
||||
finalOutputFile = new File(outputFile.getAbsolutePath() + ".csv");
|
||||
} else {
|
||||
finalOutputFile = outputFile;
|
||||
}
|
||||
|
||||
log.info("选择的输出文件路径: {}", finalOutputFile.getAbsolutePath());
|
||||
|
||||
// 创建后台任务
|
||||
Task<String> task = new Task<String>() {
|
||||
@Override
|
||||
protected String call() throws Exception {
|
||||
log.info("开始执行哈希文件生成任务");
|
||||
updateMessage("开始生成哈希文件...");
|
||||
|
||||
HashFileGenerator generator = new HashFileGenerator();
|
||||
|
||||
// 传递多个文件夹路径
|
||||
List<Path> folderPaths = folders.stream().map(File::toPath).collect(ArrayList::new,
|
||||
ArrayList::add,
|
||||
ArrayList::addAll);
|
||||
|
||||
generator.generateHashFile(folderPaths, finalOutputFile.toPath(), (current, total) -> {
|
||||
updateProgress(current, total);
|
||||
updateMessage("处理文件: " + current + "/" + total);
|
||||
if (current % 500 == 0 || current == total) { // 每500个文件或完成时记录一次日志
|
||||
log.info("处理进度: {}/{}", current, total);
|
||||
}
|
||||
});
|
||||
|
||||
log.info("哈希文件生成任务完成,输出文件: {}", finalOutputFile.getAbsolutePath());
|
||||
return "哈希列表文件已生成: " + finalOutputFile.getAbsolutePath();
|
||||
}
|
||||
};
|
||||
|
||||
// 绑定任务的消息到结果文本区域,实时显示进度
|
||||
task.messageProperty().addListener((observable, oldValue, newValue) -> {
|
||||
result7TA.setText(newValue);
|
||||
});
|
||||
|
||||
// 任务成功完成
|
||||
task.setOnSucceeded(e -> {
|
||||
log.info("哈希文件生成任务成功完成");
|
||||
result7TA.setText(task.getValue());
|
||||
});
|
||||
|
||||
// 任务失败处理
|
||||
task.setOnFailed(e -> {
|
||||
Throwable exception = task.getException();
|
||||
String errorMsg = "生成哈希文件时出错: " + (exception != null ? exception.getMessage() : "未知错误");
|
||||
log.error("哈希文件生成任务失败", exception);
|
||||
result7TA.setText(errorMsg);
|
||||
});
|
||||
|
||||
// 任务取消处理
|
||||
task.setOnCancelled(e -> {
|
||||
log.info("哈希文件生成任务被用户取消");
|
||||
result7TA.setText("哈希文件生成操作已取消");
|
||||
});
|
||||
|
||||
// 在新线程中执行任务
|
||||
Thread thread = new Thread(task);
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
package top.r3944realms.docchecktoolrefactored.ui.task;
|
||||
|
||||
import javafx.concurrent.Task;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.core.MD5HashCalculator;
|
||||
import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
|
||||
import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
public class DuplicateDocumentDetectionTask extends Task<String> {
|
||||
private final String folderPath;
|
||||
private final MD5HashCalculator hashCalculator;
|
||||
private volatile RobustParallelScanner scanner;
|
||||
|
||||
public DuplicateDocumentDetectionTask(String folderPath) {
|
||||
this.folderPath = folderPath;
|
||||
this.hashCalculator = new MD5HashCalculator();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String call() throws Exception {
|
||||
updateMessage("正在初始化扫描...");
|
||||
|
||||
Path rootPath = Paths.get(folderPath);
|
||||
if (!Files.exists(rootPath) || !Files.isDirectory(rootPath)) {
|
||||
throw new IllegalArgumentException("指定路径不是有效目录: " + folderPath);
|
||||
}
|
||||
|
||||
// 使用 RobustParallelScanner 和 MD5HashCalculator 进行并行扫描和哈希计算
|
||||
Map<String, List<Path>> hashToFileMap = new ConcurrentHashMap<>();
|
||||
AtomicInteger processed = new AtomicInteger(0);
|
||||
AtomicReference<Exception> errorRef = new AtomicReference<>(null);
|
||||
AtomicBoolean scanCompleted = new AtomicBoolean(false);
|
||||
|
||||
// 使用 CountDownLatch 等待扫描完成
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
// 创建扫描器
|
||||
scanner = new RobustParallelScanner(10);
|
||||
|
||||
// 异步启动扫描任务
|
||||
Thread scanThread = new Thread(() -> {
|
||||
try {
|
||||
scanner.scanWithProgress(rootPath, new FileScanner.ProgressAwareListener() {
|
||||
@Override
|
||||
public void onFileFound(Path file) {
|
||||
if (isCancelled()) {
|
||||
scanner.cancel();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String hash = hashCalculator.calculatePartialHash(file);
|
||||
hashToFileMap.computeIfAbsent(hash, k -> new ArrayList<>()).add(file);
|
||||
} catch (IOException e) {
|
||||
// 记录无法计算哈希的文件,但不中断整个过程
|
||||
updateMessage("警告: 无法处理文件 " + file.toString() + " - " + e.getMessage());
|
||||
}
|
||||
processed.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Path path, Exception e) {
|
||||
// 记录错误但不中断扫描过程
|
||||
errorRef.set(e);
|
||||
updateMessage("扫描错误: " + path.toString() + " - " + e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScanComplete() {
|
||||
// 扫描完成
|
||||
scanCompleted.set(true);
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressUpdate(int current, int total) {
|
||||
if (isCancelled()) {
|
||||
scanner.cancel();
|
||||
return;
|
||||
}
|
||||
updateProgress(current, total);
|
||||
updateMessage("正在处理文件: " + current + "/" + total);
|
||||
if (current % 500 == 0 || current == total) { // 每500个文件或完成时记录一次日志
|
||||
log.info("处理进度: {}/{}", current, total);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
errorRef.set(e);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
scanThread.setDaemon(true);
|
||||
scanThread.start();
|
||||
|
||||
// 等待扫描完成,设置更长的超时时间(例如5分钟)
|
||||
if (!latch.await(5*60, TimeUnit.MINUTES)) {
|
||||
scanner.cancel();
|
||||
throw new TimeoutException("扫描超时(5分钟)");
|
||||
}
|
||||
|
||||
// 检查是否被取消
|
||||
if (isCancelled()) {
|
||||
scanner.cancel();
|
||||
return "操作已被取消";
|
||||
}
|
||||
|
||||
// 如果有错误且扫描未完成,抛出异常
|
||||
if (errorRef.get() != null && !scanCompleted.get()) {
|
||||
throw errorRef.get();
|
||||
}
|
||||
|
||||
// 分析重复文件并构建结果
|
||||
updateMessage("正在分析重复文件...");
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append("重复文件检测结果:\n");
|
||||
|
||||
if (errorRef.get() != null) {
|
||||
result.append("警告: 扫描过程中发生错误 - ").append(errorRef.get().getMessage()).append("\n\n");
|
||||
}
|
||||
|
||||
result.append("总共处理 ").append(processed.get()).append(" 个文件\n");
|
||||
|
||||
List<Map.Entry<String, List<Path>>> duplicateGroups = hashToFileMap.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().size() > 1)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!duplicateGroups.isEmpty()) {
|
||||
result.append("有 ").append(duplicateGroups.size()).append(" 组重复文件\n\n");
|
||||
|
||||
int groupIndex = 1;
|
||||
for (Map.Entry<String, List<Path>> entry : duplicateGroups) {
|
||||
result.append("第 ").append(groupIndex).append(" 组\t");
|
||||
result.append("哈希值: ").append(entry.getKey()).append("\n");
|
||||
|
||||
int fileIndex = 1;
|
||||
for (Path file : entry.getValue()) {
|
||||
result.append("文件名").append(fileIndex).append(": ").append(file.getFileName()).append("\t\t");
|
||||
result.append("文件路径").append(fileIndex).append(": ").append(file.toAbsolutePath()).append("\n");
|
||||
fileIndex++;
|
||||
}
|
||||
result.append("\n");
|
||||
groupIndex++;
|
||||
}
|
||||
} else {
|
||||
result.append("没有重复文件\n");
|
||||
}
|
||||
|
||||
updateMessage("检测完成!");
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cancelled() {
|
||||
super.cancelled();
|
||||
if (scanner != null) {
|
||||
scanner.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -95,4 +95,13 @@ public class DialogUtil {
|
|||
alert.showAndWait();
|
||||
});
|
||||
}
|
||||
public static void showErrorDialog(String title, String content) {
|
||||
showErrorDialog(title, title, content);
|
||||
}
|
||||
public static void showWarningDialog(String title, String content) {
|
||||
showWarningDialog(title, title, content);
|
||||
}
|
||||
public static void showInformationDialog(String title, String content) {
|
||||
showInformationDialog(title, title, content);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
package top.r3944realms.docchecktoolrefactored.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 文件工具类
|
||||
*/
|
||||
@Slf4j
|
||||
public class FileUtil {
|
||||
/**
|
||||
* 将字节数转换为易读的格式
|
||||
*/
|
||||
public static String humanReadableByteCount(long bytes) {
|
||||
if (bytes < 0) throw new IllegalArgumentException("字节数不能为负数");
|
||||
if (bytes < 1024) return bytes + " B";
|
||||
int exp = (int) (Math.log(bytes) / Math.log(1024));
|
||||
String unit = "KMGTPE".charAt(exp-1) + "iB";
|
||||
return String.format(Locale.US,"%.1f %s", bytes / Math.pow(1024, exp), unit);
|
||||
}
|
||||
|
||||
public static String getFileExtension(String filePath) throws NoSuchFileException {
|
||||
Path path = Paths.get(filePath).normalize();
|
||||
File file = path.toFile();
|
||||
if (!file.exists()) {
|
||||
log.error("文件不存在: {}", filePath);
|
||||
throw new NoSuchFileException("文件不存在: " + filePath);
|
||||
}
|
||||
|
||||
String fileName = path.getFileName().toString();
|
||||
int dotIndex = fileName.lastIndexOf('.');
|
||||
if (dotIndex == -1 || dotIndex == fileName.length() - 1) {
|
||||
throw new IllegalArgumentException("文件无有效扩展名: " + fileName);
|
||||
}
|
||||
return fileName.substring(dotIndex + 1).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件路径并返回
|
||||
*
|
||||
* @param filePath 文件路径
|
||||
* @return File
|
||||
* @throws NoSuchFileException 当指定路径无此文件时抛出
|
||||
*/
|
||||
public static File fileCheckAndGet(String filePath) throws NoSuchFileException {
|
||||
return fileCheckAndGet(filePath, (String) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件路径并返回
|
||||
*
|
||||
* @param filePath 文件路径
|
||||
* @param supportedFileExtensions 文件扩展名,例如 ["pdf", "txt]
|
||||
* @throws IllegalArgumentException 当指定路径文件无扩展名(如README),或扩展名不符时抛出
|
||||
* @throws NoSuchFileException 当指定路径无此文件时抛出
|
||||
*/
|
||||
public static File fileCheckAndGet(String filePath, String... supportedFileExtensions)
|
||||
throws NoSuchFileException, IllegalArgumentException {
|
||||
|
||||
Path path = Paths.get(filePath).normalize();
|
||||
File file = path.toFile();
|
||||
|
||||
if (!file.exists()) {
|
||||
log.error("文件不存在: {}", filePath);
|
||||
throw new NoSuchFileException("文件不存在: " + filePath);
|
||||
}
|
||||
|
||||
if (supportedFileExtensions == null || supportedFileExtensions.length == 0) {
|
||||
return file;
|
||||
}
|
||||
|
||||
String fileName = path.getFileName().toString();
|
||||
int dotIndex = fileName.lastIndexOf('.');
|
||||
if (dotIndex == -1 || dotIndex == fileName.length() - 1) {
|
||||
throw new IllegalArgumentException("文件无有效扩展名: " + fileName);
|
||||
}
|
||||
|
||||
String fileExtension = fileName.substring(dotIndex + 1).toLowerCase();
|
||||
List<String> supportedExtensions = Arrays.stream(supportedFileExtensions)
|
||||
.map(String::toLowerCase)
|
||||
.toList();
|
||||
|
||||
if (!supportedExtensions.contains(fileExtension)) {
|
||||
log.error("不支持的文件格式: {}", fileExtension);
|
||||
throw new IllegalArgumentException("不支持的文件格式,预期: "
|
||||
+ supportedExtensions + ",实际: " + fileExtension);
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,10 @@ package top.r3944realms.docchecktoolrefactored.util;
|
|||
public class StringUtil {
|
||||
public static String NO_BUG = """
|
||||
|
||||
_ooOoo_
|
||||
o8888888o
|
||||
88" . "88
|
||||
(| -_- |)
|
||||
_ooOoo_
|
||||
o8888888o
|
||||
88" . "88
|
||||
(| -_- |)
|
||||
O\\ = /O
|
||||
____/`---'\\____
|
||||
.' \\\\| |// `.
|
||||
|
|
|
|||
|
|
@ -11,14 +11,13 @@
|
|||
<?import javafx.scene.layout.RowConstraints?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<BorderPane fx:id="mainPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="400.0" style="-fx-background-color: rgb(245,245,245);" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="top.r3944realms.docchecktoolrefactored.ui.LoginStageController">
|
||||
<BorderPane fx:id="mainPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="400.0" style="-fx-background-color: rgb(245,245,245);" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.LoginStageController">
|
||||
<padding>
|
||||
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
|
||||
</padding>
|
||||
|
||||
<top>
|
||||
<Label alignment="CENTER" text="淮阴区数字化档案检查验收系统" textFill="rgb(66,133,244)">
|
||||
<Label alignment="CENTER" text="淮阴区数字化档案检查验收系统" textAlignment="CENTER" textFill="rgb(66,133,244)">
|
||||
<font>
|
||||
<Font name="Microsoft YaHei" size="24.0" />
|
||||
</font>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.Menu?>
|
||||
<?import javafx.scene.control.MenuBar?>
|
||||
<?import javafx.scene.control.MenuItem?>
|
||||
<?import javafx.scene.control.Tab?>
|
||||
<?import javafx.scene.control.TabPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
|
||||
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="1000.0" prefWidth="1200.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.MainStageController">
|
||||
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="1000.0" prefWidth="1200.0" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.MainStageController">
|
||||
<children>
|
||||
<MenuBar prefWidth="2558.0" VBox.vgrow="ALWAYS">
|
||||
<menus>
|
||||
|
|
@ -28,7 +24,9 @@
|
|||
</Menu>
|
||||
</menus>
|
||||
</MenuBar>
|
||||
<fx:include source="module/project-info-pane.fxml" />
|
||||
<!-- 导入项目信息面板 -->
|
||||
<fx:include source="module/project-info-pane.fxml" VBox.vgrow="ALWAYS"/>
|
||||
<!-- 导入项目内容面板 -->
|
||||
<TabPane stylesheets="@../css/custom-tab.css" tabClosingPolicy="UNAVAILABLE" VBox.vgrow="ALWAYS">
|
||||
<tabs>
|
||||
<Tab id="startTab" fx:id="step1T" text="1. 查重复文件">
|
||||
|
|
@ -36,11 +34,11 @@
|
|||
<fx:include source="module/step-1-pane.fxml" />
|
||||
</content>
|
||||
</Tab>
|
||||
<Tab fx:id="step2T" text="2. 查遗漏、存储路径和命名规范">
|
||||
<content>
|
||||
<fx:include source="module/step-2-pane.fxml" />
|
||||
</content>
|
||||
</Tab>
|
||||
<Tab fx:id="step2T" styleClass="long-text-tab" text="2. 查遗漏、存储路径和命名规范">
|
||||
<content>
|
||||
<fx:include source="module/step-2-pane.fxml" />
|
||||
</content>
|
||||
</Tab>
|
||||
<Tab fx:id="step3T" text="3. 查质量">
|
||||
<content>
|
||||
<fx:include source="module/step-3-pane.fxml" />
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.layout.ColumnConstraints?>
|
||||
<?import javafx.scene.layout.GridPane?>
|
||||
<?import javafx.scene.layout.RowConstraints?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<GridPane xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.ProjectInfoPaneController">
|
||||
<GridPane xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.ProjectInfoPaneController">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="-Infinity" minWidth="10.0" prefWidth="80.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="-Infinity" minWidth="10.0" prefWidth="80.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
|
||||
<ColumnConstraints hgrow="ALWAYS" maxWidth="-Infinity" minWidth="10.0" prefWidth="80.0" />
|
||||
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0" prefWidth="100.0" />
|
||||
<ColumnConstraints hgrow="ALWAYS" maxWidth="-Infinity" minWidth="10.0" prefWidth="80.0" />
|
||||
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0" prefWidth="100.0" />
|
||||
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0" percentWidth="20.0" prefWidth="100.0" />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints minHeight="30.0" prefHeight="45.0" vgrow="ALWAYS" />
|
||||
<RowConstraints minHeight="30.0" prefHeight="45.0" vgrow="ALWAYS" />
|
||||
<RowConstraints minHeight="30.0" prefHeight="45.0" vgrow="ALWAYS" />
|
||||
<RowConstraints minHeight="30.0" prefHeight="45.0" vgrow="ALWAYS" />
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<Label text="项目名称:">
|
||||
|
|
@ -29,6 +31,9 @@
|
|||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<Label text="验收时间:" GridPane.rowIndex="1">
|
||||
<GridPane.margin>
|
||||
|
|
@ -37,6 +42,9 @@
|
|||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<Label text="全宗号:" GridPane.rowIndex="2">
|
||||
<GridPane.margin>
|
||||
|
|
@ -45,6 +53,9 @@
|
|||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<Label text="档案门类:" GridPane.rowIndex="3">
|
||||
<GridPane.margin>
|
||||
|
|
@ -53,14 +64,20 @@
|
|||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<Label text="档案年度:" GridPane.columnIndex="2" GridPane.rowIndex="3">
|
||||
<Label text=" 归档年度:" GridPane.columnIndex="2" GridPane.rowIndex="3">
|
||||
<GridPane.margin>
|
||||
<Insets />
|
||||
</GridPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<TextField fx:id="projectNameTF" GridPane.columnIndex="1" GridPane.columnSpan="3">
|
||||
<GridPane.margin>
|
||||
|
|
@ -69,6 +86,9 @@
|
|||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</TextField>
|
||||
<TextField fx:id="AcceptanceTimeTF" GridPane.columnIndex="1" GridPane.columnSpan="3" GridPane.rowIndex="1">
|
||||
<GridPane.margin>
|
||||
|
|
@ -77,6 +97,9 @@
|
|||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</TextField>
|
||||
<TextField fx:id="totalCatalogNumberTF" GridPane.columnIndex="1" GridPane.columnSpan="3" GridPane.rowIndex="2">
|
||||
<GridPane.margin>
|
||||
|
|
@ -85,14 +108,20 @@
|
|||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</TextField>
|
||||
<TextField fx:id="fileCategoriesTF" GridPane.columnIndex="1" GridPane.rowIndex="3">
|
||||
<TextField fx:id="fileCategoriesTF" prefHeight="24.0" prefWidth="100.0" GridPane.columnIndex="1" GridPane.rowIndex="3">
|
||||
<GridPane.margin>
|
||||
<Insets />
|
||||
</GridPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</TextField>
|
||||
<TextField fx:id="fileYearTF" GridPane.columnIndex="3" GridPane.rowIndex="3">
|
||||
<GridPane.margin>
|
||||
|
|
@ -101,9 +130,20 @@
|
|||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</TextField>
|
||||
<Button fx:id="infoResetB" mnemonicParsing="false" onAction="#onReset" prefHeight="93.0" prefWidth="55.0" text="重置" GridPane.columnIndex="4" GridPane.rowSpan="4">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="20.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</Button>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
</GridPane>
|
||||
</GridPane>
|
||||
|
|
|
|||
|
|
@ -8,50 +8,69 @@
|
|||
<?import javafx.scene.layout.ColumnConstraints?>
|
||||
<?import javafx.scene.layout.GridPane?>
|
||||
<?import javafx.scene.layout.RowConstraints?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.DuplicateDocumentPaneController">
|
||||
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.DuplicateDocumentPaneController">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="288.0" minWidth="0.0" percentWidth="0.0" prefWidth="82.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="1263.9999633789064" minWidth="0.0" prefWidth="745.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="873.0" minWidth="0.0" prefWidth="82.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="873.0" minWidth="10.0" prefWidth="407.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="450.0" minWidth="10.0" prefWidth="400.0" />
|
||||
<ColumnConstraints />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints maxHeight="173.0" minHeight="10.0" prefHeight="53.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="173.0" minHeight="10.0" prefHeight="83.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="173.0" minHeight="10.0" prefHeight="31.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="549.0" minHeight="10.0" prefHeight="549.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="173.0" minHeight="10.0" prefHeight="71.00000127156576" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="173.0" minHeight="10.0" prefHeight="77.66666539510092" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="173.0" minHeight="10.0" prefHeight="49.33331298828125" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="592.6666666666666" minHeight="10.0" prefHeight="581.3333536783855" vgrow="SOMETIMES" />
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<TextArea fx:id="result1B" GridPane.columnSpan="3" GridPane.rowIndex="3">
|
||||
<TextArea fx:id="result1B" editable="false" GridPane.columnSpan="3" GridPane.rowIndex="3">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
</TextArea>
|
||||
<Label text="载入文件夹:" />
|
||||
<Label text="载入文件夹:">
|
||||
<GridPane.margin>
|
||||
<Insets left="10.0" />
|
||||
</GridPane.margin></Label>
|
||||
<TextField fx:id="loadFolder1TF" GridPane.columnIndex="1">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
</TextField>
|
||||
<Button fx:id="selectLoadFolder1B" mnemonicParsing="false" onAction="#onSelectFolder" text="选择文件夹" GridPane.columnIndex="2" GridPane.columnSpan="2">
|
||||
<Button fx:id="selectLoadFolder1B" mnemonicParsing="false" onAction="#onSelectFolder" text="选择文件夹" GridPane.columnIndex="3" GridPane.columnSpan="2" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin></Button>
|
||||
<Button fx:id="start1B" minWidth="-Infinity" mnemonicParsing="false" onAction="#onStart" prefHeight="75.0" prefWidth="800.0" text="开始检查" GridPane.columnSpan="4" GridPane.rowIndex="1">
|
||||
<Button fx:id="start1B" minWidth="-Infinity" mnemonicParsing="false" onAction="#onStart" prefHeight="58.0" prefWidth="650.0" text="开始检查" GridPane.columnSpan="3" GridPane.halignment="LEFT" GridPane.rowIndex="1" GridPane.valignment="CENTER">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
<Insets bottom="2.0" left="10.0" right="2.0" top="2.0" />
|
||||
</GridPane.margin>
|
||||
</Button>
|
||||
<Label text="反馈结果:" GridPane.rowIndex="2" />
|
||||
<TextArea editable="false" prefHeight="530.0" prefWidth="300.0" text="在此反馈结果: 如无重复文件,则反馈:无重复文件; 如有重复文件,则在此反馈档号。 " GridPane.columnIndex="3" GridPane.columnSpan="2" GridPane.rowIndex="3">
|
||||
<Label text="反馈结果:" GridPane.rowIndex="2">
|
||||
<GridPane.margin>
|
||||
<Insets left="10.0" />
|
||||
</GridPane.margin></Label>
|
||||
<TextArea editable="false" maxWidth="1.7976931348623157E308" prefWidth="400.0" text="批量取出数字化成果的哈希值,采用对比法查找重复文件,导出重复文件搜索结果,进行人工一一比对,并将比对台帐和统计结果填入查重登记表(附件1)。 " wrapText="true" GridPane.columnIndex="3" GridPane.columnSpan="2" GridPane.rowIndex="3">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</TextArea>
|
||||
<Label text="工作内容:" GridPane.columnIndex="3" GridPane.rowIndex="2">
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<GridPane.margin>
|
||||
<Insets left="10.0" />
|
||||
</GridPane.margin>
|
||||
</Label>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@
|
|||
<?import javafx.scene.layout.ColumnConstraints?>
|
||||
<?import javafx.scene.layout.GridPane?>
|
||||
<?import javafx.scene.layout.RowConstraints?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.PathCheckPaneController">
|
||||
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.PathCheckPaneController">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="940.0" minWidth="0.0" percentWidth="0.0" prefWidth="104.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="940.0" minWidth="10.0" percentWidth="0.0" prefWidth="104.0" />
|
||||
|
|
@ -19,29 +20,38 @@
|
|||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="965.0" minWidth="10.0" prefWidth="867.0" />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints maxHeight="224.0" minHeight="0.0" prefHeight="34.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="289.0" minHeight="10.0" prefHeight="40.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="373.0" minHeight="10.0" prefHeight="131.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="224.0" minHeight="0.0" prefHeight="29.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="500.0" minHeight="10.0" prefHeight="493.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="224.0" minHeight="0.0" prefHeight="49.333343505859375" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="289.0" minHeight="10.0" prefHeight="68.66666920979819" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="373.0" minHeight="10.0" prefHeight="76.66666412353516" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="224.0" minHeight="0.0" prefHeight="31.333333333333343" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="573.3333180745443" minHeight="10.0" prefHeight="553.999989827474" vgrow="SOMETIMES" />
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<Label text="载入目录:">
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<GridPane.margin>
|
||||
<Insets left="10.0" />
|
||||
</GridPane.margin>
|
||||
</Label>
|
||||
<Label text="载入文件夹:" GridPane.rowIndex="1">
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<GridPane.margin>
|
||||
<Insets left="10.0" />
|
||||
</GridPane.margin>
|
||||
</Label>
|
||||
<Label text="反馈结果:" GridPane.rowIndex="3">
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<GridPane.margin>
|
||||
<Insets left="10.0" />
|
||||
</GridPane.margin>
|
||||
</Label>
|
||||
<TextArea fx:id="result2TA" prefHeight="473.0" prefWidth="1754.0" GridPane.columnSpan="4" GridPane.rowIndex="4">
|
||||
<TextArea fx:id="result2TA" editable="false" GridPane.columnSpan="4" GridPane.rowIndex="4">
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
|
|
@ -49,15 +59,18 @@
|
|||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
</TextArea>
|
||||
<Button fx:id="start2B" mnemonicParsing="false" onAction="#onStart" prefHeight="75.0" prefWidth="800.0" text="开始比对" GridPane.columnSpan="5" GridPane.rowIndex="2">
|
||||
<Button fx:id="start2B" mnemonicParsing="false" onAction="#onStart" prefHeight="75.0" prefWidth="800.0" text="开始比对" GridPane.columnSpan="4" GridPane.halignment="LEFT" GridPane.rowIndex="2" GridPane.valignment="CENTER">
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</Button>
|
||||
<TextField fx:id="loadCatalog2TF" GridPane.columnIndex="1" GridPane.columnSpan="2">
|
||||
<TextField fx:id="loadCatalog2TF" GridPane.columnIndex="2">
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
|
|
@ -73,7 +86,7 @@
|
|||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
</TextField>
|
||||
<Button fx:id="selectLoadCatalog2B" mnemonicParsing="false" onAction="#onSelectLC" text="选择文件" GridPane.columnIndex="3" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
|
||||
<Button fx:id="selectLoadCatalog2B" mnemonicParsing="false" onAction="#onSelectLC" text=" 选择文件 " GridPane.columnIndex="3" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
|
|
@ -81,7 +94,7 @@
|
|||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
</Button>
|
||||
<Button fx:id="selectJPGFolder2B" mnemonicParsing="false" onAction="#onSelectJPGF" text="选择文件夹" GridPane.columnIndex="3" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">
|
||||
<Button fx:id="selectJPGFolder2B" mnemonicParsing="false" onAction="#onSelectJPGF" text=" 选择文件夹" GridPane.columnIndex="3" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
|
|
@ -89,7 +102,7 @@
|
|||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
</Button>
|
||||
<Button fx:id="generateLogicalAddress2B" mnemonicParsing="false" onAction="#onGenerateLA" text="生成逻辑地址" GridPane.columnIndex="4" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
|
||||
<Button fx:id="generateLogicalAddress2B" mnemonicParsing="false" onAction="#onGenerateLA" text="生成逻辑地址文件" GridPane.columnIndex="4" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
|
|
@ -97,7 +110,7 @@
|
|||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
</Button>
|
||||
<Button fx:id="generatePhysicalAddress2B" mnemonicParsing="false" onAction="#onGeneratePA" text="生成物理地址" GridPane.columnIndex="4" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">
|
||||
<Button fx:id="generatePhysicalAddress2B" mnemonicParsing="false" onAction="#onGeneratePA" text="生成物理地址文件" GridPane.columnIndex="4" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
|
|
@ -105,19 +118,27 @@
|
|||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
</Button>
|
||||
<TextArea editable="false" prefHeight="473.0" prefWidth="673.0" text="在此反馈结果: 目录对应文件XXXX件,XXXX页; 实际文件XXXX件,XXXX页; 如比对一致,告知文件路径一致,文件名无误,无遗漏; 如有问题,则 ①文件路径不一致,请检查,可反馈档号,也可不反馈; ②反馈遗漏档号, ③或文件名错误,错误文件名为XXXX;" GridPane.columnIndex="4" GridPane.rowIndex="4">
|
||||
<TextArea editable="false" prefHeight="450.0" prefWidth="400.0" text="①获取数字化成果文件的物理存储路径(实际存储位置) ②处理案卷级/文件级目录生成逻辑存储路径(理论存储位置) ③自动对比物理路径与逻辑路径,识别以下问题: 1)文件漏扫/存储路径错误/命名不规范的文件 2)目录数据库档号错误或页数著录。 ④统计结果填入《查遗漏、查存储路径和命名规范登记表》(附件2)。 " wrapText="true" GridPane.columnIndex="4" GridPane.rowIndex="4">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</TextArea>
|
||||
<ChoiceBox fx:id="loadFolderType2CB" prefHeight="23.0" prefWidth="98.0" GridPane.columnIndex="1" GridPane.rowIndex="1">
|
||||
<ChoiceBox fx:id="loadFolderType2CB" prefHeight="40.0" prefWidth="98.0" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowSpan="2" GridPane.valignment="CENTER">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
<Insets left="10.0" right="10.0" />
|
||||
</GridPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
</ChoiceBox>
|
||||
<Label text="工作内容:" GridPane.columnIndex="4" GridPane.rowIndex="3">
|
||||
<GridPane.margin>
|
||||
<Insets left="10.0" />
|
||||
</GridPane.margin>
|
||||
</Label>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.TextArea?>
|
||||
<?import javafx.scene.layout.AnchorPane?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<AnchorPane xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1">
|
||||
|
||||
</AnchorPane>
|
||||
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17">
|
||||
<children>
|
||||
<TextArea editable="false" prefHeight="800.0" prefWidth="1000.0" scrollLeft="1.0" text="工作内容: ①汇总前两步检查结果,计算合格率(要求100%) ②若合格率达标,按总页数5%比例抽检: 著录准确性/规范性/完整性(要求100%合格率) 图像清晰度/倾斜度/黑边(要求95%以上的合格率) ③结果填入《质量检查登记表》(附件3)。" wrapText="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
|
||||
<font>
|
||||
<Font size="18.0" />
|
||||
</font></TextArea>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
</AnchorPane>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.layout.AnchorPane?>
|
||||
<?import javafx.geometry.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.text.*?>
|
||||
|
||||
<AnchorPane xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1">
|
||||
|
||||
</AnchorPane>
|
||||
<AnchorPane prefHeight="8000.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17">
|
||||
<children>
|
||||
<TextArea editable="false" text="工作内容: 对照《元数据检查登记表》(附件4)检查并登记数字化项目信息、技术环境及技术参数的完整性等情况。" wrapText="true" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="10.0">
|
||||
<font>
|
||||
<Font size="18.0" />
|
||||
</font></TextArea>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
</AnchorPane>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.TextArea?>
|
||||
<?import javafx.scene.layout.AnchorPane?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<AnchorPane xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1">
|
||||
|
||||
</AnchorPane>
|
||||
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17">
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
<children>
|
||||
<TextArea editable="false" prefHeight="200.0" prefWidth="200.0" text="工作内容: ①检查档案管理系统或电子目录的挂接准确率(要求100%) ②逐件验证数字化成果与目录的关联性 ③结果填入《挂接检查登记表》(附件5)。" wrapText="true" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="10.0">
|
||||
<font>
|
||||
<Font size="18.0" />
|
||||
</font></TextArea>
|
||||
</children>
|
||||
</AnchorPane>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.TextArea?>
|
||||
<?import javafx.scene.layout.AnchorPane?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<AnchorPane xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1">
|
||||
|
||||
</AnchorPane>
|
||||
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<TextArea editable="false" prefHeight="200.0" prefWidth="200.0" text="工作内容: 对照《工作记录检查登记表》(附件6)检查数字化工作台帐的规范性及与成果的一致性,并在表格中登记检查情况。" wrapText="true" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="10.0">
|
||||
<font>
|
||||
<Font size="18.0" />
|
||||
</font></TextArea>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
</AnchorPane>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,156 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.layout.AnchorPane?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.TextArea?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.layout.ColumnConstraints?>
|
||||
<?import javafx.scene.layout.GridPane?>
|
||||
<?import javafx.scene.layout.RowConstraints?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<AnchorPane xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1">
|
||||
|
||||
</AnchorPane>
|
||||
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.StorageCarrierPaneController">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="226.33331298828125" minWidth="10.0" percentWidth="10.0" prefWidth="108.33333333333334" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="559.3333511352539" minWidth="10.0" percentWidth="40.0" prefWidth="373.00002034505206" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="345.3333435058594" minWidth="10.0" percentWidth="25.0" prefWidth="227.00004069010413" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="500.0" minWidth="10.0" percentWidth="25.0" prefWidth="400.0" />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints maxHeight="151.33334350585938" minHeight="0.0" percentHeight="7.0" prefHeight="55.00001017252603" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="407.0" minHeight="0.0" percentHeight="7.0" prefHeight="73.33333841959634" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="508.33333333333326" minHeight="0.0" percentHeight="7.0" prefHeight="73.99999491373697" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="591.6666666666667" minHeight="10.0" percentHeight="7.0" prefHeight="43.33332316080731" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="602.0000152587891" minHeight="10.0" prefHeight="534.6666615804037" vgrow="SOMETIMES" />
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<Label text="载入数字化成果:">
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<GridPane.margin>
|
||||
<Insets left="5.0" />
|
||||
</GridPane.margin>
|
||||
</Label>
|
||||
<Label text="将档案目录、哈希值列表文件和检测过程文件打包制成打包制成“数字化验收检测包.rar”压缩包" GridPane.columnSpan="4" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font name="System Bold" size="14.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<Label text="反馈结果:" GridPane.rowIndex="3">
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<GridPane.margin>
|
||||
<Insets left="5.0" />
|
||||
</GridPane.margin>
|
||||
</Label>
|
||||
<Label text="载入压缩包:" GridPane.rowIndex="2">
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<GridPane.margin>
|
||||
<Insets left="5.0" />
|
||||
</GridPane.margin>
|
||||
</Label>
|
||||
<TextField fx:id="loadDigitalOutcomes" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
</TextField>
|
||||
<TextField fx:id="loadCompressedFile" GridPane.columnIndex="1" GridPane.rowIndex="2">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
</TextField>
|
||||
<TextArea fx:id="result7TA" editable="false" GridPane.columnSpan="3" GridPane.hgrow="ALWAYS" GridPane.rowIndex="4">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
</TextArea>
|
||||
<Button fx:id="selectLoadDigitalOutcomes7B" mnemonicParsing="false" onAction="#onSelectLD" text="选择文件" textAlignment="CENTER" GridPane.columnIndex="2" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</Button>
|
||||
<Button fx:id="generateHashFile7B" mnemonicParsing="false" onAction="#onGenerateHF" text="生成哈希值列表文件" GridPane.columnIndex="3" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</Button>
|
||||
<Button fx:id="selectLoadCompressedFile7B" mnemonicParsing="false" onAction="#onSelectLC" text="选择文件" GridPane.columnIndex="2" GridPane.halignment="CENTER" GridPane.rowIndex="2" GridPane.valignment="CENTER">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</Button>
|
||||
<Button fx:id="caculateHash7B" mnemonicParsing="false" onAction="#onCaculateHash" text="计算哈希值" GridPane.columnIndex="3" GridPane.halignment="CENTER" GridPane.rowIndex="2" GridPane.valignment="CENTER">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</Button>
|
||||
<TextArea editable="false" prefWidth="400.0" text="①对照《存储载体检查登记表》(附件7)检查并记录存储载体的类型/数量/内容/可读性情况。 ②将数字化成果(包括单页、多页文件及目录)打包生成"数字化验收检测包.rar"(含目录、哈希值列表、检测文件) ③计算并验证压缩包的MD5或哈希值" wrapText="true" GridPane.columnIndex="3" GridPane.rowIndex="4">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</TextArea>
|
||||
<Label text="工作内容:" GridPane.columnIndex="3" GridPane.rowIndex="3">
|
||||
<GridPane.margin>
|
||||
<Insets left="10.0" />
|
||||
</GridPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
</Label>
|
||||
<Button fx:id="clearSelectedFoldersButton" mnemonicParsing="false" onAction="#onClearSelectedFolders" text="清除" GridPane.columnIndex="2">
|
||||
<font>
|
||||
<Font size="14.0" />
|
||||
</font>
|
||||
</Button>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
</GridPane>
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
module top.r3944realms.docchecktoolrefactored.test {
|
||||
requires static lombok;
|
||||
requires org.slf4j;
|
||||
requires top.r3944realms.docchecktoolrefactored;
|
||||
requires org.junit.jupiter.api;
|
||||
|
||||
exports top.r3944realms.docchecktoolrefactored.test;
|
||||
opens top.r3944realms.docchecktoolrefactored.test;
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
|
|||
import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
|
@ -28,16 +29,17 @@ public class DuplicateFinderPerformanceTest {
|
|||
|
||||
@RepeatedTest(5)
|
||||
void compareFinderPerformance() throws IOException {
|
||||
Path rootDir = Paths.get(TEST_PATH);
|
||||
// Test finder WITHOUT pre-counting
|
||||
long startWithoutPrecount = System.nanoTime();
|
||||
DuplicateFinder finderWithoutPrecount = new DuplicateFinder(scanner, hashCalculator, false);
|
||||
finderWithoutPrecount.findDuplicates(Paths.get(TEST_PATH));
|
||||
finderWithoutPrecount.findDuplicates(rootDir);
|
||||
long durationWithoutPrecount = System.nanoTime() - startWithoutPrecount;
|
||||
|
||||
// Test finder WITH pre-counting
|
||||
long startWithPrecount = System.nanoTime();
|
||||
DuplicateFinder finderWithPrecount = new DuplicateFinder(scanner, hashCalculator, true);
|
||||
finderWithPrecount.findDuplicates(Paths.get(TEST_PATH));
|
||||
finderWithPrecount.findDuplicates(rootDir);
|
||||
long durationWithPrecount = System.nanoTime() - startWithPrecount;
|
||||
|
||||
// Convert to milliseconds
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user