1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
//
// VICacheConfiguration.m
// VIMediaCacheDemo
//
// Created by Vito on 4/21/16.
// Copyright © 2016 Vito. All rights reserved.
//
#import "VICacheConfiguration.h"
#import "VICacheManager.h"
#import <MobileCoreServices/MobileCoreServices.h>
static NSString *kFileNameKey = @"kFileNameKey";
static NSString *kCacheFragmentsKey = @"kCacheFragmentsKey";
static NSString *kDownloadInfoKey = @"kDownloadInfoKey";
static NSString *kContentInfoKey = @"kContentInfoKey";
static NSString *kURLKey = @"kURLKey";
@interface VICacheConfiguration () <NSCoding>
@property (nonatomic, copy) NSString *filePath;
@property (nonatomic, copy) NSString *fileName;
@property (nonatomic, copy) NSArray<NSValue *> *internalCacheFragments;
@property (nonatomic, copy) NSArray *downloadInfo;
@end
@implementation VICacheConfiguration
+ (instancetype)configurationWithFilePath:(NSString *)filePath {
// filePath = [self configurationFilePathForFilePath:filePath];
VICacheConfiguration *configuration = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
if (!configuration) {
configuration = [[VICacheConfiguration alloc] init];
configuration.fileName = [filePath lastPathComponent];
}
configuration.filePath = filePath;
return configuration;
}
+ (NSString *)configurationFilePathForFilePath:(NSString *)filePath {
return [filePath stringByAppendingPathExtension:@"mt_cfg"];
}
- (NSArray<NSValue *> *)internalCacheFragments {
if (!_internalCacheFragments) {
_internalCacheFragments = [NSArray array];
}
return _internalCacheFragments;
}
- (NSArray *)downloadInfo {
if (!_downloadInfo) {
_downloadInfo = [NSArray array];
}
return _downloadInfo;
}
- (NSArray<NSValue *> *)cacheFragments {
return [_internalCacheFragments copy];
}
- (float)progress {
float progress = self.downloadedBytes / (float)self.contentInfo.contentLength;
return progress;
}
- (long long)downloadedBytes {
float bytes = 0;
@synchronized (self.internalCacheFragments) {
for (NSValue *range in self.internalCacheFragments) {
bytes += range.rangeValue.length;
}
}
return bytes;
}
- (float)downloadSpeed {
long long bytes = 0;
NSTimeInterval time = 0;
@synchronized (self.downloadInfo) {
for (NSArray *a in self.downloadInfo) {
bytes += [[a firstObject] longLongValue];
time += [[a lastObject] doubleValue];
}
}
return bytes / 1024.0 / time;
}
#pragma mark - NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.fileName forKey:kFileNameKey];
[aCoder encodeObject:self.internalCacheFragments forKey:kCacheFragmentsKey];
[aCoder encodeObject:self.downloadInfo forKey:kDownloadInfoKey];
[aCoder encodeObject:self.contentInfo forKey:kContentInfoKey];
[aCoder encodeObject:self.url forKey:kURLKey];
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
_fileName = [aDecoder decodeObjectForKey:kFileNameKey];
_internalCacheFragments = [[aDecoder decodeObjectForKey:kCacheFragmentsKey] mutableCopy];
if (!_internalCacheFragments) {
_internalCacheFragments = [NSArray array];
}
_downloadInfo = [aDecoder decodeObjectForKey:kDownloadInfoKey];
_contentInfo = [aDecoder decodeObjectForKey:kContentInfoKey];
_url = [aDecoder decodeObjectForKey:kURLKey];
}
return self;
}
#pragma mark - NSCopying
- (id)copyWithZone:(nullable NSZone *)zone {
VICacheConfiguration *configuration = [[VICacheConfiguration allocWithZone:zone] init];
configuration.fileName = self.fileName;
configuration.filePath = self.filePath;
configuration.internalCacheFragments = self.internalCacheFragments;
configuration.downloadInfo = self.downloadInfo;
configuration.url = self.url;
configuration.contentInfo = self.contentInfo;
return configuration;
}
#pragma mark - Update
- (void)save {
@synchronized (self.internalCacheFragments) {
[NSKeyedArchiver archiveRootObject:self toFile:self.filePath];
}
}
- (void)addCacheFragment:(NSRange)fragment {
if (fragment.location == NSNotFound || fragment.length == 0) {
return;
}
@synchronized (self.internalCacheFragments) {
NSMutableArray *internalCacheFragments = [self.internalCacheFragments mutableCopy];
NSValue *fragmentValue = [NSValue valueWithRange:fragment];
NSInteger count = self.internalCacheFragments.count;
if (count == 0) {
[internalCacheFragments addObject:fragmentValue];
} else {
NSMutableIndexSet *indexSet = [NSMutableIndexSet indexSet];
[internalCacheFragments enumerateObjectsUsingBlock:^(NSValue * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSRange range = obj.rangeValue;
if ((fragment.location + fragment.length) <= range.location) {
if (indexSet.count == 0) {
[indexSet addIndex:idx];
}
*stop = YES;
} else if (fragment.location <= (range.location + range.length) && (fragment.location + fragment.length) > range.location) {
[indexSet addIndex:idx];
} else if (fragment.location >= range.location + range.length) {
if (idx == count - 1) { // Append to last index
[indexSet addIndex:idx];
}
}
}];
if (indexSet.count > 1) {
NSRange firstRange = self.internalCacheFragments[indexSet.firstIndex].rangeValue;
NSRange lastRange = self.internalCacheFragments[indexSet.lastIndex].rangeValue;
NSInteger location = MIN(firstRange.location, fragment.location);
NSInteger endOffset = MAX(lastRange.location + lastRange.length, fragment.location + fragment.length);
NSRange combineRange = NSMakeRange(location, endOffset - location);
[internalCacheFragments removeObjectsAtIndexes:indexSet];
[internalCacheFragments insertObject:[NSValue valueWithRange:combineRange] atIndex:indexSet.firstIndex];
} else if (indexSet.count == 1) {
NSRange firstRange = self.internalCacheFragments[indexSet.firstIndex].rangeValue;
NSRange expandFirstRange = NSMakeRange(firstRange.location, firstRange.length + 1);
NSRange expandFragmentRange = NSMakeRange(fragment.location, fragment.length + 1);
NSRange intersectionRange = NSIntersectionRange(expandFirstRange, expandFragmentRange);
if (intersectionRange.length > 0) { // Should combine
NSInteger location = MIN(firstRange.location, fragment.location);
NSInteger endOffset = MAX(firstRange.location + firstRange.length, fragment.location + fragment.length);
NSRange combineRange = NSMakeRange(location, endOffset - location);
[internalCacheFragments removeObjectAtIndex:indexSet.firstIndex];
[internalCacheFragments insertObject:[NSValue valueWithRange:combineRange] atIndex:indexSet.firstIndex];
} else {
if (firstRange.location > fragment.location) {
[internalCacheFragments insertObject:fragmentValue atIndex:[indexSet lastIndex]];
} else {
[internalCacheFragments insertObject:fragmentValue atIndex:[indexSet lastIndex] + 1];
}
}
}
}
self.internalCacheFragments = [internalCacheFragments copy];
}
}
- (void)addDownloadedBytes:(long long)bytes spent:(NSTimeInterval)time {
@synchronized (self.downloadInfo) {
self.downloadInfo = [self.downloadInfo arrayByAddingObject:@[@(bytes), @(time)]];
}
}
@end
@implementation VICacheConfiguration (VIConvenient)
+ (BOOL)createAndSaveDownloadedConfigurationForURL:(NSURL *)url error:(NSError **)error {
NSString *filePath = [VICacheManager cachedFilePathForURL:url];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSDictionary<NSFileAttributeKey, id> *attributes = [fileManager attributesOfItemAtPath:filePath error:error];
if (!attributes) {
return NO;
}
long long fileSize = attributes.fileSize;
NSRange range = NSMakeRange(0, fileSize);
VICacheConfiguration *configuration = [VICacheConfiguration configurationWithFilePath:filePath];
configuration.url = url;
VIContentInfo *contentInfo = [VIContentInfo new];
NSString *fileExtension = [url pathExtension];
NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtension, NULL);
NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType);
if (!contentType) {
contentType = @"application/octet-stream";
}
contentInfo.contentType = contentType;
contentInfo.contentLength = fileSize;
contentInfo.byteRangeAccessSupported = YES;
contentInfo.downloadedContentLength = fileSize;
configuration.contentInfo = contentInfo;
[configuration addCacheFragment:range];
[configuration save];
return YES;
}
@end