VLCHTTPConnection.m 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. /*****************************************************************************
  2. * VLCHTTPConnection.m
  3. * VLC for iOS
  4. *****************************************************************************
  5. * Copyright (c) 2013 VideoLAN. All rights reserved.
  6. * $Id$
  7. *
  8. * Authors: Felix Paul Kühne <fkuehne # videolan.org>
  9. * Jean-Baptiste Kempf <jb # videolan.org>
  10. *
  11. * Refer to the COPYING file of the official project for license.
  12. *****************************************************************************/
  13. #import "VLCAppDelegate.h"
  14. #import "VLCHTTPConnection.h"
  15. #import "HTTPConnection.h"
  16. #import "MultipartFormDataParser.h"
  17. #import "HTTPMessage.h"
  18. #import "HTTPDataResponse.h"
  19. #import "HTTPFileResponse.h"
  20. #import "MultipartMessageHeaderField.h"
  21. #import "VLCHTTPUploaderController.h"
  22. #import "HTTPDynamicFileResponse.h"
  23. #import "VLCThumbnailsCache.h"
  24. @interface VLCHTTPConnection()
  25. {
  26. MultipartFormDataParser *_parser;
  27. NSFileHandle *_storeFile;
  28. NSString *_filepath;
  29. UInt64 _contentLength;
  30. UInt64 _receivedContent;
  31. }
  32. @end
  33. @implementation VLCHTTPConnection
  34. - (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path
  35. {
  36. // Add support for POST
  37. if ([method isEqualToString:@"POST"]) {
  38. if ([path isEqualToString:@"/upload.json"])
  39. return YES;
  40. }
  41. return [super supportsMethod:method atPath:path];
  42. }
  43. - (BOOL)expectsRequestBodyFromMethod:(NSString *)method atPath:(NSString *)path
  44. {
  45. // Inform HTTP server that we expect a body to accompany a POST request
  46. if ([method isEqualToString:@"POST"] && [path isEqualToString:@"/upload.json"]) {
  47. // here we need to make sure, boundary is set in header
  48. NSString* contentType = [request headerField:@"Content-Type"];
  49. NSUInteger paramsSeparator = [contentType rangeOfString:@";"].location;
  50. if (NSNotFound == paramsSeparator)
  51. return NO;
  52. if (paramsSeparator >= contentType.length - 1)
  53. return NO;
  54. NSString* type = [contentType substringToIndex:paramsSeparator];
  55. if (![type isEqualToString:@"multipart/form-data"]) {
  56. // we expect multipart/form-data content type
  57. return NO;
  58. }
  59. // enumerate all params in content-type, and find boundary there
  60. NSArray* params = [[contentType substringFromIndex:paramsSeparator + 1] componentsSeparatedByString:@";"];
  61. for (NSString* param in params) {
  62. paramsSeparator = [param rangeOfString:@"="].location;
  63. if ((NSNotFound == paramsSeparator) || paramsSeparator >= param.length - 1)
  64. continue;
  65. NSString* paramName = [param substringWithRange:NSMakeRange(1, paramsSeparator-1)];
  66. NSString* paramValue = [param substringFromIndex:paramsSeparator+1];
  67. if ([paramName isEqualToString: @"boundary"])
  68. // let's separate the boundary from content-type, to make it more handy to handle
  69. [request setHeaderField:@"boundary" value:paramValue];
  70. }
  71. // check if boundary specified
  72. if (nil == [request headerField:@"boundary"])
  73. return NO;
  74. return YES;
  75. }
  76. return [super expectsRequestBodyFromMethod:method atPath:path];
  77. }
  78. - (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path
  79. {
  80. if ([method isEqualToString:@"POST"] && [path isEqualToString:@"/upload.json"]) {
  81. return [[HTTPDataResponse alloc] initWithData:[@"\"OK\"" dataUsingEncoding:NSUTF8StringEncoding]];
  82. }
  83. if ([path hasPrefix:@"/download/"]) {
  84. NSString *filePath = [[path stringByReplacingOccurrencesOfString:@"/download/" withString:@""]stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
  85. HTTPFileResponse *fileResponse = [[HTTPFileResponse alloc] initWithFilePath:filePath forConnection:self];
  86. fileResponse.contentType = @"application/octet-stream";
  87. return fileResponse;
  88. }
  89. if ([path hasPrefix:@"/thumbnail"]) {
  90. NSString *filePath = [[path stringByReplacingOccurrencesOfString:@"/thumbnail/" withString:@""]stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
  91. filePath = [filePath stringByReplacingOccurrencesOfString:@".png" withString:@""];
  92. NSManagedObjectContext *moc = [[MLMediaLibrary sharedMediaLibrary] managedObjectContext];
  93. NSPersistentStoreCoordinator *psc = [moc persistentStoreCoordinator];
  94. NSManagedObject *mo = [moc existingObjectWithID:[psc managedObjectIDForURIRepresentation:[NSURL URLWithString:filePath]] error:nil];
  95. NSData *theData;
  96. if ([mo isKindOfClass:[MLFile class]])
  97. theData = UIImagePNGRepresentation([VLCThumbnailsCache thumbnailForMediaFile:(MLFile *)mo]);
  98. else if ([mo isKindOfClass:[MLShow class]])
  99. theData = UIImagePNGRepresentation([VLCThumbnailsCache thumbnailForShow:(MLShow *)mo]);
  100. else if ([mo isKindOfClass:[MLLabel class]])
  101. theData = UIImagePNGRepresentation([VLCThumbnailsCache thumbnailForLabel:(MLLabel *)mo]);
  102. else if ([mo isKindOfClass:[MLAlbum class]])
  103. theData = UIImagePNGRepresentation([VLCThumbnailsCache thumbnailForMediaFile:[[(MLAlbum *)mo tracks].anyObject files].anyObject]);
  104. else if ([mo isKindOfClass:[MLAlbumTrack class]])
  105. theData = UIImagePNGRepresentation([VLCThumbnailsCache thumbnailForMediaFile:[(MLAlbumTrack *)mo files].anyObject]);
  106. else if ([mo isKindOfClass:[MLShowEpisode class]])
  107. theData = UIImagePNGRepresentation([VLCThumbnailsCache thumbnailForMediaFile:[(MLShowEpisode *)mo files].anyObject]);
  108. if (theData) {
  109. HTTPDataResponse *dataResponse = [[HTTPDataResponse alloc] initWithData:theData];
  110. dataResponse.contentType = @"image/png";
  111. return dataResponse;
  112. }
  113. }
  114. NSString *filePath = [self filePathForURI:path];
  115. NSString *documentRoot = [config documentRoot];
  116. NSString *relativePath = [filePath substringFromIndex:[documentRoot length]];
  117. if ([relativePath isEqualToString:@"/index.html"]) {
  118. NSMutableArray *allMedia = [[NSMutableArray alloc] init];
  119. /* add all albums */
  120. NSArray *allAlbums = [MLAlbum allAlbums];
  121. for (MLAlbum *album in allAlbums) {
  122. if (album.name.length > 0 && album.tracks.count > 1)
  123. [allMedia addObject:album];
  124. }
  125. /* add all shows */
  126. NSArray *allShows = [MLShow allShows];
  127. for (MLShow *show in allShows) {
  128. if (show.name.length > 0 && show.episodes.count > 1)
  129. [allMedia addObject:show];
  130. }
  131. /* add all folders*/
  132. NSArray *allFolders = [MLLabel allLabels];
  133. for (MLLabel *folder in allFolders)
  134. [allMedia addObject:folder];
  135. /* add all remaining files */
  136. NSArray *allFiles = [MLFile allFiles];
  137. for (MLFile *file in allFiles) {
  138. if (file.labels.count > 0) continue;
  139. if (!file.isShowEpisode && !file.isAlbumTrack)
  140. [allMedia addObject:file];
  141. else if (file.isShowEpisode) {
  142. if (file.showEpisode.show.episodes.count < 2)
  143. [allMedia addObject:file];
  144. } else if (file.isAlbumTrack) {
  145. if (file.albumTrack.album.tracks.count < 2)
  146. [allMedia addObject:file];
  147. }
  148. }
  149. NSMutableArray *mediaInHtml = [[NSMutableArray alloc] initWithCapacity:allMedia.count];
  150. for (NSManagedObject *mo in allMedia) {
  151. if ([mo isKindOfClass:[MLFile class]])
  152. [mediaInHtml addObject:[NSString stringWithFormat:
  153. @"<div style=\"background-image:url('thumbnail/%@.png')\"> \
  154. <a href=\"%@\" class=\"inner\"> \
  155. <div class=\"down icon\"></div> \
  156. <div class=\"infos\"> \
  157. <span class=\"first-line\">%@</span> \
  158. <span class=\"second-line\">12:34:56 - 123MB</span> \
  159. </div> \
  160. </a> \
  161. </div>",
  162. mo.objectID.URIRepresentation,
  163. [[(MLFile *)mo url] stringByReplacingOccurrencesOfString:@"file://"withString:@""],
  164. [(MLFile *)mo title]]];
  165. else if ([mo isKindOfClass:[MLShow class]]) {
  166. NSArray *episodes = [(MLShow *)mo sortedEpisodes];
  167. [mediaInHtml addObject:[NSString stringWithFormat:
  168. @"<div style=\"background-image:url('thumbnail/%@.png')\"> \
  169. <a href=\"#\" class=\"inner\"> \
  170. <div class=\"open icon\"></div> \
  171. <div class=\"infos\"> \
  172. <span class=\"first-line\">%@</span> \
  173. <span class=\"second-line\">42 items</span> \
  174. </div> \
  175. </a> \
  176. <div class=\"content\">",
  177. mo.objectID.URIRepresentation,
  178. [(MLShow *)mo name]]];
  179. for (MLShowEpisode *showEp in episodes)
  180. [mediaInHtml addObject:[NSString stringWithFormat:
  181. @"<div style=\"background-image:url('thumbnail/%@.png')\"> \
  182. <a href=\"%@\" class=\"inner\"> \
  183. <div class=\"down icon\"></div> \
  184. <div class=\"infos\"> \
  185. <span class=\"first-line\">S%@E%@ - %@</span> \
  186. <span class=\"second-line\">12:34:56 - 123MB</span> \
  187. </div> \
  188. </a> \
  189. </div>",
  190. showEp.objectID.URIRepresentation,
  191. [[(MLFile *)[[showEp files] anyObject] url] stringByReplacingOccurrencesOfString:@"file://"withString:@""],
  192. showEp.seasonNumber,
  193. showEp.episodeNumber,
  194. showEp.name]];
  195. [mediaInHtml addObject:@"</div></div>"];
  196. } else if ([mo isKindOfClass:[MLLabel class]]) {
  197. NSArray *folderItems = [(MLLabel *)mo sortedFolderItems];
  198. [mediaInHtml addObject:[NSString stringWithFormat:
  199. @"<div style=\"background-image:url('thumbnail/%@.png')\"> \
  200. <a href=\"#\" class=\"inner\"> \
  201. <div class=\"open icon\"></div> \
  202. <div class=\"infos\"> \
  203. <span class=\"first-line\">%@</span> \
  204. <span class=\"second-line\">42 items</span> \
  205. </div> \
  206. </a> \
  207. <div class=\"content\">",
  208. mo.objectID.URIRepresentation,
  209. [(MLLabel *)mo name]]];
  210. for (MLFile *file in folderItems)
  211. [mediaInHtml addObject:[NSString stringWithFormat:
  212. @"<div style=\"background-image:url('thumbnail/%@.png')\"> \
  213. <a href=\"%@\" class=\"inner\"> \
  214. <div class=\"down icon\"></div> \
  215. <div class=\"infos\"> \
  216. <span class=\"first-line\">%@</span> \
  217. <span class=\"second-line\">12:34:56 - 123MB</span> \
  218. </div> \
  219. </a> \
  220. </div>",
  221. file.objectID.URIRepresentation,
  222. [[file url] stringByReplacingOccurrencesOfString:@"file://"withString:@""],
  223. file.title]];
  224. [mediaInHtml addObject:@"</div></div>"];
  225. } else if ([mo isKindOfClass:[MLAlbum class]]) {
  226. NSArray *albumTracks = [(MLAlbum *)mo sortedTracks];
  227. [mediaInHtml addObject:[NSString stringWithFormat:
  228. @"<div style=\"background-image:url('thumbnail/%@.png')\"> \
  229. <a href=\"#\" class=\"inner\"> \
  230. <div class=\"open icon\"></div> \
  231. <div class=\"infos\"> \
  232. <span class=\"first-line\">%@</span> \
  233. <span class=\"second-line\">42 items</span> \
  234. </div> \
  235. </a> \
  236. <div class=\"content\">",
  237. mo.objectID.URIRepresentation,
  238. [(MLAlbum *)mo name]]];
  239. for (MLAlbumTrack *track in albumTracks)
  240. [mediaInHtml addObject:[NSString stringWithFormat:
  241. @"<div style=\"background-image:url('thumbnail/%@.png')\"> \
  242. <a href=\"%@\" class=\"inner\"> \
  243. <div class=\"down icon\"></div> \
  244. <div class=\"infos\"> \
  245. <span class=\"first-line\">%@</span> \
  246. <span class=\"second-line\">12:34:56 - 123MB</span> \
  247. </div> \
  248. </a> \
  249. </div>",
  250. track.objectID.URIRepresentation,
  251. [[(MLFile *)[[track files] anyObject] url] stringByReplacingOccurrencesOfString:@"file://"withString:@""],
  252. track.title]];
  253. [mediaInHtml addObject:@"</div></div>"];
  254. }
  255. }
  256. NSString *deviceModel = [[UIDevice currentDevice] model];
  257. NSDictionary *replacementDict = @{@"FILES" : [mediaInHtml componentsJoinedByString:@" "],
  258. @"WEBINTF_TITLE" : NSLocalizedString(@"WEBINTF_TITLE", nil),
  259. @"WEBINTF_DROPFILES" : NSLocalizedString(@"WEBINTF_DROPFILES", nil),
  260. @"WEBINTF_DROPFILES_LONG" : [NSString stringWithFormat:NSLocalizedString(@"WEBINTF_DROPFILES_LONG", nil), deviceModel],
  261. @"WEBINTF_DOWNLOADFILES" : NSLocalizedString(@"WEBINTF_DOWNLOADFILES", nil),
  262. @"WEBINTF_DOWNLOADFILES_LONG" : [NSString stringWithFormat: NSLocalizedString(@"WEBINTF_DOWNLOADFILES_LONG", nil), deviceModel]};
  263. return [[HTTPDynamicFileResponse alloc] initWithFilePath:[self filePathForURI:path]
  264. forConnection:self
  265. separator:@"%%"
  266. replacementDictionary:replacementDict];
  267. } else if ([relativePath isEqualToString:@"/style.css"]) {
  268. NSDictionary *replacementDict = @{@"WEBINTF_TITLE" : NSLocalizedString(@"WEBINTF_TITLE", nil)};
  269. return [[HTTPDynamicFileResponse alloc] initWithFilePath:[self filePathForURI:path]
  270. forConnection:self
  271. separator:@"%%"
  272. replacementDictionary:replacementDict];
  273. }
  274. return [super httpResponseForMethod:method URI:path];
  275. }
  276. - (void)prepareForBodyWithSize:(UInt64)contentLength
  277. {
  278. // set up mime parser
  279. NSString* boundary = [request headerField:@"boundary"];
  280. _parser = [[MultipartFormDataParser alloc] initWithBoundary:boundary formEncoding:NSUTF8StringEncoding];
  281. _parser.delegate = self;
  282. APLog(@"expecting file of size %lli kB", contentLength / 1024);
  283. _contentLength = contentLength;
  284. }
  285. - (void)processBodyData:(NSData *)postDataChunk
  286. {
  287. /* append data to the parser. It will invoke callbacks to let us handle
  288. * parsed data. */
  289. [_parser appendData:postDataChunk];
  290. _receivedContent += postDataChunk.length;
  291. APLog(@"received %lli kB (%lli %%)", _receivedContent / 1024, ((_receivedContent * 100) / _contentLength));
  292. }
  293. //-----------------------------------------------------------------
  294. #pragma mark multipart form data parser delegate
  295. - (void)processStartOfPartWithHeader:(MultipartMessageHeader*) header
  296. {
  297. /* in this sample, we are not interested in parts, other then file parts.
  298. * check content disposition to find out filename */
  299. MultipartMessageHeaderField* disposition = (header.fields)[@"Content-Disposition"];
  300. NSString* filename = [(disposition.params)[@"filename"] lastPathComponent];
  301. if ((nil == filename) || [filename isEqualToString: @""]) {
  302. // it's either not a file part, or
  303. // an empty form sent. we won't handle it.
  304. return;
  305. }
  306. // create the path where to store the media temporarily
  307. NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
  308. NSString* uploadDirPath = [searchPaths[0] stringByAppendingPathComponent:@"Upload"];
  309. NSFileManager *fileManager = [NSFileManager defaultManager];
  310. BOOL isDir = YES;
  311. if (![fileManager fileExistsAtPath:uploadDirPath isDirectory:&isDir ]) {
  312. [fileManager createDirectoryAtPath:uploadDirPath withIntermediateDirectories:YES attributes:nil error:nil];
  313. }
  314. _filepath = [uploadDirPath stringByAppendingPathComponent: filename];
  315. APLog(@"Saving file to %@", _filepath);
  316. if (![fileManager createDirectoryAtPath:uploadDirPath withIntermediateDirectories:true attributes:nil error:nil])
  317. APLog(@"Could not create directory at path: %@", _filepath);
  318. if (![fileManager createFileAtPath:_filepath contents:nil attributes:nil])
  319. APLog(@"Could not create file at path: %@", _filepath);
  320. _storeFile = [NSFileHandle fileHandleForWritingAtPath:_filepath];
  321. [(VLCAppDelegate*)[UIApplication sharedApplication].delegate networkActivityStarted];
  322. [(VLCAppDelegate*)[UIApplication sharedApplication].delegate disableIdleTimer];
  323. }
  324. - (void)processContent:(NSData*)data WithHeader:(MultipartMessageHeader*) header
  325. {
  326. // here we just write the output from parser to the file.
  327. if (_storeFile) {
  328. @try {
  329. [_storeFile writeData:data];
  330. }
  331. @catch (NSException *exception) {
  332. APLog(@"File to write further data because storage is full.");
  333. [_storeFile closeFile];
  334. _storeFile = nil;
  335. /* don't block */
  336. [self performSelector:@selector(stop) withObject:nil afterDelay:0.1];
  337. }
  338. }
  339. }
  340. - (void)processEndOfPartWithHeader:(MultipartMessageHeader*)header
  341. {
  342. // as the file part is over, we close the file.
  343. APLog(@"closing file");
  344. [_storeFile closeFile];
  345. _storeFile = nil;
  346. }
  347. - (BOOL)shouldDie
  348. {
  349. if (_filepath) {
  350. if (_filepath.length > 0)
  351. [[(VLCAppDelegate*)[UIApplication sharedApplication].delegate uploadController] moveFileFrom:_filepath];
  352. }
  353. return [super shouldDie];
  354. }
  355. @end