From 17955fac78cf579e9837e5f2c248fded3ddd620f Mon Sep 17 00:00:00 2001 From: Yeung Yiu Hung <446404+darkcl@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:12:54 +0800 Subject: [PATCH] feat(imagefs): avoid extraction when changing container variant feat(imagefs): report merge directory progress --- .../java/com/winlator/core/FileUtils.java | 45 +++++++ .../winlator/core/OnFileMergedListener.java | 5 + .../xenvironment/ImageFsInstaller.java | 125 +++++++++++------- 3 files changed, 124 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/com/winlator/core/OnFileMergedListener.java diff --git a/app/src/main/java/com/winlator/core/FileUtils.java b/app/src/main/java/com/winlator/core/FileUtils.java index a7d787df8..192d18dd6 100644 --- a/app/src/main/java/com/winlator/core/FileUtils.java +++ b/app/src/main/java/com/winlator/core/FileUtils.java @@ -25,15 +25,21 @@ import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Stack; import java.util.UUID; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -179,6 +185,45 @@ public static boolean isEmpty(File targetFile) { else return targetFile.length() == 0; } + public static void mergeDirectoryRecursively(File srcFile, File dstFile, OnFileMergedListener onProgress) throws IOException { + Path source = srcFile.toPath(); + Path target = dstFile.toPath(); + + Files.createDirectories(target); + + AtomicLong totalFiles = new AtomicLong(0); + try (var stream = Files.walk(source)) { + stream.filter(Files::isRegularFile) + .forEach(p -> totalFiles.incrementAndGet()); + } + + long total = totalFiles.get(); + AtomicLong processed = new AtomicLong(0); + + Files.walkFileTree(source, new SimpleFileVisitor<>() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + Path targetDir = target.resolve(source.relativize(dir)); + Files.createDirectories(targetDir); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Path targetFile = target.resolve(source.relativize(file)); + Files.copy(file, targetFile, LinkOption.NOFOLLOW_LINKS, StandardCopyOption.REPLACE_EXISTING); + + long done = processed.incrementAndGet(); + + if (onProgress != null) { + onProgress.onFileMerged(done, total); + } + + return FileVisitResult.CONTINUE; + } + }); + } + public static boolean copy(File srcFile, File dstFile) { return copy(srcFile, dstFile, null); } diff --git a/app/src/main/java/com/winlator/core/OnFileMergedListener.java b/app/src/main/java/com/winlator/core/OnFileMergedListener.java new file mode 100644 index 000000000..2138aa62f --- /dev/null +++ b/app/src/main/java/com/winlator/core/OnFileMergedListener.java @@ -0,0 +1,5 @@ +package com.winlator.core; + +public interface OnFileMergedListener { + void onFileMerged(long done, long filesCount); +} diff --git a/app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java b/app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java index cfa9a6d9a..883c75273 100644 --- a/app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java +++ b/app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java @@ -6,6 +6,8 @@ import android.content.res.AssetManager; import android.util.Log; +import androidx.annotation.NonNull; + import app.gamenative.R; import app.gamenative.enums.Marker; import app.gamenative.service.SteamService; @@ -77,6 +79,7 @@ public static void installWineFromDownloads(final Context context) { for (String version : versions) { File downloaded = new File(imageFs.getFilesDir(), version + ".txz"); File outFile = new File(rootDir, "/opt/" + version); + if (outFile.exists()) continue; outFile.mkdirs(); TarCompressorUtils.extract( TarCompressorUtils.Type.XZ, @@ -98,44 +101,7 @@ private static Future installFromAssetsFuture(final Context context, As // dialog.show(R.string.installing_system_files); return Executors.newSingleThreadExecutor().submit(() -> { clearRootDir(context, rootDir); - final byte compressionRatio = 22; - String imagefsFile = containerVariant.equals(Container.GLIBC) ? "imagefs_gamenative.txz" : "imagefs_bionic.txz"; - File downloaded = new File(imageFs.getFilesDir(), imagefsFile); - - boolean success = false; - - if (Arrays.asList(context.getAssets().list("")).contains(imagefsFile) == true){ - final long contentLength = (long) (FileUtils.getSize(assetManager, imagefsFile) * (100.0f / compressionRatio)); - AtomicLong totalSizeRef = new AtomicLong(); - Log.d("Extraction", "extracting " + imagefsFile); - - success = TarCompressorUtils.extract(TarCompressorUtils.Type.XZ, assetManager, imagefsFile, rootDir, (file, size) -> { - if (size > 0) { - long totalSize = totalSizeRef.addAndGet(size); - if (onProgress != null) { - final int progress = (int) (((float) totalSize / contentLength) * 100); - onProgress.call(progress); - } - } - return file; - }); - } - - else if (downloaded.exists()){ - final long contentLength = (long) (FileUtils.getSize(downloaded) * (100.0f / compressionRatio)); - AtomicLong totalSizeRef = new AtomicLong(); - Log.d("Extraction", "extracting " + imagefsFile); - success = TarCompressorUtils.extract(TarCompressorUtils.Type.XZ, downloaded, rootDir, (file, size) -> { - if (size > 0) { - long totalSize = totalSizeRef.addAndGet(size); - if (onProgress != null) { - final int progress = (int) (((float) totalSize / contentLength) * 100); - onProgress.call(progress); - } - } - return file; - }); - } + boolean success = extractImageFs(context, assetManager, containerVariant, onProgress, imageFs, rootDir); if (success) { Log.d("ImageFsInstaller", "Successfully installed system files"); @@ -158,6 +124,76 @@ else if (downloaded.exists()){ }); } + private static void mergeImageFsFolder(Context context, String containerVariant, File rootDir, Callback onProgress, Boolean isCleanInstall) throws IOException { + File variantDir = getVariantDir(context, containerVariant); + FileUtils.mergeDirectoryRecursively(variantDir, rootDir, (done, filesCount) -> { + if (onProgress == null) return; + + int progress = (int) (((float) done / filesCount) * 100); + if (isCleanInstall) { + onProgress.call(progress / 2 + 50); + } else { + onProgress.call(progress); + } + }); + } + + private static boolean extractImageFs(Context context, AssetManager assetManager, String containerVariant, Callback onProgress, ImageFs imageFs, File rootDir) throws IOException { + final byte compressionRatio = 22; + + String imagefsFile = containerVariant.equals(Container.GLIBC) ? "imagefs_gamenative.txz" : "imagefs_bionic.txz"; + File downloaded = new File(context.getFilesDir(), imagefsFile); + + File variantDir = getVariantDir(context, containerVariant); + if (variantDir.exists()) { + mergeImageFsFolder(context, containerVariant, rootDir, onProgress, false); + return true; + } + + + boolean success = false; + + if (Arrays.asList(context.getAssets().list("")).contains(imagefsFile) == true){ + final long contentLength = (long) (FileUtils.getSize(assetManager, imagefsFile) * (100.0f / compressionRatio)); + AtomicLong totalSizeRef = new AtomicLong(); + Log.d("Extraction", "extracting " + imagefsFile); + + success = TarCompressorUtils.extract(TarCompressorUtils.Type.XZ, assetManager, imagefsFile, variantDir, (file, size) -> { + if (size > 0) { + long totalSize = totalSizeRef.addAndGet(size); + if (onProgress != null) { + final int progress = (int) (((float) totalSize / contentLength) * 100 / 2); + onProgress.call(progress); + } + } + return file; + }); + } + + else if (downloaded.exists()){ + final long contentLength = (long) (FileUtils.getSize(downloaded) * (100.0f / compressionRatio)); + AtomicLong totalSizeRef = new AtomicLong(); + Log.d("Extraction", "extracting " + imagefsFile); + success = TarCompressorUtils.extract(TarCompressorUtils.Type.XZ, downloaded, variantDir, (file, size) -> { + if (size > 0) { + long totalSize = totalSizeRef.addAndGet(size); + if (onProgress != null) { + final int progress = (int) (((float) totalSize / contentLength) * 100 / 2); + onProgress.call(progress); + } + } + return file; + }); + } + mergeImageFsFolder(context, containerVariant, rootDir, onProgress, true); + return success; + } + + @NonNull + private static File getVariantDir(Context context, String containerVariant) { + return new File(context.getFilesDir(), containerVariant.equals(Container.GLIBC) ? "glibc/imagefs" : "bionic/imagefs"); + } + private static void installGuestLibs(Context ctx) { final String ASSET_TAR = "redirect.tzst"; // ➊ add this to assets/ File imagefs = new File(ctx.getFilesDir(), "imagefs"); @@ -216,19 +252,6 @@ private static boolean isImportedWineProton(Context context, String fileName) { return false; } - // Get bundled versions from resource arrays - String[] bionicWineEntries = context.getResources().getStringArray(R.array.bionic_wine_entries); - String[] glibcWineEntries = context.getResources().getStringArray(R.array.glibc_wine_entries); - - // Check if it's a bundled version - for (String version : bionicWineEntries) { - if (lowerName.equals(version.toLowerCase())) return false; - } - for (String version : glibcWineEntries) { - if (lowerName.equals(version.toLowerCase())) return false; - } - - // It's Wine/Proton but not bundled, so it's imported return true; }