mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Preserve Epic game files when storage checks reject updates
This commit is contained in:
1 parent
93cd54c93b
commit
8ff1f5dd20
3 files changed
+53
-8
No files matched your search
@@ -205,15 +205,11 @@ public final class EpicDownloadManager {
|
||||
if (!chunkCacheDir.isDirectory() && !chunkCacheDir.mkdirs()) throw new InstallException("Could not create Epic chunk cache");
|
||||
Map<String, Long> cachedChunkBytes = prepareBoundedCache(chunkCacheDir, needed);
|
||||
long pendingBytes = 0;
|
||||
long reclaimableBytes = 0;
|
||||
for (FileInfo f : pending) {
|
||||
pendingBytes += f.fileSize();
|
||||
File output = new File(installDir, f.filename.replace("\\", "/"));
|
||||
File parent = output.getParentFile();
|
||||
if (parent != null) parent.mkdirs();
|
||||
// This file failed the delta check, so it cannot be resumed as a valid install.
|
||||
// Remove it before estimating/writing its replacement to avoid requiring space
|
||||
// for both the corrupt file and its verified replacement.
|
||||
if (output.isFile() && !output.delete() && output.exists()) throw new InstallException("Could not replace incomplete file " + output.getName());
|
||||
if (output.isFile()) reclaimableBytes += output.length();
|
||||
}
|
||||
boolean cacheOnInstallVolume = cachePath.isEmpty();
|
||||
long peakGameAdditional = cacheOnInstallVolume
|
||||
@@ -221,13 +217,23 @@ public final class EpicDownloadManager {
|
||||
: pendingBytes;
|
||||
long peakCacheAdditional = cacheOnInstallVolume
|
||||
? 0 : estimatePeakAdditionalBytes(pending, chunksByGuid, cachedChunkBytes, false);
|
||||
peakGameAdditional = Math.max(0, peakGameAdditional - reclaimableBytes);
|
||||
long freeGame = installDir.getUsableSpace();
|
||||
if (cacheOnInstallVolume) {
|
||||
if (freeGame > 0 && peakGameAdditional > freeGame) throw new InstallException("Not enough free space for the bounded Epic install: need up to " + fmt(peakGameAdditional) + " additional, only " + fmt(freeGame) + " free");
|
||||
} else {
|
||||
long freeCache = chunkCacheDir.getUsableSpace();
|
||||
if (freeCache > 0 && peakCacheAdditional > freeCache) throw new InstallException("Not enough internal space for the bounded Epic download cache: need up to " + fmt(peakCacheAdditional) + " additional, only " + fmt(freeCache) + " free");
|
||||
if (freeGame > 0 && pendingBytes > freeGame) throw new InstallException("Not enough free space: need " + fmt(pendingBytes) + ", only " + fmt(freeGame) + " free");
|
||||
if (freeGame > 0 && peakGameAdditional > freeGame) throw new InstallException("Not enough free space: need " + fmt(peakGameAdditional) + ", only " + fmt(freeGame) + " free");
|
||||
}
|
||||
if (cancel.get()) return null;
|
||||
// Reclaim invalid/old outputs only after both volume checks accept the update.
|
||||
// The peak estimate assumes these bytes are released before replacement writes.
|
||||
for (FileInfo f : pending) {
|
||||
File output = new File(installDir, f.filename.replace("\\", "/"));
|
||||
File parent = output.getParentFile();
|
||||
if (parent != null) parent.mkdirs();
|
||||
if (output.isFile() && !output.delete() && output.exists()) throw new InstallException("Could not replace incomplete file " + output.getName());
|
||||
}
|
||||
cb.onLog("epic: cache " + (cacheOnInstallVolume ? "beside the game" : "scratch") + ", peak game additional=" + fmt(peakGameAdditional) + (cacheOnInstallVolume ? "" : ", peak cache additional=" + fmt(peakCacheAdditional)) + " for " + fmt(pendingBytes) + " of files");
|
||||
final long fTotalBytes = pendingBytes;
|
||||
|
||||
@@ -101,6 +101,43 @@ class EpicBoundedInstallTest {
|
||||
assertEquals("Verified files must not be downloaded again", 1, requests.get())
|
||||
}
|
||||
|
||||
private fun oversizedUpdate() {
|
||||
val file = manifest.files[0]
|
||||
val part = file.parts[0]
|
||||
file.parts.clear()
|
||||
repeat(100_000) {
|
||||
file.parts.add(EpicDownloadManager.ChunkPart().apply {
|
||||
guid = part.guid
|
||||
size = Int.MAX_VALUE
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun insufficientInstallSpacePreservesExistingFilesOnBothCacheLayouts() {
|
||||
val oldGame = byteArrayOf(9, 8, 7)
|
||||
val oldPart = byteArrayOf(6, 5, 4)
|
||||
File(root, "game.bin").writeBytes(oldGame)
|
||||
File(root, "game.bin.part").writeBytes(oldPart)
|
||||
oversizedUpdate()
|
||||
assertTrue(manifest.files[0].fileSize() > root.usableSpace)
|
||||
|
||||
for (cache in listOf("", temp.newFolder("scratch-rejected").path)) {
|
||||
val error = assertThrows(EpicDownloadManager.InstallException::class.java) { install(cache = cache) }
|
||||
assertTrue(error.message.orEmpty().contains("Not enough free space"))
|
||||
assertArrayEquals(oldGame, File(root, "game.bin").readBytes())
|
||||
assertArrayEquals(oldPart, File(root, "game.bin.part").readBytes())
|
||||
}
|
||||
assertEquals(0, requests.get())
|
||||
}
|
||||
|
||||
@Test fun acceptedUpdateReplacesOutdatedFilesWithVerifiedOutputs() {
|
||||
File(root, "game.bin").writeBytes(byteArrayOf(9, 8, 7))
|
||||
File(root, "game.bin.part").writeBytes(byteArrayOf(6, 5, 4))
|
||||
assertNotNull(install())
|
||||
assertArrayEquals(bytes.copyOfRange(0, bytes.size / 2), File(root, "game.bin").readBytes())
|
||||
assertArrayEquals(bytes.copyOfRange(bytes.size / 2, bytes.size), File(root, "game.bin.part").readBytes())
|
||||
}
|
||||
|
||||
@Test fun pauseKeepsCompletedFilesAndResumeReusesSharedCache() {
|
||||
val cancel = AtomicBoolean()
|
||||
val callback = object : EpicDownloadManager.Callback {
|
||||
|
||||
@@ -51,7 +51,9 @@ Streaming stays in the Download stage until all files are written and verified,
|
||||
Install for registration; it does not bounce between stages for each file.
|
||||
|
||||
The space check follows the manifest delta pass and simulates file writes, chunk fetches,
|
||||
and evictions, rather than adding the entire cache to the entire game. Completed files and
|
||||
and evictions, rather than adding the entire cache to the entire game. It accounts for space
|
||||
reclaimable from outdated outputs without deleting them until both game and cache volume
|
||||
checks pass. An insufficient-space rejection preserves existing game files. Completed files and
|
||||
verified cached chunks are accounted for on resume. On a card, game and cache requirements
|
||||
are checked separately. File SHA-1 hashes are checked where supplied by the manifest; failed
|
||||
or cancelled output files are removed, and an interrupted process is repaired by the next
|
||||
|
||||
Reference in new issue
Block a user