diff --git a/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java b/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java index c63458e2..14cd5e5c 100644 --- a/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +++ b/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java @@ -352,7 +352,13 @@ private boolean transferArchive( long received = 0; int currentPercentage = 0; long lastPostedBytes = baseOffset; - long lastFreeSpaceProbeBytes = 0; + // Unknown length: the response-time check could only reserve the + // margin, so the disk is probed before any write that would run + // past the bytes reserved so far, each probe reserving at least + // the next PROBE bytes (more when one chunk is larger) — no write + // can ever eat into the margin (the archive cap alone is far more + // than the margin). A throw keeps the partial for a later attempt. + long freeSpaceReservedUntil = 0; try ( BufferedSource source = body.source(); @@ -361,6 +367,20 @@ private boolean transferArchive( ) { while ((bytesRead = source.read(sink.buffer(), DOWNLOAD_CHUNK_SIZE)) != -1) { received += bytesRead; + if (totalAll <= 0 && received > freeSpaceReservedUntil) { + long reserve = Math.max( + ArchiveLimits.UNKNOWN_LENGTH_FREE_SPACE_PROBE_BYTES, bytesRead); + freeSpaceReservedUntil = received - bytesRead + reserve; + try { + ArchiveLimits.ensureFreeSpace(writePath, reserve); + } catch (IOException e) { + // The chunk is still only buffered; closing the + // sink would flush it onto the disk that just + // failed the probe. + sink.buffer().clear(); + throw e; + } + } sink.emit(); long overall = baseOffset + received; @@ -369,15 +389,6 @@ private boolean transferArchive( throw new IOException( "archive too large: exceeded " + ArchiveLimits.MAX_ARCHIVE_BYTES); } - if (totalAll <= 0 && received - lastFreeSpaceProbeBytes - >= ArchiveLimits.UNKNOWN_LENGTH_FREE_SPACE_PROBE_BYTES) { - // Unknown length: the response-time check could only - // reserve the margin, so re-probe as bytes stream in - // (the archive cap alone is far more than the margin). - // A throw keeps the partial for a later attempt. - lastFreeSpaceProbeBytes = received; - ArchiveLimits.ensureFreeSpace(writePath, DOWNLOAD_CHUNK_SIZE); - } if (totalAll > 0) { int percentage = (int) (overall * 100.0 / totalAll + 0.5); if (percentage > currentPercentage) { diff --git a/cpp/patch_core/archive_limits.h b/cpp/patch_core/archive_limits.h index 98b9e643..65c57983 100644 --- a/cpp/patch_core/archive_limits.h +++ b/cpp/patch_core/archive_limits.h @@ -33,7 +33,9 @@ constexpr long long kMaxManifestBytes = 16LL * 1024 * 1024; constexpr long long kFreeDiskMarginBytes = 64LL * 1024 * 1024; // A download whose length is unknown up front (chunked / encoded body) can // only reserve the margin when the response arrives; the disk is re-probed -// every this many streamed bytes so the cap above cannot eat the margin. +// before any write that would run past the bytes reserved so far, each +// probe reserving at least this many bytes ahead (a single larger chunk +// reserves its own size), so no write can ever eat into the margin. constexpr long long kUnknownLengthFreeSpaceProbeBytes = 8LL * 1024 * 1024; } // namespace archive_limits diff --git a/harmony/pushy/src/main/ets/DownloadTask.ts b/harmony/pushy/src/main/ets/DownloadTask.ts index a97ac40d..46af09cf 100644 --- a/harmony/pushy/src/main/ets/DownloadTask.ts +++ b/harmony/pushy/src/main/ets/DownloadTask.ts @@ -679,7 +679,7 @@ export class DownloadTask { } }; - let lastFreeSpaceProbeBytes = 0; + let freeSpaceReservedUntil = 0; const enqueueWrite = (data: ArrayBuffer) => { received += data.byteLength; if (!writeError && baseOffset + received > MAX_ARCHIVE_BYTES) { @@ -689,12 +689,18 @@ export class DownloadTask { `archive too large: exceeded ${MAX_ARCHIVE_BYTES}`, ); } - const probeFreeSpace = - totalAll <= 0 && - received - lastFreeSpaceProbeBytes >= - UNKNOWN_LENGTH_FREE_SPACE_PROBE_BYTES; + // 未知长度:响应到达时只能预留安全余量,所以任何会写过已预留字节数的 + // 写入之前先探测,每次至少预留接下来的 PROBE 字节(单个 chunk 更大时按 + // chunk 算)——任何写入都吃不到余量(归档上限本身远大于余量)。失败保留 + // partial 供下次续传。 + const writtenBefore = received - data.byteLength; + const probeFreeSpace = totalAll <= 0 && received > freeSpaceReservedUntil; + const reserve = Math.max( + UNKNOWN_LENGTH_FREE_SPACE_PROBE_BYTES, + data.byteLength, + ); if (probeFreeSpace) { - lastFreeSpaceProbeBytes = received; + freeSpaceReservedUntil = writtenBefore + reserve; } writeQueue = writeQueue.then(async () => { if (!writer || writeError) { @@ -702,9 +708,7 @@ export class DownloadTask { } try { if (probeFreeSpace) { - // 未知长度:响应到达时只能预留安全余量,边收边重新探测磁盘 - // (归档上限本身远大于余量)。失败保留 partial 供下次续传。 - await ensureFreeSpace(params.targetFile, data.byteLength); + await ensureFreeSpace(params.targetFile, reserve); } await fileIo.write(writer.fd, data); } catch (error) { diff --git a/ios/RCTPushy/RCTPushyDownloader.mm b/ios/RCTPushy/RCTPushyDownloader.mm index d6b68ee6..ceb6bc03 100644 --- a/ios/RCTPushy/RCTPushyDownloader.mm +++ b/ios/RCTPushy/RCTPushyDownloader.mm @@ -145,8 +145,9 @@ @interface RCTPushyDownloader() @property (nonatomic, assign) BOOL discardPartial; @property (nonatomic, assign) int lastReportedPercentage; @property (nonatomic, assign) long long lastReportedBytes; -// receivedBytes at the last streaming free-space probe (unknown-length bodies) -@property (nonatomic, assign) long long lastFreeSpaceProbeBytes; +// receivedBytes up to which free disk has been probed for (unknown-length +// bodies; 0 = probe before the first write) +@property (nonatomic, assign) long long freeSpaceReservedUntil; @end @implementation RCTPushyDownloader @@ -471,7 +472,7 @@ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)data if (!append) { [fileManager createFileAtPath:self.savePath contents:nil attributes:nil]; } - self.lastFreeSpaceProbeBytes = 0; + self.freeSpaceReservedUntil = 0; self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.savePath]; if (self.fileHandle == nil) { [self failWithDescription:@"cannot open download file for writing" code:-1]; @@ -523,14 +524,18 @@ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)data return; } if (self.expectedTotal <= 0 - && self.receivedBytes - self.lastFreeSpaceProbeBytes - >= pushy::archive_limits::kUnknownLengthFreeSpaceProbeBytes) { + && self.receivedBytes + (long long)data.length > self.freeSpaceReservedUntil) { // Unknown/encoded length: the response-time check could only - // reserve the margin, so re-probe the disk as bytes stream in. The - // archive cap alone (512 MiB) is far more than the margin protects. - // The partial stays: a later attempt may find the space. - self.lastFreeSpaceProbeBytes = self.receivedBytes; - NSString *shortfall = RCTPushyFreeSpaceShortfall(self.savePath, data.length); + // reserve the margin, so probe before any write that would run past + // the bytes reserved so far, each probe reserving at least the next + // PROBE bytes (more when one callback carries a larger buffer) — no + // write can ever eat into the margin. The archive cap alone (512 + // MiB) is far more than the margin protects. The partial stays: a + // later attempt may find the space. + long long reserve = MAX(pushy::archive_limits::kUnknownLengthFreeSpaceProbeBytes, + (long long)data.length); + self.freeSpaceReservedUntil = self.receivedBytes + reserve; + NSString *shortfall = RCTPushyFreeSpaceShortfall(self.savePath, reserve); if (shortfall != nil) { [self failWithDescription:shortfall code:-1]; [dataTask cancel];