adding pods method of package managing
This commit is contained in:
+1386
File diff suppressed because it is too large
Load Diff
+4758
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,111 @@
|
||||
/* Copyright 2014 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import "GTMSessionFetcher.h"
|
||||
|
||||
// GTM HTTP Logging
|
||||
//
|
||||
// All traffic using GTMSessionFetcher can be easily logged. Call
|
||||
//
|
||||
// [GTMSessionFetcher setLoggingEnabled:YES];
|
||||
//
|
||||
// to begin generating log files.
|
||||
//
|
||||
// Unless explicitly set by the application using +setLoggingDirectory:,
|
||||
// logs are put into a default directory, located at:
|
||||
// * macOS: ~/Desktop/GTMHTTPDebugLogs
|
||||
// * iOS simulator: ~/GTMHTTPDebugLogs (in application sandbox)
|
||||
// * iOS device: ~/Documents/GTMHTTPDebugLogs (in application sandbox)
|
||||
//
|
||||
// Tip: use the Finder's "Sort By Date" to find the most recent logs.
|
||||
//
|
||||
// Each run of an application gets a separate set of log files. An html
|
||||
// file is generated to simplify browsing the run's http transactions.
|
||||
// The html file includes javascript links for inline viewing of uploaded
|
||||
// and downloaded data.
|
||||
//
|
||||
// A symlink is created in the logs folder to simplify finding the html file
|
||||
// for the latest run of the application; the symlink is called
|
||||
//
|
||||
// AppName_http_log_newest.html
|
||||
//
|
||||
// For better viewing of XML logs, use Camino or Firefox rather than Safari.
|
||||
//
|
||||
// Each fetcher may be given a comment to be inserted as a label in the logs,
|
||||
// such as
|
||||
// [fetcher setCommentWithFormat:@"retrieve item %@", itemName];
|
||||
//
|
||||
// Projects may define STRIP_GTM_FETCH_LOGGING to remove logging code.
|
||||
|
||||
#if !STRIP_GTM_FETCH_LOGGING
|
||||
|
||||
@interface GTMSessionFetcher (GTMSessionFetcherLogging)
|
||||
|
||||
// Note: on macOS the default logs directory is ~/Desktop/GTMHTTPDebugLogs; on
|
||||
// iOS simulators it will be the ~/GTMHTTPDebugLogs (in the app sandbox); on
|
||||
// iOS devices it will be in ~/Documents/GTMHTTPDebugLogs (in the app sandbox).
|
||||
// These directories will be created as needed, and are excluded from backups
|
||||
// to iCloud and iTunes.
|
||||
//
|
||||
// If a custom directory is set, the directory should already exist. It is
|
||||
// the application's responsibility to exclude any custom directory from
|
||||
// backups, if desired.
|
||||
+ (void)setLoggingDirectory:(NSString *)path;
|
||||
+ (NSString *)loggingDirectory;
|
||||
|
||||
// client apps can turn logging on and off
|
||||
+ (void)setLoggingEnabled:(BOOL)isLoggingEnabled;
|
||||
+ (BOOL)isLoggingEnabled;
|
||||
|
||||
// client apps can turn off logging to a file if they want to only check
|
||||
// the fetcher's log property
|
||||
+ (void)setLoggingToFileEnabled:(BOOL)isLoggingToFileEnabled;
|
||||
+ (BOOL)isLoggingToFileEnabled;
|
||||
|
||||
// client apps can optionally specify process name and date string used in
|
||||
// log file names
|
||||
+ (void)setLoggingProcessName:(NSString *)processName;
|
||||
+ (NSString *)loggingProcessName;
|
||||
|
||||
+ (void)setLoggingDateStamp:(NSString *)dateStamp;
|
||||
+ (NSString *)loggingDateStamp;
|
||||
|
||||
// client apps can specify the directory for the log for this specific run:
|
||||
//
|
||||
// [GTMSessionFetcher setLogDirectoryForCurrentRun:logDirectoryPath];
|
||||
//
|
||||
// Setting this overrides the logging directory, process name, and date stamp when writing
|
||||
// the log file.
|
||||
+ (void)setLogDirectoryForCurrentRun:(NSString *)logDirectoryForCurrentRun;
|
||||
+ (NSString *)logDirectoryForCurrentRun;
|
||||
|
||||
// Prunes old log directories that have not been modified since the provided date.
|
||||
// This will not delete the current run's log directory.
|
||||
+ (void)deleteLogDirectoriesOlderThanDate:(NSDate *)date;
|
||||
|
||||
// internal; called by fetcher
|
||||
- (void)logFetchWithError:(NSError *)error;
|
||||
- (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream;
|
||||
- (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider:
|
||||
(GTMSessionFetcherBodyStreamProvider)streamProvider;
|
||||
|
||||
// internal; accessors useful for viewing logs
|
||||
+ (NSString *)processNameLogPrefix;
|
||||
+ (NSString *)symlinkNameSuffix;
|
||||
+ (NSString *)htmlFileName;
|
||||
|
||||
@end
|
||||
|
||||
#endif // !STRIP_GTM_FETCH_LOGGING
|
||||
@@ -0,0 +1,965 @@
|
||||
/* Copyright 2014 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#if !defined(__has_feature) || !__has_feature(objc_arc)
|
||||
#error "This file requires ARC support."
|
||||
#endif
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#import "GTMSessionFetcherLogging.h"
|
||||
|
||||
#ifndef STRIP_GTM_FETCH_LOGGING
|
||||
#error GTMSessionFetcher headers should have defaulted this if it wasn't already defined.
|
||||
#endif
|
||||
|
||||
#if !STRIP_GTM_FETCH_LOGGING
|
||||
|
||||
// Sensitive credential strings are replaced in logs with _snip_
|
||||
//
|
||||
// Apps that must see the contents of sensitive tokens can set this to 1
|
||||
#ifndef SKIP_GTM_FETCH_LOGGING_SNIPPING
|
||||
#define SKIP_GTM_FETCH_LOGGING_SNIPPING 0
|
||||
#endif
|
||||
|
||||
// If GTMReadMonitorInputStream is available, it can be used for
|
||||
// capturing uploaded streams of data
|
||||
//
|
||||
// We locally declare methods of GTMReadMonitorInputStream so we
|
||||
// do not need to import the header, as some projects may not have it available
|
||||
#if !GTMSESSION_BUILD_COMBINED_SOURCES
|
||||
@interface GTMReadMonitorInputStream : NSInputStream
|
||||
|
||||
+ (instancetype)inputStreamWithStream:(NSInputStream *)input;
|
||||
|
||||
@property(assign) id readDelegate;
|
||||
@property(assign) SEL readSelector;
|
||||
|
||||
@end
|
||||
#else
|
||||
@class GTMReadMonitorInputStream;
|
||||
#endif // !GTMSESSION_BUILD_COMBINED_SOURCES
|
||||
|
||||
@interface GTMSessionFetcher (GTMSessionFetcherLoggingUtilities)
|
||||
|
||||
+ (NSString *)headersStringForDictionary:(NSDictionary *)dict;
|
||||
+ (NSString *)snipSubstringOfString:(NSString *)originalStr
|
||||
betweenStartString:(NSString *)startStr
|
||||
endString:(NSString *)endStr;
|
||||
- (void)inputStream:(GTMReadMonitorInputStream *)stream
|
||||
readIntoBuffer:(void *)buffer
|
||||
length:(int64_t)length;
|
||||
|
||||
@end
|
||||
|
||||
@implementation GTMSessionFetcher (GTMSessionFetcherLogging)
|
||||
|
||||
// fetchers come and fetchers go, but statics are forever
|
||||
static BOOL gIsLoggingEnabled = NO;
|
||||
static BOOL gIsLoggingToFile = YES;
|
||||
static NSString *gLoggingDirectoryPath = nil;
|
||||
static NSString *gLogDirectoryForCurrentRun = nil;
|
||||
static NSString *gLoggingDateStamp = nil;
|
||||
static NSString *gLoggingProcessName = nil;
|
||||
|
||||
+ (void)setLoggingDirectory:(NSString *)path {
|
||||
gLoggingDirectoryPath = [path copy];
|
||||
}
|
||||
|
||||
+ (NSString *)loggingDirectory {
|
||||
if (!gLoggingDirectoryPath) {
|
||||
NSArray *paths = nil;
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
// default to a directory called GTMHTTPDebugLogs into a sandbox-safe
|
||||
// directory that a developer can find easily, the application home
|
||||
paths = @[ NSHomeDirectory() ];
|
||||
#elif TARGET_OS_IPHONE
|
||||
// Neither ~/Desktop nor ~/Home is writable on an actual iOS, watchOS, or tvOS device.
|
||||
// Put it in ~/Documents.
|
||||
paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
||||
#else
|
||||
// default to a directory called GTMHTTPDebugLogs in the desktop folder
|
||||
paths = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES);
|
||||
#endif
|
||||
|
||||
NSString *desktopPath = paths.firstObject;
|
||||
if (desktopPath) {
|
||||
NSString *const kGTMLogFolderName = @"GTMHTTPDebugLogs";
|
||||
NSString *logsFolderPath = [desktopPath stringByAppendingPathComponent:kGTMLogFolderName];
|
||||
|
||||
NSFileManager *fileMgr = [NSFileManager defaultManager];
|
||||
BOOL isDir;
|
||||
BOOL doesFolderExist = [fileMgr fileExistsAtPath:logsFolderPath isDirectory:&isDir];
|
||||
if (!doesFolderExist) {
|
||||
// make the directory
|
||||
doesFolderExist = [fileMgr createDirectoryAtPath:logsFolderPath
|
||||
withIntermediateDirectories:YES
|
||||
attributes:nil
|
||||
error:NULL];
|
||||
if (doesFolderExist) {
|
||||
// The directory has been created. Exclude it from backups.
|
||||
NSURL *pathURL = [NSURL fileURLWithPath:logsFolderPath isDirectory:YES];
|
||||
[pathURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:NULL];
|
||||
}
|
||||
}
|
||||
|
||||
if (doesFolderExist) {
|
||||
// it's there; store it in the global
|
||||
gLoggingDirectoryPath = [logsFolderPath copy];
|
||||
}
|
||||
}
|
||||
}
|
||||
return gLoggingDirectoryPath;
|
||||
}
|
||||
|
||||
+ (void)setLogDirectoryForCurrentRun:(NSString *)logDirectoryForCurrentRun {
|
||||
// Set the path for this run's logs.
|
||||
gLogDirectoryForCurrentRun = [logDirectoryForCurrentRun copy];
|
||||
}
|
||||
|
||||
+ (NSString *)logDirectoryForCurrentRun {
|
||||
// make a directory for this run's logs, like SyncProto_logs_10-16_01-56-58PM
|
||||
if (gLogDirectoryForCurrentRun) return gLogDirectoryForCurrentRun;
|
||||
|
||||
NSString *parentDir = [self loggingDirectory];
|
||||
NSString *logNamePrefix = [self processNameLogPrefix];
|
||||
NSString *dateStamp = [self loggingDateStamp];
|
||||
NSString *dirName = [NSString stringWithFormat:@"%@%@", logNamePrefix, dateStamp];
|
||||
NSString *logDirectory = [parentDir stringByAppendingPathComponent:dirName];
|
||||
|
||||
if (gIsLoggingToFile) {
|
||||
NSFileManager *fileMgr = [NSFileManager defaultManager];
|
||||
// Be sure that the first time this app runs, it's not writing to a preexisting folder
|
||||
static BOOL gShouldReuseFolder = NO;
|
||||
if (!gShouldReuseFolder) {
|
||||
gShouldReuseFolder = YES;
|
||||
NSString *origLogDir = logDirectory;
|
||||
for (int ctr = 2; ctr < 20; ++ctr) {
|
||||
if (![fileMgr fileExistsAtPath:logDirectory]) break;
|
||||
|
||||
// append a digit
|
||||
logDirectory = [origLogDir stringByAppendingFormat:@"_%d", ctr];
|
||||
}
|
||||
}
|
||||
if (![fileMgr createDirectoryAtPath:logDirectory
|
||||
withIntermediateDirectories:YES
|
||||
attributes:nil
|
||||
error:NULL])
|
||||
return nil;
|
||||
}
|
||||
gLogDirectoryForCurrentRun = logDirectory;
|
||||
|
||||
return gLogDirectoryForCurrentRun;
|
||||
}
|
||||
|
||||
+ (void)setLoggingEnabled:(BOOL)isLoggingEnabled {
|
||||
gIsLoggingEnabled = isLoggingEnabled;
|
||||
}
|
||||
|
||||
+ (BOOL)isLoggingEnabled {
|
||||
return gIsLoggingEnabled;
|
||||
}
|
||||
|
||||
+ (void)setLoggingToFileEnabled:(BOOL)isLoggingToFileEnabled {
|
||||
gIsLoggingToFile = isLoggingToFileEnabled;
|
||||
}
|
||||
|
||||
+ (BOOL)isLoggingToFileEnabled {
|
||||
return gIsLoggingToFile;
|
||||
}
|
||||
|
||||
+ (void)setLoggingProcessName:(NSString *)processName {
|
||||
gLoggingProcessName = [processName copy];
|
||||
}
|
||||
|
||||
+ (NSString *)loggingProcessName {
|
||||
// get the process name (once per run) replacing spaces with underscores
|
||||
if (!gLoggingProcessName) {
|
||||
NSString *procName = [[NSProcessInfo processInfo] processName];
|
||||
gLoggingProcessName = [procName stringByReplacingOccurrencesOfString:@" " withString:@"_"];
|
||||
}
|
||||
return gLoggingProcessName;
|
||||
}
|
||||
|
||||
+ (void)setLoggingDateStamp:(NSString *)dateStamp {
|
||||
gLoggingDateStamp = [dateStamp copy];
|
||||
}
|
||||
|
||||
+ (NSString *)loggingDateStamp {
|
||||
// We'll pick one date stamp per run, so a run that starts at a later second
|
||||
// will get a unique results html file
|
||||
if (!gLoggingDateStamp) {
|
||||
// produce a string like 08-21_01-41-23PM
|
||||
|
||||
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
|
||||
[formatter setFormatterBehavior:NSDateFormatterBehavior10_4];
|
||||
[formatter setDateFormat:@"M-dd_hh-mm-ssa"];
|
||||
|
||||
gLoggingDateStamp = [formatter stringFromDate:[NSDate date]];
|
||||
}
|
||||
return gLoggingDateStamp;
|
||||
}
|
||||
|
||||
+ (NSString *)processNameLogPrefix {
|
||||
static NSString *gPrefix = nil;
|
||||
if (!gPrefix) {
|
||||
NSString *processName = [self loggingProcessName];
|
||||
gPrefix = [[NSString alloc] initWithFormat:@"%@_log_", processName];
|
||||
}
|
||||
return gPrefix;
|
||||
}
|
||||
|
||||
+ (NSString *)symlinkNameSuffix {
|
||||
return @"_log_newest.html";
|
||||
}
|
||||
|
||||
+ (NSString *)htmlFileName {
|
||||
return @"aperçu_http_log.html";
|
||||
}
|
||||
|
||||
+ (void)deleteLogDirectoriesOlderThanDate:(NSDate *)cutoffDate {
|
||||
NSFileManager *fileMgr = [NSFileManager defaultManager];
|
||||
NSURL *parentDir = [NSURL fileURLWithPath:[[self class] loggingDirectory]];
|
||||
NSURL *logDirectoryForCurrentRun =
|
||||
[NSURL fileURLWithPath:[[self class] logDirectoryForCurrentRun]];
|
||||
NSError *error;
|
||||
NSArray *contents = [fileMgr contentsOfDirectoryAtURL:parentDir
|
||||
includingPropertiesForKeys:@[ NSURLContentModificationDateKey ]
|
||||
options:0
|
||||
error:&error];
|
||||
for (NSURL *itemURL in contents) {
|
||||
if ([itemURL isEqual:logDirectoryForCurrentRun]) continue;
|
||||
|
||||
NSDate *modDate;
|
||||
if ([itemURL getResourceValue:&modDate forKey:NSURLContentModificationDateKey error:&error]) {
|
||||
if ([modDate compare:cutoffDate] == NSOrderedAscending) {
|
||||
if (![fileMgr removeItemAtURL:itemURL error:&error]) {
|
||||
NSLog(@"deleteLogDirectoriesOlderThanDate failed to delete %@: %@", itemURL.path, error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NSLog(@"deleteLogDirectoriesOlderThanDate failed to get mod date of %@: %@", itemURL.path,
|
||||
error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// formattedStringFromData returns a prettyprinted string for XML or JSON input,
|
||||
// and a plain string for other input data
|
||||
- (NSString *)formattedStringFromData:(NSData *)inputData
|
||||
contentType:(NSString *)contentType
|
||||
JSON:(NSDictionary **)outJSON {
|
||||
if (!inputData) return nil;
|
||||
|
||||
// if the content type is JSON and we have the parsing class available, use that
|
||||
if ([contentType hasPrefix:@"application/json"] && inputData.length > 5) {
|
||||
// convert from JSON string to NSObjects and back to a formatted string
|
||||
NSMutableDictionary *obj =
|
||||
[NSJSONSerialization JSONObjectWithData:inputData
|
||||
options:NSJSONReadingMutableContainers
|
||||
error:NULL];
|
||||
if (obj) {
|
||||
if (outJSON) *outJSON = obj;
|
||||
if ([obj isKindOfClass:[NSMutableDictionary class]]) {
|
||||
// for security and privacy, omit OAuth 2 response access and refresh tokens
|
||||
if ([obj valueForKey:@"refresh_token"] != nil) {
|
||||
[obj setObject:@"_snip_" forKey:@"refresh_token"];
|
||||
}
|
||||
if ([obj valueForKey:@"access_token"] != nil) {
|
||||
[obj setObject:@"_snip_" forKey:@"access_token"];
|
||||
}
|
||||
}
|
||||
NSData *data = [NSJSONSerialization dataWithJSONObject:obj
|
||||
options:NSJSONWritingPrettyPrinted
|
||||
error:NULL];
|
||||
if (data) {
|
||||
NSString *jsonStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
return jsonStr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if !TARGET_OS_IPHONE && !GTM_SKIP_LOG_XMLFORMAT
|
||||
// verify that this data starts with the bytes indicating XML
|
||||
|
||||
NSString *const kXMLLintPath = @"/usr/bin/xmllint";
|
||||
static BOOL gHasCheckedAvailability = NO;
|
||||
static BOOL gIsXMLLintAvailable = NO;
|
||||
|
||||
if (!gHasCheckedAvailability) {
|
||||
gIsXMLLintAvailable = [[NSFileManager defaultManager] fileExistsAtPath:kXMLLintPath];
|
||||
gHasCheckedAvailability = YES;
|
||||
}
|
||||
if (gIsXMLLintAvailable && inputData.length > 5 && strncmp(inputData.bytes, "<?xml", 5) == 0) {
|
||||
// call xmllint to format the data
|
||||
NSTask *task = [[NSTask alloc] init];
|
||||
[task setLaunchPath:kXMLLintPath];
|
||||
|
||||
// use the dash argument to specify stdin as the source file
|
||||
[task setArguments:@[ @"--format", @"-" ]];
|
||||
[task setEnvironment:@{}];
|
||||
|
||||
NSPipe *inputPipe = [NSPipe pipe];
|
||||
NSPipe *outputPipe = [NSPipe pipe];
|
||||
[task setStandardInput:inputPipe];
|
||||
[task setStandardOutput:outputPipe];
|
||||
|
||||
[task launch];
|
||||
|
||||
[[inputPipe fileHandleForWriting] writeData:inputData];
|
||||
[[inputPipe fileHandleForWriting] closeFile];
|
||||
|
||||
// drain the stdout before waiting for the task to exit
|
||||
NSData *formattedData = [[outputPipe fileHandleForReading] readDataToEndOfFile];
|
||||
|
||||
[task waitUntilExit];
|
||||
|
||||
int status = [task terminationStatus];
|
||||
if (status == 0 && formattedData.length > 0) {
|
||||
// success
|
||||
inputData = formattedData;
|
||||
}
|
||||
}
|
||||
#else
|
||||
// we can't call external tasks on the iPhone; leave the XML unformatted
|
||||
#endif
|
||||
|
||||
NSString *dataStr = [[NSString alloc] initWithData:inputData encoding:NSUTF8StringEncoding];
|
||||
return dataStr;
|
||||
}
|
||||
|
||||
// stringFromStreamData creates a string given the supplied data
|
||||
//
|
||||
// If NSString can create a UTF-8 string from the data, then that is returned.
|
||||
//
|
||||
// Otherwise, this routine tries to find a MIME boundary at the beginning of the data block, and
|
||||
// uses that to break up the data into parts. Each part will be used to try to make a UTF-8 string.
|
||||
// For parts that fail, a replacement string showing the part header and <<n bytes>> is supplied
|
||||
// in place of the binary data.
|
||||
|
||||
- (NSString *)stringFromStreamData:(NSData *)data contentType:(NSString *)contentType {
|
||||
if (!data) return nil;
|
||||
|
||||
// optimistically, see if the whole data block is UTF-8
|
||||
NSString *streamDataStr = [self formattedStringFromData:data contentType:contentType JSON:NULL];
|
||||
if (streamDataStr) return streamDataStr;
|
||||
|
||||
// Munge a buffer by replacing non-ASCII bytes with underscores, and turn that munged buffer an
|
||||
// NSString. That gives us a string we can use with NSScanner.
|
||||
NSMutableData *mutableData = [NSMutableData dataWithData:data];
|
||||
unsigned char *bytes = (unsigned char *)mutableData.mutableBytes;
|
||||
|
||||
for (unsigned int idx = 0; idx < mutableData.length; ++idx) {
|
||||
if (bytes[idx] > 0x7F || bytes[idx] == 0) {
|
||||
bytes[idx] = '_';
|
||||
}
|
||||
}
|
||||
|
||||
NSString *mungedStr = [[NSString alloc] initWithData:mutableData encoding:NSUTF8StringEncoding];
|
||||
if (mungedStr) {
|
||||
// scan for the boundary string
|
||||
NSString *boundary = nil;
|
||||
NSScanner *scanner = [NSScanner scannerWithString:mungedStr];
|
||||
|
||||
if ([scanner scanUpToString:@"\r\n" intoString:&boundary] && [boundary hasPrefix:@"--"]) {
|
||||
// we found a boundary string; use it to divide the string into parts
|
||||
NSArray *mungedParts = [mungedStr componentsSeparatedByString:boundary];
|
||||
|
||||
// look at each munged part in the original string, and try to convert those into UTF-8
|
||||
NSMutableArray *origParts = [NSMutableArray array];
|
||||
NSUInteger offset = 0;
|
||||
for (NSString *mungedPart in mungedParts) {
|
||||
NSUInteger partSize = mungedPart.length;
|
||||
NSData *origPartData = [data subdataWithRange:NSMakeRange(offset, partSize)];
|
||||
NSString *origPartStr = [[NSString alloc] initWithData:origPartData
|
||||
encoding:NSUTF8StringEncoding];
|
||||
if (origPartStr) {
|
||||
// we could make this original part into UTF-8; use the string
|
||||
[origParts addObject:origPartStr];
|
||||
} else {
|
||||
// this part can't be made into UTF-8; scan the header, if we can
|
||||
NSString *header = nil;
|
||||
NSScanner *headerScanner = [NSScanner scannerWithString:mungedPart];
|
||||
if (![headerScanner scanUpToString:@"\r\n\r\n" intoString:&header]) {
|
||||
// we couldn't find a header
|
||||
header = @"";
|
||||
}
|
||||
// make a part string with the header and <<n bytes>>
|
||||
NSString *binStr = [NSString
|
||||
stringWithFormat:@"\r%@\r<<%lu bytes>>\r", header, (long)(partSize - header.length)];
|
||||
[origParts addObject:binStr];
|
||||
}
|
||||
offset += partSize + boundary.length;
|
||||
}
|
||||
// rejoin the original parts
|
||||
streamDataStr = [origParts componentsJoinedByString:boundary];
|
||||
}
|
||||
}
|
||||
if (!streamDataStr) {
|
||||
// give up; just make a string showing the uploaded bytes
|
||||
streamDataStr = [NSString stringWithFormat:@"<<%u bytes>>", (unsigned int)data.length];
|
||||
}
|
||||
return streamDataStr;
|
||||
}
|
||||
|
||||
// logFetchWithError is called following a successful or failed fetch attempt
|
||||
//
|
||||
// This method does all the work for appending to and creating log files
|
||||
|
||||
- (void)logFetchWithError:(NSError *)error {
|
||||
if (![[self class] isLoggingEnabled]) return;
|
||||
NSString *logDirectory = [[self class] logDirectoryForCurrentRun];
|
||||
if (!logDirectory) return;
|
||||
NSString *processName = [[self class] loggingProcessName];
|
||||
|
||||
// TODO: add Javascript to display response data formatted in hex
|
||||
|
||||
// each response's NSData goes into its own xml or txt file, though all responses for this run of
|
||||
// the app share a main html file. This counter tracks all fetch responses for this app run.
|
||||
//
|
||||
// we'll use a local variable since this routine may be reentered while waiting for XML formatting
|
||||
// to be completed by an external task
|
||||
static int gResponseCounter = 0;
|
||||
int responseCounter = ++gResponseCounter;
|
||||
|
||||
NSURLResponse *response = [self response];
|
||||
NSDictionary *responseHeaders = [self responseHeaders];
|
||||
NSString *responseDataStr = nil;
|
||||
NSDictionary *responseJSON = nil;
|
||||
|
||||
// if there's response data, decide what kind of file to put it in based on the first bytes of the
|
||||
// file or on the mime type supplied by the server
|
||||
NSString *responseMIMEType = [response MIMEType];
|
||||
BOOL isResponseImage = NO;
|
||||
|
||||
// file name for an image data file
|
||||
NSString *responseDataFileName = nil;
|
||||
|
||||
int64_t responseDataLength = self.downloadedLength;
|
||||
if (responseDataLength > 0) {
|
||||
NSData *downloadedData = self.downloadedData;
|
||||
if (downloadedData == nil && responseDataLength > 0 && responseDataLength < 20000 &&
|
||||
self.destinationFileURL) {
|
||||
// There's a download file that's not too big, so get the data to display from the downloaded
|
||||
// file.
|
||||
NSURL *destinationURL = self.destinationFileURL;
|
||||
downloadedData = [NSData dataWithContentsOfURL:destinationURL];
|
||||
}
|
||||
NSString *responseType = [responseHeaders valueForKey:@"Content-Type"];
|
||||
responseDataStr = [self formattedStringFromData:downloadedData
|
||||
contentType:responseType
|
||||
JSON:&responseJSON];
|
||||
NSString *responseDataExtn = nil;
|
||||
NSData *dataToWrite = nil;
|
||||
if (responseDataStr) {
|
||||
// we were able to make a UTF-8 string from the response data
|
||||
if ([responseMIMEType isEqual:@"application/atom+xml"] ||
|
||||
[responseMIMEType hasSuffix:@"/xml"]) {
|
||||
responseDataExtn = @"xml";
|
||||
dataToWrite = [responseDataStr dataUsingEncoding:NSUTF8StringEncoding];
|
||||
}
|
||||
} else if ([responseMIMEType isEqual:@"image/jpeg"]) {
|
||||
responseDataExtn = @"jpg";
|
||||
dataToWrite = downloadedData;
|
||||
isResponseImage = YES;
|
||||
} else if ([responseMIMEType isEqual:@"image/gif"]) {
|
||||
responseDataExtn = @"gif";
|
||||
dataToWrite = downloadedData;
|
||||
isResponseImage = YES;
|
||||
} else if ([responseMIMEType isEqual:@"image/png"]) {
|
||||
responseDataExtn = @"png";
|
||||
dataToWrite = downloadedData;
|
||||
isResponseImage = YES;
|
||||
} else {
|
||||
// add more non-text types here
|
||||
}
|
||||
// if we have an extension, save the raw data in a file with that extension
|
||||
if (responseDataExtn && dataToWrite) {
|
||||
// generate a response file base name like
|
||||
NSString *responseBaseName =
|
||||
[NSString stringWithFormat:@"fetch_%d_response", responseCounter];
|
||||
responseDataFileName = [responseBaseName stringByAppendingPathExtension:responseDataExtn];
|
||||
NSString *responseDataFilePath =
|
||||
[logDirectory stringByAppendingPathComponent:responseDataFileName];
|
||||
|
||||
NSError *downloadedError = nil;
|
||||
if (gIsLoggingToFile && ![dataToWrite writeToFile:responseDataFilePath
|
||||
options:0
|
||||
error:&downloadedError]) {
|
||||
NSLog(@"%@ logging write error:%@ (%@)", [self class], downloadedError,
|
||||
responseDataFileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
// we'll have one main html file per run of the app
|
||||
NSString *htmlName = [[self class] htmlFileName];
|
||||
NSString *htmlPath = [logDirectory stringByAppendingPathComponent:htmlName];
|
||||
|
||||
// if the html file exists (from logging previous fetches) we don't need
|
||||
// to re-write the header or the scripts
|
||||
NSFileManager *fileMgr = [NSFileManager defaultManager];
|
||||
BOOL didFileExist = [fileMgr fileExistsAtPath:htmlPath];
|
||||
|
||||
NSMutableString *outputHTML = [NSMutableString string];
|
||||
|
||||
// we need a header to say we'll have UTF-8 text
|
||||
if (!didFileExist) {
|
||||
[outputHTML
|
||||
appendFormat:@"<html><head><meta http-equiv=\"content-type\" "
|
||||
"content=\"text/html; charset=UTF-8\"><title>%@ HTTP fetch log %@</title>",
|
||||
processName, [[self class] loggingDateStamp]];
|
||||
}
|
||||
// now write the visible html elements
|
||||
NSString *copyableFileName = [NSString stringWithFormat:@"fetch_%d.txt", responseCounter];
|
||||
|
||||
NSDate *now = [NSDate date];
|
||||
// write the date & time, the comment, and the link to the plain-text (copyable) log
|
||||
[outputHTML appendFormat:@"<b>%@ ", now];
|
||||
|
||||
NSString *comment = [self comment];
|
||||
if (comment.length > 0) {
|
||||
[outputHTML appendFormat:@"%@ ", comment];
|
||||
}
|
||||
[outputHTML
|
||||
appendFormat:@"</b><a href='%@'><i>request/response log</i></a><br>", copyableFileName];
|
||||
NSTimeInterval elapsed = -self.initialBeginFetchDate.timeIntervalSinceNow;
|
||||
[outputHTML appendFormat:@"elapsed: %5.3fsec<br>", elapsed];
|
||||
|
||||
// write the request URL
|
||||
NSURLRequest *request = self.request;
|
||||
NSString *requestMethod = request.HTTPMethod;
|
||||
NSURL *requestURL = request.URL;
|
||||
|
||||
// Save the request URL for next time in case this redirects.
|
||||
NSString *redirectedFromURLString = [self.redirectedFromURL absoluteString];
|
||||
self.redirectedFromURL = [requestURL copy];
|
||||
if (redirectedFromURLString) {
|
||||
[outputHTML appendFormat:@"<FONT COLOR='#990066'><i>redirected from %@</i></FONT><br>",
|
||||
redirectedFromURLString];
|
||||
}
|
||||
[outputHTML appendFormat:@"<b>request:</b> %@ <code>%@</code><br>\n", requestMethod, requestURL];
|
||||
|
||||
// write the request headers
|
||||
NSDictionary *requestHeaders = request.allHTTPHeaderFields;
|
||||
NSUInteger numberOfRequestHeaders = requestHeaders.count;
|
||||
if (numberOfRequestHeaders > 0) {
|
||||
// Indicate if the request is authorized; warn if the request is authorized but non-SSL
|
||||
NSString *auth = [requestHeaders objectForKey:@"Authorization"];
|
||||
NSString *headerDetails = @"";
|
||||
if (auth) {
|
||||
BOOL isInsecure = [[requestURL scheme] isEqual:@"http"];
|
||||
if (isInsecure) {
|
||||
// 26A0 = ⚠
|
||||
headerDetails =
|
||||
@" <i>authorized, non-SSL</i><FONT COLOR='#FF00FF'> ⚠</FONT> ";
|
||||
} else {
|
||||
headerDetails = @" <i>authorized</i>";
|
||||
}
|
||||
}
|
||||
NSString *cookiesHdr = [requestHeaders objectForKey:@"Cookie"];
|
||||
if (cookiesHdr) {
|
||||
headerDetails = [headerDetails stringByAppendingString:@" <i>cookies</i>"];
|
||||
}
|
||||
NSString *matchHdr = [requestHeaders objectForKey:@"If-Match"];
|
||||
if (matchHdr) {
|
||||
headerDetails = [headerDetails stringByAppendingString:@" <i>if-match</i>"];
|
||||
}
|
||||
matchHdr = [requestHeaders objectForKey:@"If-None-Match"];
|
||||
if (matchHdr) {
|
||||
headerDetails =
|
||||
[headerDetails stringByAppendingString:@" <i>if-none-match</i>"];
|
||||
}
|
||||
[outputHTML appendFormat:@" headers: %d %@<br>", (int)numberOfRequestHeaders,
|
||||
headerDetails];
|
||||
} else {
|
||||
[outputHTML appendFormat:@" headers: none<br>"];
|
||||
}
|
||||
// write the request post data
|
||||
NSData *bodyData = nil;
|
||||
NSData *loggedStreamData = self.loggedStreamData;
|
||||
if (loggedStreamData) {
|
||||
bodyData = loggedStreamData;
|
||||
} else {
|
||||
bodyData = self.bodyData;
|
||||
if (bodyData == nil) {
|
||||
bodyData = self.request.HTTPBody;
|
||||
}
|
||||
}
|
||||
uint64_t bodyDataLength = bodyData.length;
|
||||
|
||||
if (bodyData.length == 0) {
|
||||
// If the data is in a body upload file URL, read that in if it's not huge.
|
||||
NSURL *bodyFileURL = self.bodyFileURL;
|
||||
if (bodyFileURL) {
|
||||
NSNumber *fileSizeNum = nil;
|
||||
NSError *fileSizeError = nil;
|
||||
if ([bodyFileURL getResourceValue:&fileSizeNum
|
||||
forKey:NSURLFileSizeKey
|
||||
error:&fileSizeError]) {
|
||||
bodyDataLength = [fileSizeNum unsignedLongLongValue];
|
||||
if (bodyDataLength > 0 && bodyDataLength < 50000) {
|
||||
bodyData = [NSData dataWithContentsOfURL:bodyFileURL
|
||||
options:NSDataReadingUncached
|
||||
error:&fileSizeError];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
NSString *bodyDataStr = nil;
|
||||
NSString *postType = [requestHeaders valueForKey:@"Content-Type"];
|
||||
|
||||
if (bodyDataLength > 0) {
|
||||
[outputHTML appendFormat:@" data: %llu bytes, <code>%@</code><br>\n",
|
||||
bodyDataLength, postType ? postType : @"(no type)"];
|
||||
NSString *logRequestBody = self.logRequestBody;
|
||||
if (logRequestBody) {
|
||||
bodyDataStr = [logRequestBody copy];
|
||||
self.logRequestBody = nil;
|
||||
} else {
|
||||
bodyDataStr = [self stringFromStreamData:bodyData contentType:postType];
|
||||
if (bodyDataStr) {
|
||||
// remove OAuth 2 client secret and refresh token
|
||||
bodyDataStr = [[self class] snipSubstringOfString:bodyDataStr
|
||||
betweenStartString:@"client_secret="
|
||||
endString:@"&"];
|
||||
bodyDataStr = [[self class] snipSubstringOfString:bodyDataStr
|
||||
betweenStartString:@"refresh_token="
|
||||
endString:@"&"];
|
||||
// remove ClientLogin password
|
||||
bodyDataStr = [[self class] snipSubstringOfString:bodyDataStr
|
||||
betweenStartString:@"&Passwd="
|
||||
endString:@"&"];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no post data
|
||||
}
|
||||
// write the response status, MIME type, URL
|
||||
NSInteger status = [self statusCode];
|
||||
if (response) {
|
||||
NSString *statusString = @"";
|
||||
if (status != 0) {
|
||||
if (status == 200 || status == 201) {
|
||||
statusString = [NSString stringWithFormat:@"%ld", (long)status];
|
||||
|
||||
// report any JSON-RPC error
|
||||
if ([responseJSON isKindOfClass:[NSDictionary class]]) {
|
||||
NSDictionary *jsonError = [responseJSON objectForKey:@"error"];
|
||||
if ([jsonError isKindOfClass:[NSDictionary class]]) {
|
||||
NSString *jsonCode = [[jsonError valueForKey:@"code"] description];
|
||||
NSString *jsonMessage = [jsonError valueForKey:@"message"];
|
||||
if (jsonCode || jsonMessage) {
|
||||
// 2691 = ⚑
|
||||
NSString *const jsonErrFmt = @" <i>JSON error:</i> <FONT "
|
||||
@"COLOR='#FF00FF'>%@ %@ ⚑</FONT>";
|
||||
statusString =
|
||||
[statusString stringByAppendingFormat:jsonErrFmt, jsonCode ? jsonCode : @"",
|
||||
jsonMessage ? jsonMessage : @""];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// purple for anything other than 200 or 201
|
||||
NSString *flag = status >= 400 ? @" ⚑" : @""; // 2691 = ⚑
|
||||
NSString *explanation = [NSHTTPURLResponse localizedStringForStatusCode:status];
|
||||
NSString *const statusFormat = @"<FONT COLOR='#FF00FF'>%ld %@ %@</FONT>";
|
||||
statusString = [NSString stringWithFormat:statusFormat, (long)status, explanation, flag];
|
||||
}
|
||||
}
|
||||
// show the response URL only if it's different from the request URL
|
||||
NSString *responseURLStr = @"";
|
||||
NSURL *responseURL = response.URL;
|
||||
|
||||
if (responseURL && ![responseURL isEqual:request.URL]) {
|
||||
NSString *const responseURLFormat =
|
||||
@"<FONT COLOR='#FF00FF'>response URL:</FONT> <code>%@</code><br>\n";
|
||||
responseURLStr = [NSString stringWithFormat:responseURLFormat, [responseURL absoluteString]];
|
||||
}
|
||||
[outputHTML appendFormat:@"<b>response:</b> status %@<br>\n%@", statusString,
|
||||
responseURLStr];
|
||||
// Write the response headers
|
||||
NSUInteger numberOfResponseHeaders = responseHeaders.count;
|
||||
if (numberOfResponseHeaders > 0) {
|
||||
// Indicate if the server is setting cookies
|
||||
NSString *cookiesSet = [responseHeaders valueForKey:@"Set-Cookie"];
|
||||
NSString *cookiesStr =
|
||||
cookiesSet ? @" <FONT COLOR='#990066'><i>sets cookies</i></FONT>" : @"";
|
||||
// Indicate if the server is redirecting
|
||||
NSString *location = [responseHeaders valueForKey:@"Location"];
|
||||
BOOL isRedirect = status >= 300 && status <= 399 && location != nil;
|
||||
NSString *redirectsStr =
|
||||
isRedirect ? @" <FONT COLOR='#990066'><i>redirects</i></FONT>" : @"";
|
||||
[outputHTML appendFormat:@" headers: %d %@ %@<br>\n",
|
||||
(int)numberOfResponseHeaders, cookiesStr, redirectsStr];
|
||||
} else {
|
||||
[outputHTML appendString:@" headers: none<br>\n"];
|
||||
}
|
||||
}
|
||||
// error
|
||||
if (error) {
|
||||
[outputHTML appendFormat:@"<b>Error:</b> %@ <br>\n", error.description];
|
||||
}
|
||||
// Write the response data
|
||||
if (responseDataFileName) {
|
||||
if (isResponseImage) {
|
||||
// Make a small inline image that links to the full image file
|
||||
[outputHTML appendFormat:@" data: %lld bytes, <code>%@</code><br>",
|
||||
responseDataLength, responseMIMEType];
|
||||
NSString *const fmt = @"<a href=\"%@\"><img src='%@' alt='image' style='border:solid "
|
||||
@"thin;max-height:32'></a>\n";
|
||||
[outputHTML appendFormat:fmt, responseDataFileName, responseDataFileName];
|
||||
} else {
|
||||
// The response data was XML; link to the xml file
|
||||
NSString *const fmt = @" data: %lld bytes, "
|
||||
@"<code>%@</code> <i><a href=\"%@\">%@</a></i>\n";
|
||||
[outputHTML appendFormat:fmt, responseDataLength, responseMIMEType, responseDataFileName,
|
||||
[responseDataFileName pathExtension]];
|
||||
}
|
||||
} else {
|
||||
// The response data was not an image; just show the length and MIME type
|
||||
[outputHTML appendFormat:@" data: %lld bytes, <code>%@</code>\n",
|
||||
responseDataLength,
|
||||
responseMIMEType ? responseMIMEType : @"(no response type)"];
|
||||
}
|
||||
// Make a single string of the request and response, suitable for copying
|
||||
// to the clipboard and pasting into a bug report
|
||||
NSMutableString *copyable = [NSMutableString string];
|
||||
if (comment) {
|
||||
[copyable appendFormat:@"%@\n\n", comment];
|
||||
}
|
||||
[copyable appendFormat:@"%@ elapsed: %5.3fsec\n", now, elapsed];
|
||||
if (redirectedFromURLString) {
|
||||
[copyable appendFormat:@"Redirected from %@\n", redirectedFromURLString];
|
||||
}
|
||||
[copyable appendFormat:@"Request: %@ %@\n", requestMethod, requestURL];
|
||||
if (requestHeaders.count > 0) {
|
||||
[copyable appendFormat:@"Request headers:\n%@\n",
|
||||
[[self class] headersStringForDictionary:requestHeaders]];
|
||||
}
|
||||
if (bodyDataLength > 0) {
|
||||
[copyable appendFormat:@"Request body: (%llu bytes)\n", bodyDataLength];
|
||||
if (bodyDataStr) {
|
||||
[copyable appendFormat:@"%@\n", bodyDataStr];
|
||||
}
|
||||
[copyable appendString:@"\n"];
|
||||
}
|
||||
if (response) {
|
||||
[copyable appendFormat:@"Response: status %d\n", (int)status];
|
||||
[copyable appendFormat:@"Response headers:\n%@\n",
|
||||
[[self class] headersStringForDictionary:responseHeaders]];
|
||||
[copyable appendFormat:@"Response body: (%lld bytes)\n", responseDataLength];
|
||||
if (responseDataLength > 0) {
|
||||
NSString *logResponseBody = self.logResponseBody;
|
||||
if (logResponseBody) {
|
||||
// The user has provided the response body text.
|
||||
responseDataStr = [logResponseBody copy];
|
||||
self.logResponseBody = nil;
|
||||
}
|
||||
if (responseDataStr != nil) {
|
||||
[copyable appendFormat:@"%@\n", responseDataStr];
|
||||
} else {
|
||||
// Even though it's redundant, we'll put in text to indicate that all the bytes are binary.
|
||||
if (self.destinationFileURL) {
|
||||
[copyable appendFormat:@"<<%lld bytes>> to file %@\n", responseDataLength,
|
||||
self.destinationFileURL.path];
|
||||
} else {
|
||||
[copyable appendFormat:@"<<%lld bytes>>\n", responseDataLength];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
[copyable appendFormat:@"Error: %@\n", error];
|
||||
}
|
||||
// Save to log property before adding the separator
|
||||
self.log = copyable;
|
||||
|
||||
[copyable appendString:@"-----------------------------------------------------------\n"];
|
||||
|
||||
// Write the copyable version to another file (linked to at the top of the html file, above)
|
||||
//
|
||||
// Ideally, something to just copy this to the clipboard like
|
||||
// <span onCopy='window.event.clipboardData.setData(\"Text\",
|
||||
// \"copyable stuff\");return false;'>Copy here.</span>"
|
||||
// would work everywhere, but it only works in Safari as of 8/2010
|
||||
if (gIsLoggingToFile) {
|
||||
NSString *parentDir = [[self class] loggingDirectory];
|
||||
NSString *copyablePath = [logDirectory stringByAppendingPathComponent:copyableFileName];
|
||||
NSError *copyableError = nil;
|
||||
if (![copyable writeToFile:copyablePath
|
||||
atomically:NO
|
||||
encoding:NSUTF8StringEncoding
|
||||
error:©ableError]) {
|
||||
// Error writing to file
|
||||
NSLog(@"%@ logging write error:%@ (%@)", [self class], copyableError, copyablePath);
|
||||
}
|
||||
[outputHTML appendString:@"<br><hr><p>"];
|
||||
|
||||
// Append the HTML to the main output file
|
||||
const char *htmlBytes = outputHTML.UTF8String;
|
||||
NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:htmlPath append:YES];
|
||||
[stream open];
|
||||
[stream write:(const uint8_t *)htmlBytes maxLength:strlen(htmlBytes)];
|
||||
[stream close];
|
||||
|
||||
// Make a symlink to the latest html
|
||||
NSString *const symlinkNameSuffix = [[self class] symlinkNameSuffix];
|
||||
NSString *symlinkName = [processName stringByAppendingString:symlinkNameSuffix];
|
||||
NSString *symlinkPath = [parentDir stringByAppendingPathComponent:symlinkName];
|
||||
|
||||
[fileMgr removeItemAtPath:symlinkPath error:NULL];
|
||||
[fileMgr createSymbolicLinkAtPath:symlinkPath withDestinationPath:htmlPath error:NULL];
|
||||
#if TARGET_OS_IPHONE
|
||||
static BOOL gReportedLoggingPath = NO;
|
||||
if (!gReportedLoggingPath) {
|
||||
gReportedLoggingPath = YES;
|
||||
NSLog(@"GTMSessionFetcher logging to \"%@\"", parentDir);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
- (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream {
|
||||
if (!inputStream) return nil;
|
||||
if (![GTMSessionFetcher isLoggingEnabled]) return inputStream;
|
||||
|
||||
[self clearLoggedStreamData]; // Clear any previous data.
|
||||
Class monitorClass = NSClassFromString(@"GTMReadMonitorInputStream");
|
||||
if (!monitorClass) {
|
||||
NSString const *str = @"<<Uploaded stream log unavailable without GTMReadMonitorInputStream>>";
|
||||
NSData *stringData = [str dataUsingEncoding:NSUTF8StringEncoding];
|
||||
[self appendLoggedStreamData:stringData];
|
||||
return inputStream;
|
||||
}
|
||||
inputStream = [monitorClass inputStreamWithStream:inputStream];
|
||||
|
||||
GTMReadMonitorInputStream *readMonitorInputStream = (GTMReadMonitorInputStream *)inputStream;
|
||||
[readMonitorInputStream setReadDelegate:self];
|
||||
SEL readSel = @selector(inputStream:readIntoBuffer:length:);
|
||||
[readMonitorInputStream setReadSelector:readSel];
|
||||
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
- (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider:
|
||||
(GTMSessionFetcherBodyStreamProvider)streamProvider {
|
||||
if (!streamProvider) return nil;
|
||||
if (![GTMSessionFetcher isLoggingEnabled]) return streamProvider;
|
||||
|
||||
[self clearLoggedStreamData]; // Clear any previous data.
|
||||
Class monitorClass = NSClassFromString(@"GTMReadMonitorInputStream");
|
||||
if (!monitorClass) {
|
||||
NSString const *str = @"<<Uploaded stream log unavailable without GTMReadMonitorInputStream>>";
|
||||
NSData *stringData = [str dataUsingEncoding:NSUTF8StringEncoding];
|
||||
[self appendLoggedStreamData:stringData];
|
||||
return streamProvider;
|
||||
}
|
||||
GTMSessionFetcherBodyStreamProvider loggedStreamProvider =
|
||||
^(GTMSessionFetcherBodyStreamProviderResponse response) {
|
||||
streamProvider(^(NSInputStream *bodyStream) {
|
||||
bodyStream = [self loggedInputStreamForInputStream:bodyStream];
|
||||
response(bodyStream);
|
||||
});
|
||||
};
|
||||
return loggedStreamProvider;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GTMSessionFetcher (GTMSessionFetcherLoggingUtilities)
|
||||
|
||||
- (void)inputStream:(GTMReadMonitorInputStream *)stream
|
||||
readIntoBuffer:(void *)buffer
|
||||
length:(int64_t)length {
|
||||
// append the captured data
|
||||
NSData *data = [NSData dataWithBytesNoCopy:buffer length:(NSUInteger)length freeWhenDone:NO];
|
||||
[self appendLoggedStreamData:data];
|
||||
}
|
||||
|
||||
#pragma mark Fomatting Utilities
|
||||
|
||||
+ (NSString *)snipSubstringOfString:(NSString *)originalStr
|
||||
betweenStartString:(NSString *)startStr
|
||||
endString:(NSString *)endStr {
|
||||
#if SKIP_GTM_FETCH_LOGGING_SNIPPING
|
||||
return originalStr;
|
||||
#else
|
||||
if (!originalStr) return nil;
|
||||
|
||||
// Find the start string, and replace everything between it
|
||||
// and the end string (or the end of the original string) with "_snip_"
|
||||
NSRange startRange = [originalStr rangeOfString:startStr];
|
||||
if (startRange.location == NSNotFound) return originalStr;
|
||||
|
||||
// We found the start string
|
||||
NSUInteger originalLength = originalStr.length;
|
||||
NSUInteger startOfTarget = NSMaxRange(startRange);
|
||||
NSRange targetAndRest = NSMakeRange(startOfTarget, originalLength - startOfTarget);
|
||||
NSRange endRange = [originalStr rangeOfString:endStr options:0 range:targetAndRest];
|
||||
NSRange replaceRange;
|
||||
if (endRange.location == NSNotFound) {
|
||||
// Found no end marker so replace to end of string
|
||||
replaceRange = targetAndRest;
|
||||
} else {
|
||||
// Replace up to the endStr
|
||||
replaceRange = NSMakeRange(startOfTarget, endRange.location - startOfTarget);
|
||||
}
|
||||
NSString *result = [originalStr stringByReplacingCharactersInRange:replaceRange
|
||||
withString:@"_snip_"];
|
||||
return result;
|
||||
#endif // SKIP_GTM_FETCH_LOGGING_SNIPPING
|
||||
}
|
||||
|
||||
+ (NSString *)headersStringForDictionary:(NSDictionary *)dict {
|
||||
// Format the dictionary in http header style, like
|
||||
// Accept: application/json
|
||||
// Cache-Control: no-cache
|
||||
// Content-Type: application/json; charset=utf-8
|
||||
//
|
||||
// Pad the key names, but not beyond 16 chars, since long custom header
|
||||
// keys just create too much whitespace
|
||||
NSArray *keys = [dict.allKeys sortedArrayUsingSelector:@selector(compare:)];
|
||||
|
||||
NSMutableString *str = [NSMutableString string];
|
||||
for (NSString *key in keys) {
|
||||
NSString *value = [dict valueForKey:key];
|
||||
if ([key isEqual:@"Authorization"]) {
|
||||
// Remove OAuth 1 token
|
||||
value = [[self class] snipSubstringOfString:value
|
||||
betweenStartString:@"oauth_token=\""
|
||||
endString:@"\""];
|
||||
|
||||
// Remove OAuth 2 bearer token (draft 16, and older form)
|
||||
value = [[self class] snipSubstringOfString:value
|
||||
betweenStartString:@"Bearer "
|
||||
endString:@"\n"];
|
||||
value = [[self class] snipSubstringOfString:value
|
||||
betweenStartString:@"OAuth "
|
||||
endString:@"\n"];
|
||||
|
||||
// Remove Google ClientLogin
|
||||
value = [[self class] snipSubstringOfString:value
|
||||
betweenStartString:@"GoogleLogin auth="
|
||||
endString:@"\n"];
|
||||
}
|
||||
[str appendFormat:@" %@: %@\n", key, value];
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif // !STRIP_GTM_FETCH_LOGGING
|
||||
@@ -0,0 +1,210 @@
|
||||
/* Copyright 2014 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// For best performance and convenient usage, fetchers should be generated by a common
|
||||
// GTMSessionFetcherService instance, like
|
||||
//
|
||||
// _fetcherService = [[GTMSessionFetcherService alloc] init];
|
||||
// GTMSessionFetcher* myFirstFetcher = [_fetcherService fetcherWithRequest:request1];
|
||||
// GTMSessionFetcher* mySecondFetcher = [_fetcherService fetcherWithRequest:request2];
|
||||
|
||||
#import "GTMSessionFetcher.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// Notifications.
|
||||
|
||||
// This notification indicates a reusable session has become invalid. It is intended mainly for the
|
||||
// service's unit tests.
|
||||
//
|
||||
// The notification object is the fetcher service.
|
||||
// The invalid session is provided via the userInfo kGTMSessionFetcherServiceSessionKey key.
|
||||
extern NSString *const kGTMSessionFetcherServiceSessionBecameInvalidNotification;
|
||||
extern NSString *const kGTMSessionFetcherServiceSessionKey;
|
||||
|
||||
@interface GTMSessionFetcherService : NSObject <GTMSessionFetcherServiceProtocol>
|
||||
|
||||
// Queues of delayed and running fetchers. Each dictionary contains arrays
|
||||
// of GTMSessionFetcher *fetchers, keyed by NSString *host
|
||||
@property(atomic, strong, readonly, nullable)
|
||||
NSDictionary<NSString *, NSArray *> *delayedFetchersByHost;
|
||||
@property(atomic, strong, readonly, nullable)
|
||||
NSDictionary<NSString *, NSArray *> *runningFetchersByHost;
|
||||
|
||||
// A max value of 0 means no fetchers should be delayed.
|
||||
// The default limit is 10 simultaneous fetchers targeting each host.
|
||||
// This does not apply to fetchers whose useBackgroundSession property is YES. Since services are
|
||||
// not resurrected on an app relaunch, delayed fetchers would effectively be abandoned.
|
||||
@property(atomic, assign) NSUInteger maxRunningFetchersPerHost;
|
||||
|
||||
// Properties to be applied to each fetcher; see GTMSessionFetcher.h for descriptions
|
||||
@property(atomic, strong, nullable) NSURLSessionConfiguration *configuration;
|
||||
@property(atomic, copy, nullable) GTMSessionFetcherConfigurationBlock configurationBlock;
|
||||
@property(atomic, strong, nullable) NSHTTPCookieStorage *cookieStorage;
|
||||
@property(atomic, strong, null_resettable) dispatch_queue_t callbackQueue;
|
||||
@property(atomic, copy, nullable) GTMSessionFetcherChallengeBlock challengeBlock;
|
||||
@property(atomic, strong, nullable) NSURLCredential *credential;
|
||||
@property(atomic, strong) NSURLCredential *proxyCredential;
|
||||
@property(atomic, copy, nullable) NSArray<NSString *> *allowedInsecureSchemes;
|
||||
@property(atomic, assign) BOOL allowLocalhostRequest;
|
||||
@property(atomic, assign) BOOL allowInvalidServerCertificates;
|
||||
@property(atomic, assign, getter=isRetryEnabled) BOOL retryEnabled;
|
||||
@property(atomic, copy, nullable) GTMSessionFetcherRetryBlock retryBlock;
|
||||
@property(atomic, assign) NSTimeInterval maxRetryInterval;
|
||||
@property(atomic, assign) NSTimeInterval minRetryInterval;
|
||||
@property(atomic, copy, nullable) NSDictionary<NSString *, id> *properties;
|
||||
@property(atomic, copy, nullable)
|
||||
GTMSessionFetcherMetricsCollectionBlock metricsCollectionBlock API_AVAILABLE(
|
||||
ios(10.0), macosx(10.12), tvos(10.0), watchos(6.0));
|
||||
|
||||
#if GTM_BACKGROUND_TASK_FETCHING
|
||||
@property(atomic, assign) BOOL skipBackgroundTask;
|
||||
#endif
|
||||
|
||||
// A default useragent of GTMFetcherStandardUserAgentString(nil) will be given to each fetcher
|
||||
// created by this service unless the request already has a user-agent header set.
|
||||
// This default will be added starting with builds with the SDKs for OS X 10.11 and iOS 9.
|
||||
//
|
||||
// To use the configuration's default user agent, set this property to nil.
|
||||
@property(atomic, copy, nullable) NSString *userAgent;
|
||||
|
||||
// The authorizer to attach to the created fetchers. If a specific fetcher should
|
||||
// not authorize its requests, the fetcher's authorizer property may be set to nil
|
||||
// before the fetch begins.
|
||||
@property(atomic, strong, nullable) id<GTMFetcherAuthorizationProtocol> authorizer;
|
||||
|
||||
// Delegate queue used by the session when calling back to the fetcher. The default
|
||||
// is the main queue. Changing this does not affect the queue used to call back to the
|
||||
// application; that is specified by the callbackQueue property above.
|
||||
@property(atomic, strong, null_resettable) NSOperationQueue *sessionDelegateQueue;
|
||||
|
||||
// When enabled, indicates the same session should be used by subsequent fetchers.
|
||||
//
|
||||
// This is enabled by default.
|
||||
@property(atomic, assign) BOOL reuseSession;
|
||||
|
||||
// Sets the delay until an unused session is invalidated.
|
||||
// The default interval is 60 seconds.
|
||||
//
|
||||
// If the interval is set to 0, then any reused session is not invalidated except by
|
||||
// explicitly invoking -resetSession. Be aware that setting the interval to 0 thus
|
||||
// causes the session's delegate to be retained until the session is explicitly reset.
|
||||
@property(atomic, assign) NSTimeInterval unusedSessionTimeout;
|
||||
|
||||
// If shouldReuseSession is enabled, this will force creation of a new session when future
|
||||
// fetchers begin.
|
||||
- (void)resetSession;
|
||||
|
||||
// Create a fetcher
|
||||
//
|
||||
// These methods will return a fetcher. If successfully created, the connection
|
||||
// will hold a strong reference to it for the life of the connection as well.
|
||||
// So the caller doesn't have to hold onto the fetcher explicitly unless they
|
||||
// want to be able to monitor or cancel it.
|
||||
- (GTMSessionFetcher *)fetcherWithRequest:(NSURLRequest *)request;
|
||||
- (GTMSessionFetcher *)fetcherWithURL:(NSURL *)requestURL;
|
||||
- (GTMSessionFetcher *)fetcherWithURLString:(NSString *)requestURLString;
|
||||
|
||||
// Common method for fetcher creation.
|
||||
//
|
||||
// -fetcherWithRequest:fetcherClass: may be overridden to customize creation of
|
||||
// fetchers. This is the ONLY method in the GTMSessionFetcher library intended to
|
||||
// be overridden.
|
||||
- (id)fetcherWithRequest:(NSURLRequest *)request fetcherClass:(Class)fetcherClass;
|
||||
|
||||
- (BOOL)isDelayingFetcher:(GTMSessionFetcher *)fetcher;
|
||||
|
||||
- (NSUInteger)numberOfFetchers; // running + delayed fetchers
|
||||
- (NSUInteger)numberOfRunningFetchers;
|
||||
- (NSUInteger)numberOfDelayedFetchers;
|
||||
|
||||
// Return a list of all running or delayed fetchers. This includes fetchers created
|
||||
// by the service which have been started and have not yet stopped.
|
||||
//
|
||||
// Returns an array of fetcher objects, or nil if none.
|
||||
- (nullable NSArray<GTMSessionFetcher *> *)issuedFetchers;
|
||||
|
||||
// Search for running or delayed fetchers with the specified URL.
|
||||
//
|
||||
// Returns an array of fetcher objects found, or nil if none found.
|
||||
- (nullable NSArray<GTMSessionFetcher *> *)issuedFetchersWithRequestURL:(NSURL *)requestURL;
|
||||
|
||||
- (void)stopAllFetchers;
|
||||
|
||||
// Holds a weak reference to `decorator`. When creating a fetcher via
|
||||
// `-fetcherWithRequest:fetcherClass:`, each registered `decorator` can inspect and potentially
|
||||
// change the fetcher's request before it starts. Decorators are invoked in the order in which
|
||||
// they are passed to this method.
|
||||
- (void)addDecorator:(id<GTMFetcherDecoratorProtocol>)decorator;
|
||||
|
||||
// Removes a `decorator` previously passed to `-removeDecorator:`.
|
||||
- (void)removeDecorator:(id<GTMFetcherDecoratorProtocol>)decorator;
|
||||
|
||||
// Methods for use by the fetcher class only.
|
||||
- (nullable NSURLSession *)session;
|
||||
- (nullable NSURLSession *)sessionForFetcherCreation;
|
||||
- (nullable id<NSURLSessionDelegate>)sessionDelegate;
|
||||
- (nullable NSDate *)stoppedAllFetchersDate;
|
||||
|
||||
// The testBlock can inspect its fetcher parameter's request property to
|
||||
// determine which fetcher is being faked.
|
||||
@property(atomic, copy, nullable) GTMSessionFetcherTestBlock testBlock;
|
||||
|
||||
@end
|
||||
|
||||
@interface GTMSessionFetcherService (TestingSupport)
|
||||
|
||||
// Convenience methods to create a fetcher service for testing.
|
||||
//
|
||||
// Fetchers generated by this mock fetcher service will not perform any
|
||||
// network operation, but will invoke callbacks and provide the supplied data
|
||||
// or error to the completion handler.
|
||||
//
|
||||
// You can make more customized mocks by setting the test block property of the service
|
||||
// or fetcher; the test block can inspect the fetcher's request or other properties.
|
||||
//
|
||||
// See the description of the testBlock property below.
|
||||
+ (instancetype)mockFetcherServiceWithFakedData:(nullable NSData *)fakedDataOrNil
|
||||
fakedError:(nullable NSError *)fakedErrorOrNil;
|
||||
+ (instancetype)mockFetcherServiceWithFakedData:(nullable NSData *)fakedDataOrNil
|
||||
fakedResponse:(NSHTTPURLResponse *)fakedResponse
|
||||
fakedError:(nullable NSError *)fakedErrorOrNil;
|
||||
|
||||
// DEPRECATED: Callers should use XCTestExpectation instead.
|
||||
//
|
||||
// Spin the run loop and discard events (or, if not on the main thread, just sleep the thread)
|
||||
// until all running and delayed fetchers have completed.
|
||||
//
|
||||
// This is only for use in testing or in tools without a user interface.
|
||||
//
|
||||
// Synchronous fetches should never be done by shipping apps; they are
|
||||
// sufficient reason for rejection from the app store.
|
||||
//
|
||||
// Returns NO if timed out.
|
||||
- (BOOL)waitForCompletionOfAllFetchersWithTimeout:(NSTimeInterval)timeoutInSeconds
|
||||
__deprecated_msg("Use XCTestExpectation instead");
|
||||
|
||||
@end
|
||||
|
||||
@interface GTMSessionFetcherService (BackwardsCompatibilityOnly)
|
||||
|
||||
// Clients using GTMSessionFetcher should set the cookie storage explicitly themselves;
|
||||
// this property is deprecated and will be removed soon.
|
||||
@property(atomic, assign) NSInteger cookieStorageMethod __deprecated_msg(
|
||||
"Create an NSHTTPCookieStorage and set .cookieStorage directly.");
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
+1383
File diff suppressed because it is too large
Load Diff
+173
@@ -0,0 +1,173 @@
|
||||
/* Copyright 2014 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// GTMSessionUploadFetcher implements Google's resumable upload protocol.
|
||||
|
||||
//
|
||||
// This subclass of GTMSessionFetcher simulates the series of fetches
|
||||
// needed for chunked upload as a single fetch operation.
|
||||
//
|
||||
// Protocol document: TBD
|
||||
//
|
||||
// To the client, the only fetcher that exists is this class; the subsidiary
|
||||
// fetchers needed for uploading chunks are not visible (though the most recent
|
||||
// chunk fetcher may be accessed via the -activeFetcher or -chunkFetcher methods, and
|
||||
// -responseHeaders and -statusCode reflect results from the most recent chunk
|
||||
// fetcher.)
|
||||
//
|
||||
// Chunk fetchers are discarded as soon as they have completed.
|
||||
//
|
||||
// The protocol also allows for a cancellation notification request to be sent to the
|
||||
// server to allow discarding of the currently uploaded data and this will be sent
|
||||
// automatically upon calling stopFetching if the upload has already started.
|
||||
//
|
||||
// Note: Unlike the fetcher superclass, the methods of GTMSessionUploadFetcher should
|
||||
// only be used from the main thread until further work is done to make this subclass
|
||||
// thread-safe.
|
||||
|
||||
#import "GTMSessionFetcher.h"
|
||||
#import "GTMSessionFetcherService.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// The value to use for file size parameters when the file size is not yet known.
|
||||
extern int64_t const kGTMSessionUploadFetcherUnknownFileSize;
|
||||
|
||||
// Unless an application knows it needs a smaller chunk size, it should use the standard
|
||||
// chunk size, which sends the entire file as a single chunk to minimize upload overhead.
|
||||
// Setting an explicit chunk size that comfortably fits in memory is advisable for large
|
||||
// uploads.
|
||||
extern int64_t const kGTMSessionUploadFetcherStandardChunkSize;
|
||||
|
||||
// When uploading requires data buffer allocations (such as uploading from an NSData or
|
||||
// an NSFileHandle) this is the maximum buffer size that will be created by the fetcher.
|
||||
extern int64_t const kGTMSessionUploadFetcherMaximumDemandBufferSize;
|
||||
|
||||
// Notification that the upload location URL was provided by the server.
|
||||
extern NSString *const kGTMSessionFetcherUploadLocationObtainedNotification;
|
||||
|
||||
// Block to provide data during uploads.
|
||||
//
|
||||
// Response data may be allocated with dataWithBytesNoCopy:length:freeWhenDone: for efficiency,
|
||||
// and released after the response block returns.
|
||||
//
|
||||
// If the length of the file being uploaded is unknown or already set, send
|
||||
// kGTMSessionUploadFetcherUnknownFileSize for |fullUploadLength|. Otherwise, set |fullUploadLength|
|
||||
// to its proper value.
|
||||
//
|
||||
// Pass nil as the data (and optionally an NSError) for a failure.
|
||||
typedef void (^GTMSessionUploadFetcherDataProviderResponse)(NSData *_Nullable data,
|
||||
int64_t fullUploadLength,
|
||||
NSError *_Nullable error);
|
||||
// Do not call the response with an NSData object with less data than the requested length unless
|
||||
// you are passing the fullUploadLength to the fetcher for the first time and it is the last chunk
|
||||
// of data in the file being uploaded.
|
||||
typedef void (^GTMSessionUploadFetcherDataProvider)(
|
||||
int64_t offset, int64_t length, GTMSessionUploadFetcherDataProviderResponse response);
|
||||
|
||||
// Block to be notified about the final status of the cancellation request started in stopFetching.
|
||||
//
|
||||
// |fetcher| will be the cancel request that was sent to the server, or nil if stopFetching is not
|
||||
// going to send a cancel request. If |fetcher| is provided, the other parameters correspond to the
|
||||
// completion handler of the cancellation request fetcher.
|
||||
typedef void (^GTMSessionUploadFetcherCancellationHandler)(GTMSessionFetcher *_Nullable fetcher,
|
||||
NSData *_Nullable data,
|
||||
NSError *_Nullable error);
|
||||
|
||||
@interface GTMSessionUploadFetcher : GTMSessionFetcher
|
||||
|
||||
// Create an upload fetcher specifying either the request or the resume location URL,
|
||||
// then set an upload data source using one of these:
|
||||
//
|
||||
// setUploadFileURL:
|
||||
// setUploadDataLength:provider:
|
||||
// setUploadFileHandle:
|
||||
// setUploadData:
|
||||
|
||||
+ (instancetype)uploadFetcherWithRequest:(NSURLRequest *)request
|
||||
uploadMIMEType:(NSString *)uploadMIMEType
|
||||
chunkSize:(int64_t)chunkSize
|
||||
fetcherService:(nullable GTMSessionFetcherService *)fetcherServiceOrNil;
|
||||
|
||||
// Allows cellular access.
|
||||
+ (instancetype)uploadFetcherWithLocation:(nullable NSURL *)uploadLocationURL
|
||||
uploadMIMEType:(NSString *)uploadMIMEType
|
||||
chunkSize:(int64_t)chunkSize
|
||||
fetcherService:(nullable GTMSessionFetcherService *)fetcherServiceOrNil;
|
||||
|
||||
+ (instancetype)uploadFetcherWithLocation:(nullable NSURL *)uploadLocationURL
|
||||
uploadMIMEType:(NSString *)uploadMIMEType
|
||||
chunkSize:(int64_t)chunkSize
|
||||
allowsCellularAccess:(BOOL)allowsCellularAccess
|
||||
fetcherService:(nullable GTMSessionFetcherService *)fetcherServiceOrNil;
|
||||
|
||||
// Allows dataProviders for files of unknown length. Pass kGTMSessionUploadFetcherUnknownFileSize as
|
||||
// |fullLength| if the length is unknown.
|
||||
- (void)setUploadDataLength:(int64_t)fullLength
|
||||
provider:(nullable GTMSessionUploadFetcherDataProvider)block;
|
||||
|
||||
+ (NSArray *)uploadFetchersForBackgroundSessions;
|
||||
+ (nullable instancetype)uploadFetcherForSessionIdentifier:(NSString *)sessionIdentifier;
|
||||
|
||||
- (void)pauseFetching;
|
||||
- (void)resumeFetching;
|
||||
- (BOOL)isPaused;
|
||||
|
||||
@property(atomic, strong, nullable) NSURL *uploadLocationURL;
|
||||
@property(atomic, strong, nullable) NSData *uploadData;
|
||||
@property(atomic, strong, nullable) NSURL *uploadFileURL;
|
||||
@property(atomic, strong, nullable) NSFileHandle *uploadFileHandle;
|
||||
@property(atomic, copy, readonly, nullable) GTMSessionUploadFetcherDataProvider uploadDataProvider;
|
||||
@property(atomic, copy) NSString *uploadMIMEType;
|
||||
@property(atomic, readonly, assign) int64_t chunkSize;
|
||||
@property(atomic, readonly, assign) int64_t currentOffset;
|
||||
// Reflects the original NSURLRequest's @c allowCellularAccess property.
|
||||
@property(atomic, readonly, assign) BOOL allowsCellularAccess;
|
||||
|
||||
// The fetcher for the current data chunk, if any
|
||||
@property(atomic, strong, nullable) GTMSessionFetcher *chunkFetcher;
|
||||
|
||||
// The active fetcher is the current chunk fetcher, or the upload fetcher itself
|
||||
// if no chunk fetcher has yet been created.
|
||||
@property(atomic, readonly) GTMSessionFetcher *activeFetcher;
|
||||
|
||||
// The last request made by an active fetcher. Useful for testing.
|
||||
@property(atomic, readonly, nullable) NSURLRequest *lastChunkRequest;
|
||||
|
||||
// The status code from the most recently-completed fetch.
|
||||
@property(atomic, assign) NSInteger statusCode;
|
||||
|
||||
// Invoked as part of the stop fetching process. Invoked immediately if there is no upload in
|
||||
// progress, otherwise invoked with the results of the attempt to notify the server that the
|
||||
// upload will not continue.
|
||||
//
|
||||
// Unlike other callbacks, since this is related specifically to the stopFetching flow it is not
|
||||
// cleared by stopFetching. It will instead clear itself after it is invoked or if the completion
|
||||
// has occured before stopFetching is called.
|
||||
@property(atomic, copy, nullable) GTMSessionUploadFetcherCancellationHandler cancellationHandler;
|
||||
|
||||
// Exposed for testing only.
|
||||
@property(atomic, readonly, nullable) dispatch_queue_t delegateCallbackQueue;
|
||||
@property(atomic, readonly, nullable) GTMSessionFetcherCompletionHandler delegateCompletionHandler;
|
||||
|
||||
@end
|
||||
|
||||
@interface GTMSessionFetcher (GTMSessionUploadFetcherMethods)
|
||||
|
||||
@property(readonly, nullable) GTMSessionUploadFetcher *parentUploadFetcher;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
+2001
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user