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
4 changes: 4 additions & 0 deletions flutter_cache_manager/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## [3.4.6] - 2026-10-02

* Retries the atomic rename in `JsonCacheInfoRepository` briefly when Windows reports that another process has the cache info file or its temp file open (`ERROR_SHARING_VIOLATION` / `ERROR_ACCESS_DENIED`), as antivirus and the search indexer often do just after a write. The write previously failed and stayed pending until the next change.

## [3.4.5] - 2026-09-18

* Prevents image resize decoding from completing more than once and propagates decoding errors ([#400](https://github.com/Baseflow/flutter_cache_manager/issues/400)).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,40 @@ class JsonCacheInfoRepository extends CacheInfoRepository
final content = jsonEncode(_jsonCache.values.toList());
final tempFile = _createSiblingFile('${file.path}.tmp');
await tempFile.writeAsString(content, flush: true);
await tempFile.rename(file.path);
await _renameOver(tempFile, file.path);
}

/// The pauses between attempts of [_renameOver], about 750ms in all.
static const _renameRetryDelays = [
Duration(milliseconds: 50),
Duration(milliseconds: 100),
Duration(milliseconds: 200),
Duration(milliseconds: 400),
];

/// Renames [tempFile] to [path], retrying briefly while another process has
/// one of the two files open.
///
/// Windows refuses to rename over a file another process holds open without
/// delete sharing, failing with ERROR_SHARING_VIOLATION (32) or
/// ERROR_ACCESS_DENIED (5). Antivirus and the search indexer routinely open a
/// file just after it is written, so the freshly written temp file is often
/// held for a moment. Elsewhere a rename never fails with 32 (EPIPE), and 5
/// (EIO) is worth the same few retries.
Future<void> _renameOver(File tempFile, String path) async {
for (var attempt = 0; ; attempt++) {
try {
await tempFile.rename(path);
return;
} on FileSystemException catch (e) {
final errorCode = e.osError?.errorCode;
if (attempt == _renameRetryDelays.length ||
(errorCode != 32 && errorCode != 5)) {
rethrow;
}
await Future<void>.delayed(_renameRetryDelays[attempt]);
}
}
}

/// Creates a file next to [_file] on the same file system.
Expand Down
2 changes: 1 addition & 1 deletion flutter_cache_manager/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: flutter_cache_manager
description: Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite.
version: 3.4.5
version: 3.4.6
homepage: https://github.com/Baseflow/flutter_cache_manager
topics:
- cache
Expand Down
123 changes: 123 additions & 0 deletions flutter_cache_manager/test/repositories/json_file_repository_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io';

import 'package:collection/collection.dart';
import 'package:file/file.dart' as pf;
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/src/storage/cache_info_repositories/json_cache_info_repository.dart';
import 'package:flutter_cache_manager/src/storage/cache_object.dart';
Expand Down Expand Up @@ -282,7 +283,129 @@ void main() {
final allObjects = await repo2.getAllObjects();
expect(allObjects.length, JsonRepoHelpers.startCacheObjects.length + 1);
});

group('A rename refused by another process', () {
Future<(JsonCacheInfoRepository, pf.File, _FlakyRenameFileSystem)>
createRepository({required int failures, required int errorCode}) async {
final original = await JsonRepoHelpers.createDatabaseFile();
final fileSystem = _FlakyRenameFileSystem(
original.fileSystem,
failures: failures,
errorCode: errorCode,
);
final file = fileSystem.file(original.path);
final repo = JsonCacheInfoRepository.withFile(file);
await repo.open();
return (repo, file, fileSystem);
}

Future<List<FlutterErrorDetails>> insertCollectingErrors(
JsonCacheInfoRepository repo,
) async {
final errors = <FlutterErrorDetails>[];
final originalOnError = FlutterError.onError;
FlutterError.onError = errors.add;
try {
await repo.insert(JsonRepoHelpers.extraCacheObject);
} finally {
FlutterError.onError = originalOnError;
}
return errors;
}

test('is retried until it succeeds', () async {
final (repo, file, fileSystem) = await createRepository(
failures: 2,
errorCode: 32,
);

final errors = await insertCollectingErrors(repo);

expect(errors, isEmpty);
expect(fileSystem.renameAttempts, 3);
final repo2 = JsonCacheInfoRepository.withFile(file);
await repo2.open();
final allObjects = await repo2.getAllObjects();
expect(allObjects.length, JsonRepoHelpers.startCacheObjects.length + 1);
});

test('is reported once the retries run out', () async {
final (repo, _, fileSystem) = await createRepository(
failures: 100,
errorCode: 5,
);

final errors = await insertCollectingErrors(repo);

expect(errors, hasLength(1));
expect(fileSystem.renameAttempts, 5);
});

test('is not retried for other errors', () async {
final (repo, _, fileSystem) = await createRepository(
failures: 1,
errorCode: 2,
);

final errors = await insertCollectingErrors(repo);

expect(errors, hasLength(1));
expect(fileSystem.renameAttempts, 1);
});
});
});
}

/// A file system whose renames fail [failures] times, with [errorCode], before
/// working again.
class _FlakyRenameFileSystem extends pf.ForwardingFileSystem {
_FlakyRenameFileSystem(
super.delegate, {
required this.failures,
required this.errorCode,
});

int failures;
final int errorCode;
int renameAttempts = 0;

@override
pf.File file(dynamic path) => _FlakyRenameFile(this, delegate.file(path));
}

class _FlakyRenameFile extends pf.ForwardingFileSystemEntity<pf.File, File>
with pf.ForwardingFile {
_FlakyRenameFile(this.fileSystem, this.delegate);

@override
final _FlakyRenameFileSystem fileSystem;

@override
final pf.File delegate;

@override
Future<pf.File> rename(String newPath) async {
fileSystem.renameAttempts++;
if (fileSystem.failures > 0) {
fileSystem.failures--;
throw FileSystemException(
'Cannot rename file to \'$newPath\'',
path,
OSError('Refused by the test file system', fileSystem.errorCode),
);
}
return wrap(await delegate.rename(newPath));
}

@override
pf.File wrapFile(File delegate) =>
_FlakyRenameFile(fileSystem, delegate as pf.File);

@override
pf.Directory wrapDirectory(Directory delegate) => delegate as pf.Directory;

@override
pf.Link wrapLink(Link delegate) => delegate as pf.Link;
}

void expectIdInList(List<CacheObject> cacheObjects, int id) {
Expand Down