Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,17 @@ public void setDownloadThreads(int downloadThreads) {
this.downloadThreads.set(downloadThreads);
}

@SerializedName("hardlink")
private final BooleanProperty hardlink = new SimpleBooleanProperty(false);

public BooleanProperty hardlinkProperty() { return hardlink; }

public boolean getHardlink() {
return hardlink.get();
}

public void setHardlink(boolean hardlink) { this.hardlink.set(hardlink); }

@SerializedName("downloadType")
private final StringProperty downloadType = new SimpleStringProperty(DownloadProviders.DEFAULT_RAW_PROVIDER_ID);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.ResponseCodeException;

import javax.net.ssl.SSLHandshakeException;
Expand Down Expand Up @@ -60,6 +61,9 @@ private DownloadProviders() {
@SuppressWarnings("unused")
private static final InvalidationListener observer;

@SuppressWarnings("unused")
private static final InvalidationListener hardlinkObserver;

static {
String bmclapiRoot = "https://bmclapi2.bangbang93.com";
String bmclapiRootOverride = System.getProperty("hmcl.bmclapi.override");
Expand All @@ -86,6 +90,9 @@ private DownloadProviders() {
config().getAutoDownloadThreads() ? DEFAULT_CONCURRENCY : config().getDownloadThreads());
}, config().autoDownloadThreadsProperty(), config().downloadThreadsProperty());

hardlinkObserver = FXUtils.observeWeak(() -> FileUtils.setHardLink(config().getHardlink()),
config().hardlinkProperty());

provider = new DownloadProviderWrapper(MOJANG);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,63 +105,90 @@ public DownloadSettingsPage() {
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.download_source")), downloadSource);
}

ComponentList downloadList = new ComponentList();
{
VBox downloadThreads = new VBox(16);
downloadThreads.getStyleClass().add("card-non-transparent");
{
VBox downloadThreads = new VBox(16);
downloadList.getContent().add(downloadThreads);
{
JFXCheckBox chkAutoDownloadThreads = new JFXCheckBox(i18n("settings.launcher.download.threads.auto"));
VBox.setMargin(chkAutoDownloadThreads, new Insets(8, 0, 0, 0));
chkAutoDownloadThreads.selectedProperty().bindBidirectional(config().autoDownloadThreadsProperty());
downloadThreads.getChildren().add(chkAutoDownloadThreads);

chkAutoDownloadThreads.selectedProperty().addListener((a, b, newValue) -> {
if (newValue) {
config().downloadThreadsProperty().set(FetchTask.DEFAULT_CONCURRENCY);
}
});
}
{
JFXCheckBox chkAutoDownloadThreads = new JFXCheckBox(i18n("settings.launcher.download.threads.auto"));
VBox.setMargin(chkAutoDownloadThreads, new Insets(8, 0, 0, 0));
chkAutoDownloadThreads.selectedProperty().bindBidirectional(config().autoDownloadThreadsProperty());
downloadThreads.getChildren().add(chkAutoDownloadThreads);

chkAutoDownloadThreads.selectedProperty().addListener((a, b, newValue) -> {
if (newValue) {
config().downloadThreadsProperty().set(FetchTask.DEFAULT_CONCURRENCY);
}
});
}

{
HBox hbox = new HBox(8);
hbox.setStyle("-fx-view-order: -1;"); // prevent the indicator from being covered by the hint
hbox.setAlignment(Pos.CENTER);
hbox.setPadding(new Insets(0, 0, 0, 30));
hbox.disableProperty().bind(config().autoDownloadThreadsProperty());
Label label = new Label(i18n("settings.launcher.download.threads"));

JFXSlider slider = new JFXSlider(1, 256, 64);
HBox.setHgrow(slider, Priority.ALWAYS);

JFXTextField threadsField = new JFXTextField();
FXUtils.setLimitWidth(threadsField, 60);
FXUtils.bindInt(threadsField, config().downloadThreadsProperty());

AtomicBoolean changedByTextField = new AtomicBoolean(false);
FXUtils.onChangeAndOperate(config().downloadThreadsProperty(), value -> {
changedByTextField.set(true);
slider.setValue(value.intValue());
changedByTextField.set(false);
});
slider.valueProperty().addListener((value, oldVal, newVal) -> {
if (changedByTextField.get()) return;
config().downloadThreadsProperty().set(value.getValue().intValue());
});

hbox.getChildren().setAll(label, slider, threadsField);
downloadThreads.getChildren().add(hbox);
}

{
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
VBox.setMargin(hintPane, new Insets(0, 0, 0, 30));
hintPane.disableProperty().bind(config().autoDownloadThreadsProperty());
hintPane.setText(i18n("settings.launcher.download.threads.hint"));
downloadThreads.getChildren().add(hintPane);
}
}
}
{
VBox hardLink = new VBox(16);
downloadList.getContent().add(hardLink);
{
HBox hbox = new HBox(8);
hbox.setStyle("-fx-view-order: -1;"); // prevent the indicator from being covered by the hint
hbox.setAlignment(Pos.CENTER);
hbox.setPadding(new Insets(0, 0, 0, 30));
hbox.disableProperty().bind(config().autoDownloadThreadsProperty());
Label label = new Label(i18n("settings.launcher.download.threads"));

JFXSlider slider = new JFXSlider(1, 256, 64);
HBox.setHgrow(slider, Priority.ALWAYS);

JFXTextField threadsField = new JFXTextField();
FXUtils.setLimitWidth(threadsField, 60);
FXUtils.bindInt(threadsField, config().downloadThreadsProperty());

AtomicBoolean changedByTextField = new AtomicBoolean(false);
FXUtils.onChangeAndOperate(config().downloadThreadsProperty(), value -> {
changedByTextField.set(true);
slider.setValue(value.intValue());
changedByTextField.set(false);
});
slider.valueProperty().addListener((value, oldVal, newVal) -> {
if (changedByTextField.get()) return;
config().downloadThreadsProperty().set(value.getValue().intValue());
});

hbox.getChildren().setAll(label, slider, threadsField);
downloadThreads.getChildren().add(hbox);
JFXCheckBox chkHardLink = new JFXCheckBox(i18n("settings.launcher.download.hardlink"));
hardLink.getChildren().add(chkHardLink);
VBox.setMargin(chkHardLink, new Insets(8, 0, 0, 0));
chkHardLink.selectedProperty().bindBidirectional(config().hardlinkProperty());
}

{
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hardLink.getChildren().add(hintPane);
VBox.setMargin(hintPane, new Insets(0, 0, 0, 30));
hintPane.setText(i18n("settings.launcher.download.hardlink.hint"));
}

{
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
hardLink.getChildren().add(hintPane);
VBox.setMargin(hintPane, new Insets(0, 0, 0, 30));
hintPane.disableProperty().bind(config().autoDownloadThreadsProperty());
hintPane.setText(i18n("settings.launcher.download.threads.hint"));
downloadThreads.getChildren().add(hintPane);
hintPane.setText(i18n("settings.launcher.download.hardlink.note"));
}
}

content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("download")), downloadThreads);
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("download")), downloadList);
}

{
Expand Down
3 changes: 3 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,9 @@ settings.launcher.download.threads.auto=Automatically Determine
settings.launcher.download.threads.hint=Too many threads may cause your system to freeze, and your download speed may be affected by your ISP and download servers. It is not always the case that more threads increase your download speed.
settings.launcher.download_source=Download Source
settings.launcher.download_source.auto=Automatically Choose Download Sources
settings.launcher.download.hardlink=Hard Link Instead of Copying
settings.launcher.download.hardlink.hint=Using hard links saves significant disk space and time by creating multiple file entries for the same file data, rather than duplicating the data itself. To enable this, ensure your cache folder is located on the same drive or file system partition as your downloads folder. On Windows, you must also enable Developer Mode in your system settings.
settings.launcher.download.hardlink.note=Files linked in the assets, libraries, mods, resourcepacks, shaderpacks directories as well as other files downloaded via HMCL are hard links and should not be modified directly. If you need to alter a file, make a copy of it first and then delete the original source file.
settings.launcher.enable_game_list=Show instance list in homepage
settings.launcher.font=Font
settings.launcher.font.anti_aliasing=Anti-aliasing
Expand Down
3 changes: 3 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,9 @@ settings.launcher.download.threads.auto=自动选择线程数
settings.launcher.download.threads.hint=线程数过高可能导致系统卡顿。你的下载速度会受到互联网运营商、下载源服务器等方面的影响。调高下载线程数不一定能大幅提升总下载速度。
settings.launcher.download_source=下载源
settings.launcher.download_source.auto=自动选择下载源
settings.launcher.download.hardlink=使用硬链接替代复制
settings.launcher.download.hardlink.hint=硬链接通过创建指向相同文件数据的多条文件条目(而非复制数据本身),能显著节省磁盘空间和时间。启用此功能时,请确保缓存文件夹与下载文件夹位于同一驱动器或文件系统分区。在Windows系统中,还需在系统设置中启用开发者模式。
settings.launcher.download.hardlink.note=assets,libraries,mods,resourcepacks,shaderpacks目录中的文件,以及其他通过HMCL下载的文件均为硬链接,请勿直接修改。如需修改文件,请先创建副本,再删除原始文件。
settings.launcher.enable_game_list=在主页内显示版本列表
settings.launcher.font=字体
settings.launcher.font.anti_aliasing=抗锯齿 (重启后生效)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ public Path cacheLibrary(Library library, Path path, boolean forge) throws IOExc
hash = DigestUtils.digestToString(SHA1, path);

Path cache = getFile(SHA1, hash);
FileUtils.copyFile(path, cache);
FileUtils.linkFile(path, cache);

Lock writeLock = lock.writeLock();
writeLock.lock();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ protected EnumCheckETag shouldCheckETag() {
Optional<Path> cache = repository.checkExistentFile(candidate, integrityCheck.getAlgorithm(), integrityCheck.getChecksum());
if (cache.isPresent()) {
try {
FileUtils.copyFile(cache.get(), file);
FileUtils.linkFile(cache.get(), file);
LOG.trace("Successfully verified file " + file + " from " + uris.get(0));
return EnumCheckETag.CACHED;
} catch (IOException e) {
Expand All @@ -181,7 +181,7 @@ protected void beforeDownload(URI uri) {

@Override
protected void useCachedResult(Path cache) throws IOException {
FileUtils.copyFile(cache, file);
FileUtils.linkFile(cache, file);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,12 @@ protected boolean fileExists(String algorithm, String hash) {
public void tryCacheFile(Path path, String algorithm, String hash) throws IOException {
Path cache = getFile(algorithm, hash);
if (Files.isRegularFile(cache)) return;
FileUtils.copyFile(path, cache);
FileUtils.linkFile(path, cache);
}

public Path cacheFile(Path path, String algorithm, String hash) throws IOException {
Path cache = getFile(algorithm, hash);
FileUtils.copyFile(path, cache);
FileUtils.linkFile(path, cache);
return cache;
}

Expand Down
50 changes: 47 additions & 3 deletions HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
* @author huang
*/
public final class FileUtils {
private static volatile boolean hardLink = false;

private FileUtils() {
}
Expand Down Expand Up @@ -452,21 +453,64 @@ public static void forceDelete(Path file)
Files.delete(file);
}

public static void copyFile(Path srcFile, Path destFile)
throws IOException {
public static void setHardLink(boolean value) {
hardLink = value;
}

private static boolean checkCopy(Path srcFile, Path destFile) throws IOException {
Objects.requireNonNull(srcFile, "Source must not be null");
Objects.requireNonNull(destFile, "Destination must not be null");
if (!Files.exists(srcFile))
throw new FileNotFoundException("Source '" + srcFile + "' does not exist");
if (Files.isDirectory(srcFile))
throw new IOException("Source '" + srcFile + "' exists but is a directory");
Files.createDirectories(destFile.getParent());
if (Files.exists(destFile) && !Files.isWritable(destFile))
boolean destExist = Files.exists(destFile);
if (destExist && !Files.isWritable(destFile))
throw new IOException("Destination '" + destFile + "' exists but is read-only");
return destExist;
}

private static boolean isSameDisk(Path srcFile, Path destFile) {
do {
destFile = destFile.getParent();
} while (!Files.exists(destFile));

try {
return Files.getFileStore(srcFile).equals(Files.getFileStore(destFile));
} catch (IOException e) {
LOG.warning(e.toString());
return true;
}
}

private static void copy(Path srcFile, Path destFile) throws IOException {
Files.copy(srcFile, destFile, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
}

public static void copyFile(Path srcFile, Path destFile) throws IOException {
checkCopy(srcFile, destFile);
copy(srcFile, destFile);
}

public static void linkFile(Path srcFile, Path destFile) throws IOException {
boolean exist = checkCopy(srcFile, destFile);
if (hardLink && isSameDisk(srcFile, destFile)) {
if (exist) Files.delete(destFile);
try {
Files.createLink(destFile, srcFile);
return;
} catch (FileAlreadyExistsException e) {
LOG.warning("File has already been created by another thread", e);
return;
} catch (Throwable e) {
hardLink = false;
LOG.warning("Failed to create hardlink", e);
}
}
copy(srcFile, destFile);
}

public static List<Path> listFilesByExtension(Path file, String extension) {
try (Stream<Path> list = Files.list(file)) {
return list.filter(it -> Files.isRegularFile(it) && extension.equals(getExtension(it)))
Expand Down
Loading