| // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:meta/meta.dart'; |
| import 'package:path/path.dart'; |
| |
| import 'byte_store.dart'; |
| import 'cache.dart'; |
| import 'file_byte_store.dart'; |
| |
| /// The function that returns current time in milliseconds. |
| typedef int GetCurrentTime(); |
| |
| /// [ByteStore] that stores values as files, allows to mark some of keys as |
| /// temporary protected, and supports periodical [flush] of unprotected keys. |
| /// |
| /// The set of protected keys is stored in a file, which is locked during |
| /// updating to prevent races across multiple processes. |
| class ProtectedFileByteStore implements ByteStore { |
| @visibleForTesting |
| static const PROTECTED_FILE_NAME = '.temporary_protected_keys'; |
| |
| final String _cachePath; |
| final Duration _protectionDuration; |
| final GetCurrentTime _getCurrentTimeFunction; |
| |
| final FileByteStore _fileByteStore; |
| final Cache<String, List<int>> _cache; |
| |
| /// Create a new instance of the [ProtectedFileByteStore]. |
| /// |
| /// The [protectionDuration] specifies how long temporary protected keys |
| /// stay protected. |
| ProtectedFileByteStore(this._cachePath, |
| {Duration protectionDuration, |
| GetCurrentTime getCurrentTime, |
| int cacheSizeBytes: 128 * 1024 * 1024}) |
| : _protectionDuration = protectionDuration, |
| _getCurrentTimeFunction = getCurrentTime ?? _getCurrentTimeDefault, |
| _fileByteStore = new FileByteStore(_cachePath), |
| _cache = new Cache(cacheSizeBytes, (bytes) => bytes.length); |
| |
| /// Remove all not protected keys. |
| void flush() { |
| var protectedKeysText = _keysReadTextLocked(); |
| var protectedKeys = new ProtectedKeys.decode(protectedKeysText); |
| List<FileSystemEntity> files = new Directory(_cachePath).listSync(); |
| for (var file in files) { |
| if (file is File) { |
| String key = basename(file.path); |
| if (key == PROTECTED_FILE_NAME) { |
| continue; |
| } |
| if (protectedKeys.containsKey(key)) { |
| continue; |
| } |
| try { |
| file.deleteSync(); |
| } catch (e) {} |
| } |
| } |
| } |
| |
| @override |
| List<int> get(String key) { |
| return _cache.get(key, () => _fileByteStore.get(key)); |
| } |
| |
| @override |
| void put(String key, List<int> bytes) { |
| if (key == PROTECTED_FILE_NAME) { |
| throw new ArgumentError('The key $key is reserved.'); |
| } |
| _fileByteStore.put(key, bytes); |
| _cache.put(key, bytes); |
| } |
| |
| /// The [add] keys are added to the set of temporary protected keys, and |
| /// their age is reset to zero. |
| /// |
| /// The [remove] keys are removed from the set of temporary protected keys, |
| /// and become subjects of LRU cached eviction. |
| void updateProtectedKeys( |
| {List<String> add: const <String>[], |
| List<String> remove: const <String>[]}) { |
| _withProtectedKeysLockSync(_cachePath, (ProtectedKeys protectedKeys) { |
| var now = _getCurrentTimeFunction(); |
| |
| if (_protectionDuration != null) { |
| var maxAge = _protectionDuration.inMilliseconds; |
| protectedKeys.removeOlderThan(maxAge, now); |
| } |
| |
| for (var addedKey in add) { |
| protectedKeys.add(addedKey, now); |
| } |
| |
| for (var removedKey in remove) { |
| protectedKeys.remove(removedKey); |
| } |
| }); |
| } |
| |
| /// Read the protected keys, but don't keep the lock. |
| /// |
| /// We do this before performing any long running operation that |
| /// just read, and where it is important to keep system unlocked. |
| String _keysReadTextLocked() { |
| File keysFile = new File(join(_cachePath, PROTECTED_FILE_NAME)); |
| RandomAccessFile keysLock = keysFile.openSync(mode: FileMode.APPEND); |
| keysLock.lockSync(FileLock.BLOCKING_EXCLUSIVE); |
| try { |
| return _keysReadText(keysLock); |
| } finally { |
| keysLock.unlockSync(); |
| keysLock.closeSync(); |
| } |
| } |
| |
| /// The default implementation of [GetCurrentTime]. |
| static int _getCurrentTimeDefault() { |
| return new DateTime.now().millisecondsSinceEpoch; |
| } |
| |
| static ProtectedKeys _keysRead(RandomAccessFile file) { |
| String text = _keysReadText(file); |
| return new ProtectedKeys.decode(text); |
| } |
| |
| static String _keysReadText(RandomAccessFile file) { |
| file.setPositionSync(0); |
| List<int> bytes = file.readSync(file.lengthSync()); |
| return utf8.decode(bytes); |
| } |
| |
| static void _keysWrite(RandomAccessFile file, ProtectedKeys keys) { |
| String text = keys.encode(); |
| file.setPositionSync(0); |
| file.writeStringSync(text); |
| file.truncateSync(file.positionSync()); |
| } |
| |
| /// Perform [f] over the locked keys file, decoded into [ProtectedKeys]. |
| static void _withProtectedKeysLockSync( |
| String cachePath, void f(ProtectedKeys keys)) { |
| String path = join(cachePath, PROTECTED_FILE_NAME); |
| RandomAccessFile file = new File(path).openSync(mode: FileMode.APPEND); |
| file.lockSync(FileLock.BLOCKING_EXCLUSIVE); |
| try { |
| ProtectedKeys keys = _keysRead(file); |
| f(keys); |
| _keysWrite(file, keys); |
| } finally { |
| file.unlockSync(); |
| file.closeSync(); |
| } |
| } |
| } |
| |
| /// Container with protected keys. |
| @visibleForTesting |
| class ProtectedKeys { |
| /// The map from a key in [ByteStore] to the time in milliseconds when the |
| /// key was marked as temporary protected. |
| final Map<String, int> map; |
| |
| ProtectedKeys(this.map); |
| |
| factory ProtectedKeys.decode(String text) { |
| var map = <String, int>{}; |
| try { |
| List<String> lines = text.split('\n').toList(); |
| if (lines.length % 2 == 0) { |
| for (int i = 0; i < lines.length; i += 2) { |
| String key = lines[i]; |
| String startMillisecondsStr = lines[i + 1]; |
| int startMilliseconds = int.parse(startMillisecondsStr); |
| map[key] = startMilliseconds; |
| } |
| } |
| } catch (e) {} |
| return new ProtectedKeys(map); |
| } |
| |
| /// Add the given [key] with the current time. |
| void add(String key, int time) { |
| map[key] = time; |
| } |
| |
| bool containsKey(String key) => map.containsKey(key); |
| |
| String encode() { |
| var buffer = new StringBuffer(); |
| map.forEach((key, start) { |
| buffer.writeln(key); |
| buffer.writeln(start); |
| }); |
| return buffer.toString().trim(); |
| } |
| |
| void remove(String key) { |
| map.remove(key); |
| } |
| |
| /// If the time is [now] milliseconds, remove all keys that are older than |
| /// the given [maxAge] is milliseconds. |
| void removeOlderThan(int maxAge, int now) { |
| var keysToRemove = <String>[]; |
| for (var key in map.keys) { |
| if (now - map[key] > maxAge) { |
| keysToRemove.add(key); |
| } |
| } |
| keysToRemove.forEach(map.remove); |
| } |
| } |