浏览代码

add local disk cache

/main
gewentao 6 年前
当前提交
bda4734a
共有 14 个文件被更改,包括 2597 次插入1 次删除
  1. 13
      Assets/UIWidgets/painting/image_provider.cs
  2. 3
      Assets/UIWidgets/lib.meta
  3. 1001
      Assets/Plugins/Mono.Data.Sqlite.dll
  4. 30
      Assets/Plugins/Mono.Data.Sqlite.dll.meta
  5. 1001
      Assets/Plugins/System.Data.dll
  6. 30
      Assets/Plugins/System.Data.dll.meta
  7. 3
      Assets/UIWidgets/lib/cache_manager.meta
  8. 347
      Assets/UIWidgets/lib/cache_manager/cache_manager.cs
  9. 3
      Assets/UIWidgets/lib/cache_manager/cache_manager.cs.meta
  10. 156
      Assets/UIWidgets/lib/cache_manager/cache_meta.cs
  11. 11
      Assets/UIWidgets/lib/cache_manager/cache_meta.cs.meta

13
Assets/UIWidgets/painting/image_provider.cs


using System.Net;
using System;
using System.IO;
using UIWidgets.lib.cache_manager;
using UIWidgets.ui;
using UnityEngine;

}
public override ImageStreamCompleter load(NetworkImage key) {
return new OneFrameImageStreamCompleter(_loadAsync(key));
// return new OneFrameImageStreamCompleter(_loadAsync(key));
return new OneFrameImageStreamCompleter(_loadAsyncWithCache(key));
}
public static IPromise<ImageInfo> _loadAsync(NetworkImage key) {

}
return promise; // Return the promise so the caller can await resolution (or error).
}
public static IPromise<ImageInfo> _loadAsyncWithCache(NetworkImage key) {
var cache = CacheManager.getInstance();
var result = cache.getMeta(key.url).
Then(meta => cache.downloadFileIfNeeded(meta)).
Then(meta => cache.updateMeta(meta)).
Then(path => cache.loadCacheFile(path));
return result;
}
public override string ToString() {

3
Assets/UIWidgets/lib.meta


fileFormatVersion: 2
guid: d5b7bf0751a64454ba399aad57ab71fc
timeCreated: 1535510135

1001
Assets/Plugins/Mono.Data.Sqlite.dll
文件差异内容过多而无法显示
查看文件

30
Assets/Plugins/Mono.Data.Sqlite.dll.meta


fileFormatVersion: 2
guid: 3e54fe1804cc94b16a015d1a2d114b55
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
isPreloaded: 0
isOverridable: 0
platformData:
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

1001
Assets/Plugins/System.Data.dll
文件差异内容过多而无法显示
查看文件

30
Assets/Plugins/System.Data.dll.meta


fileFormatVersion: 2
guid: acabc02e8ca194aae8d9d3c268ffe747
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
isPreloaded: 0
isOverridable: 0
platformData:
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

3
Assets/UIWidgets/lib/cache_manager.meta


fileFormatVersion: 2
guid: 46a1a2081f1e4842a8ce89f2f0e15234
timeCreated: 1535510181

347
Assets/UIWidgets/lib/cache_manager/cache_manager.cs


using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using RSG;
using System.Text;
using System.IO;
using System.Linq;
using UnityEngine;
using System.Net;
using Mono.Data.Sqlite;
using UIWidgets.painting;
namespace UIWidgets.lib.cache_manager {
public class CacheManager {
private readonly string _keyCacheData = "lib_cached_image_data";
private readonly string _keyCacheCleanDate = "lib_cached_image_data_last_clean";
private string _dbUri;
private const string DbFileName = @"ui_widgets_cache.db";
private static TimeSpan inBetweenCleans = new TimeSpan(7, 0, 0, 0);
private static TimeSpan maxAgeCacheObject = new TimeSpan(30, 0, 0, 0);
private static int maxNrOfCacheObjects = 2; // configurable ?
private static CacheManager _instance;
// public DateTime lastCacheClean;
private bool _isStoringData = false;
private bool _shouldStoreDataAgain = false;
public static CacheManager getInstance() {
if (_instance == null) {
_instance = new CacheManager();
_instance._init();
}
return _instance;
}
private void _init() {
_setupDatabase();
// _getLastCleanTimestampFromPreferences();
}
private void _setupDatabase() {
var directoryPath = Application.persistentDataPath;
var _dbFilePath = Path.Combine(directoryPath, DbFileName);
_dbUri = "URI=file:" + _dbFilePath;
if (!Directory.Exists(directoryPath)) {
Directory.CreateDirectory(directoryPath);
}
if (!File.Exists(_dbFilePath)) {
SqliteConnection.CreateFile(_dbFilePath);
}
using (var connection = new SqliteConnection(_dbUri)) {
connection.Open();
const string createCacheTable = @"CREATE TABLE IF NOT EXISTS Cache (
Key TEXT NOT NULL PRIMARY KEY,
FilePath TEXT NOT NULL,
ETag TEXT,
Url TEXT NOT NULL,
Touched REAL,
ValidTill REAL
)";
using (var command = new SqliteCommand(createCacheTable, connection)) {
command.ExecuteNonQuery();
}
}
}
public IPromise<CacheMeta> getMeta(string url) {
var key = generateHashKey(url);
CacheMeta meta = null;
using (var connection = new SqliteConnection(_dbUri)) {
connection.Open();
const string metaQuery = @"SELECT Key, FilePath, ETag, Url, Touched, ValidTill FROM Cache
WHERE Key = @Key";
using (var command = new SqliteCommand(metaQuery, connection)) {
command.Parameters.AddWithValue("@Key", key);
using (var reader = command.ExecuteReader()) {
if (reader.HasRows && reader.Read()) {
meta = new CacheMeta.Builder(reader.GetString(0))
.RelativePath(reader.GetString(1))
.ETag(reader.IsDBNull(2) ? string.Empty : reader.GetString(2))
.Url(reader.GetString(3))
.Touched(reader.GetDouble(4))
.ValidTill(reader.GetDouble(5))
.Build();
}
}
}
}
var promise = new Promise<CacheMeta>();
if (meta == null) {
meta = new CacheMeta(url);
}
meta.touch();
promise.Resolve(meta);
return promise;
}
public IPromise<CacheMeta> downloadFileIfNeeded(CacheMeta meta) {
var promise = new Promise<CacheMeta>(); // Create promise.
var filepath = meta.getFilePath();
var fileExpire = meta.validTill == 0.0 ||
CacheMeta.fromMillisecondsSinceEpoch(meta.validTill) < DateTime.Now;
if (filepath == null ||
fileExpire ||
!File.Exists(filepath)) {
// download from url
WebRequest webRequest = WebRequest.Create(new Uri(meta.url));
if (fileExpire && meta.eTag != null) {
webRequest.Headers.Set("If-None-Match", meta.eTag);
}
webRequest.BeginGetResponse(result => {
const int BufferSize = 1024;
var bytes = new byte[BufferSize];
var response = webRequest.EndGetResponse(result);
var statusCode = (int) ((HttpWebResponse) response).StatusCode;
var respHeaders = response.Headers;
var headerDict = new Dictionary<string, string>();
for (int i = 0; i < respHeaders.Count; i++) {
string header = respHeaders.GetKey(i);
string value = respHeaders.Get(header);
headerDict[header] = respHeaders[value];
}
if (statusCode == 200) {
meta.setDataFromHeaders(headerDict);
var stream = response.GetResponseStream();
if (stream != null) {
var localStream = File.Create(meta.getFilePath());
int bytesRead;
while ((bytesRead = stream.Read(bytes, 0, BufferSize)) > 0) {
localStream.Write(bytes, 0, bytesRead);
}
stream.Close();
localStream.Close();
promise.Resolve(meta);
}
} else if (statusCode == 304) {
meta.setDataFromHeaders(headerDict);
promise.Resolve(meta);
}
}, null);
}
else {
promise.Resolve(meta);
}
return promise;
}
public IPromise<string> updateMeta(CacheMeta newMeta) {
var key = generateHashKey(newMeta.url);
const string checkMetaQuery = @"SELECT COUNT(*) FROM Cache WHERE
Key = @Key";
bool recordFound = false;
using (var connection = new SqliteConnection(_dbUri)) {
connection.Open();
using (var command = new SqliteCommand(checkMetaQuery, connection)) {
command.Parameters.AddWithValue("@Key", key);
using (var reader = command.ExecuteReader()) {
if (reader.Read()) {
recordFound = reader.GetInt32(0) > 0;
}
}
}
if (recordFound) {
const string updateCacheQuery =
@"UPDATE Cache SET FilePath = @FilePath, ETag = @ETag,
Url = @Url, Touched = @Touched, ValidTill = @ValidTill
WHERE Key = @Key";
using (var command = new SqliteCommand(updateCacheQuery, connection)) {
command.Parameters.AddWithValue("@FilePath", newMeta.relativePath);
command.Parameters.AddWithValue("@ETag", newMeta.eTag);
command.Parameters.AddWithValue("@Url", newMeta.url);
command.Parameters.AddWithValue("@Touched", newMeta.touched);
command.Parameters.AddWithValue("@ValidTill", newMeta.validTill);
command.Parameters.AddWithValue("@key", key);
command.ExecuteNonQuery();
}
}
else {
const string insertQuery =
@"INSERT INTO Cache (Key, FilePath, ETag, Url, Touched, ValidTill)
VALUES (@Key, @FilePath, @ETag, @Url, @Touched, @ValidTill)";
using (var command = new SqliteCommand(insertQuery, connection)) {
command.Parameters.AddWithValue("@Key", key);
command.Parameters.AddWithValue("@FilePath", newMeta.relativePath);
command.Parameters.AddWithValue("@ETag", newMeta.eTag);
command.Parameters.AddWithValue("@Url", newMeta.url);
command.Parameters.AddWithValue("@Touched", newMeta.touched);
command.Parameters.AddWithValue("@ValidTill", newMeta.validTill);
command.ExecuteNonQuery();
}
}
}
// _removeOldObjectsFromCache(); // do we need this?
_shrinkLargeCache();
var promise = new Promise<string>();
promise.Resolve(newMeta.getFilePath());
return promise;
}
public IPromise<ImageInfo> loadCacheFile(string path) {
var promise = new Promise<ImageInfo>();
var bytes = File.ReadAllBytes(path);
var imageInfo = new ImageInfo(new ui.Image(
bytes
));
promise.Resolve(imageInfo);
return promise;
}
private void _removeOldObjectsFromCache() {
var oldestDataAllowed = DateTime.Now - maxAgeCacheObject;
var metas = new List<CacheMeta>();
const string query = @"SELECT Key, FilePath, ETag, Url, Touched, ValidTill
FROM Cache
WHERE Touched < @OldestTouchedAllowed";
const string deleteQuery = @"DELETE FROM Cache WHERE Key in (@KeyList)";
using (var connection = new SqliteConnection(_dbUri)) {
connection.Open();
using (var command = new SqliteCommand(query, connection)) {
command.Parameters.AddWithValue("@OldestTouchedAllowed",
CacheMeta.millisecondsSinceEpoch(oldestDataAllowed));
using (var reader = command.ExecuteReader()) {
while (reader.HasRows && reader.Read()) {
metas.Add(
new CacheMeta.Builder(reader.GetString(0))
.RelativePath(reader.GetString(1))
.ETag(reader.IsDBNull(2) ? string.Empty : reader.GetString(2))
.Url(reader.GetString(3))
.Touched(reader.GetDouble(4))
.ValidTill(reader.GetDouble(5))
.Build()
);
}
}
}
foreach (var meta in metas) {
File.Delete(meta.getFilePath());
}
using (var command = new SqliteCommand(deleteQuery, connection)) {
command.Parameters.AddWithValue("@KeyList",
string.Join(",", metas.Select(m => m.key.ToString()).ToArray()));
command.ExecuteNonQuery();
}
}
}
private void _shrinkLargeCache() {
const string countQuery = @"SELECT COUNT(*) FROM Cache";
var totalRecord = 0;
using (var connection = new SqliteConnection(_dbUri)) {
connection.Open();
using (var command = new SqliteCommand(countQuery, connection)) {
using (var reader = command.ExecuteReader()) {
if (reader.Read()) {
totalRecord = reader.GetInt32(0);
}
}
}
if (totalRecord > maxNrOfCacheObjects) {
var metas = new List<CacheMeta>();
var overflow = totalRecord - maxNrOfCacheObjects;
const string overflowQuery = @"SELECT Key, FilePath, ETag, Url, Touched, ValidTill
FROM Cache ORDER BY Touched LIMIT @Overflow";
using (var command = new SqliteCommand(overflowQuery, connection)) {
command.Parameters.AddWithValue("@Overflow", overflow);
using (var reader = command.ExecuteReader()) {
while (reader.HasRows && reader.Read()) {
metas.Add(
new CacheMeta.Builder(reader.GetString(0))
.RelativePath(reader.GetString(1))
.ETag(reader.IsDBNull(2) ? string.Empty : reader.GetString(2))
.Url(reader.GetString(3))
.Touched(reader.GetDouble(4))
.ValidTill(reader.GetDouble(5))
.Build()
);
}
}
}
foreach (var meta in metas) {
File.Delete(meta.getFilePath());
}
const string deleteQuery = @"DELETE FROM Cache WHERE Key in (@KeyList)";
using (var command = new SqliteCommand(deleteQuery, connection)) {
command.Parameters.AddWithValue("@KeyList",
string.Join(",", metas.Select(m => m.key.ToString()).ToArray()));
command.ExecuteNonQuery();
}
}
}
}
private static string generateHashKey(string url) {
using (var md5 = MD5.Create()) {
byte[] inputBytes = Encoding.ASCII.GetBytes(url);
byte[] hashBytes = md5.ComputeHash(inputBytes);
// Convert the byte array to hexadecimal string
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hashBytes.Length; i++) {
sb.Append(hashBytes[i].ToString("X2"));
}
return sb.ToString();
}
}
}
}

3
Assets/UIWidgets/lib/cache_manager/cache_manager.cs.meta


fileFormatVersion: 2
guid: 32cd17d74783499d97a953247420a1b3
timeCreated: 1535510219

156
Assets/UIWidgets/lib/cache_manager/cache_meta.cs


using System.Collections.Generic;
using System;
using System.IO;
using UnityEngine;
namespace UIWidgets.lib.cache_manager {
public class CacheMeta {
private static readonly string _directory = Application.persistentDataPath;
public string relativePath = null;
public string eTag = null;
public double touched;
public double validTill;
public string url;
public string key;
public CacheMeta(string url) {
this.url = url;
touch();
}
private CacheMeta(Builder builder) {
key = builder.key;
relativePath = builder.relativePath;
eTag = builder.eTag;
touched = builder.touched;
validTill = builder.validTill;
url = builder.url;
}
public string getFilePath() {
if (this.relativePath == null) {
return null;
}
return _directory + this.relativePath;
}
public void setRelativePath(string path) {
this.relativePath = path;
}
public void touch() {
this.touched = millisecondsSinceEpoch(DateTime.Now);
}
public void setDataFromHeaders(Dictionary<string, string> headers) {
var ageDuration = new TimeSpan(7, 0, 0, 0); // 7 days
if (headers.ContainsKey("cache-control")) {
var cacheControl = headers["cache-control"];
string[] stringSeparators = {", "};
var controlSettings = cacheControl.Split(stringSeparators, StringSplitOptions.None);
foreach (var controlSetting in controlSettings) {
if (controlSetting.StartsWith("max-age=")) {
int validSeconds = 0;
if (int.TryParse(controlSetting.Split('=')[1], out validSeconds)) {
if (validSeconds > 0) {
ageDuration = new TimeSpan(0, 0, validSeconds);
}
}
}
}
}
validTill = millisecondsSinceEpoch(DateTime.Now + ageDuration);
if (headers.ContainsKey("etag")) {
eTag = headers["etag"];
}
var fileExtension = "";
if (headers.ContainsKey("content-type")) {
var type = headers["content-type"].Split('/');
if (type.Length == 2) {
fileExtension = string.Format(".{0}", type[1]);
}
}
var oldPath = getFilePath();
if (oldPath != null && !oldPath.EndsWith(fileExtension)) {
removeOldFile(oldPath);
relativePath = null;
}
if (relativePath == null) {
var fileName = string.Format("/cache_{0}{1}", Guid.NewGuid(), fileExtension);
relativePath = fileName;
}
}
private static void removeOldFile(string filePath) {
if (File.Exists(filePath)) {
File.Delete(filePath);
}
}
public static double millisecondsSinceEpoch(DateTime time) {
return (time - new DateTime(1970, 1, 1)).TotalMilliseconds;
}
public static DateTime fromMillisecondsSinceEpoch(double ms) {
return new DateTime(1970, 1, 1).AddMilliseconds(ms);
}
public sealed class Builder {
internal string key { get; private set; }
internal string relativePath { get; private set; }
internal string eTag { get; private set; }
internal double touched { get; private set; }
internal double validTill { get; private set; }
internal string url { get; private set; }
public Builder(string key) {
if (string.IsNullOrEmpty(key)) {
throw new ArgumentException("key can't be empty", "key");
}
this.key = key;
}
public Builder RelativePath(string relativePath) {
this.relativePath = relativePath;
return this;
}
public Builder ETag(string eTag) {
this.eTag = eTag;
return this;
}
public Builder Touched(double touched) {
this.touched = touched;
return this;
}
public Builder ValidTill(double validTill) {
this.validTill = validTill;
return this;
}
public Builder Url(string url) {
this.url = url;
return this;
}
public CacheMeta Build()
{
return new CacheMeta(this);
}
}
}
}

11
Assets/UIWidgets/lib/cache_manager/cache_meta.cs.meta


fileFormatVersion: 2
guid: d54df7319b8ca49cf9aba74db3698508
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
正在加载...
取消
保存