Proyectos de Subversion Iphone Microlearning

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
/*
2
 * Copyright 2020 Google LLC
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *      http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
 
17
#import "GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTUploadOperation.h"
18
 
19
#if __has_include(<FBLPromises/FBLPromises.h>)
20
#import <FBLPromises/FBLPromises.h>
21
#else
22
#import "FBLPromises.h"
23
#endif
24
 
25
#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORPlatform.h"
26
#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORRegistrar.h"
27
#import "GoogleDataTransport/GDTCORLibrary/Internal/GDTCORStorageProtocol.h"
28
#import "GoogleDataTransport/GDTCORLibrary/Private/GDTCORUploadBatch.h"
29
#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCORConsoleLogger.h"
30
#import "GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport/GDTCOREvent.h"
31
 
32
#import <nanopb/pb.h>
33
#import <nanopb/pb_decode.h>
34
#import <nanopb/pb_encode.h>
35
 
36
#import <GoogleUtilities/GULURLSessionDataResponse.h>
37
#import <GoogleUtilities/NSURLSession+GULPromises.h>
38
#import "GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTCompressionHelper.h"
39
#import "GoogleDataTransport/GDTCCTLibrary/Private/GDTCCTNanopbHelpers.h"
40
 
41
#import "GoogleDataTransport/GDTCCTLibrary/Protogen/nanopb/cct.nanopb.h"
42
 
43
NS_ASSUME_NONNULL_BEGIN
44
 
45
#ifdef GDTCOR_VERSION
46
#define STR(x) STR_EXPAND(x)
47
#define STR_EXPAND(x) #x
48
static NSString *const kGDTCCTSupportSDKVersion = @STR(GDTCOR_VERSION);
49
#else
50
static NSString *const kGDTCCTSupportSDKVersion = @"UNKNOWN";
51
#endif  // GDTCOR_VERSION
52
 
53
typedef void (^GDTCCTUploaderURLTaskCompletion)(NSNumber *batchID,
54
                                                NSSet<GDTCOREvent *> *_Nullable events,
55
                                                NSData *_Nullable data,
56
                                                NSURLResponse *_Nullable response,
57
                                                NSError *_Nullable error);
58
 
59
typedef void (^GDTCCTUploaderEventBatchBlock)(NSNumber *_Nullable batchID,
60
                                              NSSet<GDTCOREvent *> *_Nullable events);
61
 
62
@interface GDTCCTUploadOperation () <NSURLSessionDelegate>
63
 
64
/// The properties to store parameters passed in the initializer. See the initialized docs for
65
/// details.
66
@property(nonatomic, readonly) GDTCORTarget target;
67
@property(nonatomic, readonly) GDTCORUploadConditions conditions;
68
@property(nonatomic, readonly) NSURL *uploadURL;
69
@property(nonatomic, readonly) id<GDTCORStoragePromiseProtocol> storage;
70
@property(nonatomic, readonly) id<GDTCCTUploadMetadataProvider> metadataProvider;
71
 
72
/** The URL session that will attempt upload. */
73
@property(nonatomic) NSURLSession *uploaderSession;
74
 
75
/// NSOperation state properties implementation.
76
@property(nonatomic, readwrite, getter=isExecuting) BOOL executing;
77
@property(nonatomic, readwrite, getter=isFinished) BOOL finished;
78
 
79
@property(nonatomic, readwrite) BOOL uploadAttempted;
80
 
81
@end
82
 
83
@implementation GDTCCTUploadOperation
84
 
85
- (instancetype)initWithTarget:(GDTCORTarget)target
86
                    conditions:(GDTCORUploadConditions)conditions
87
                     uploadURL:(NSURL *)uploadURL
88
                         queue:(dispatch_queue_t)queue
89
                       storage:(id<GDTCORStoragePromiseProtocol>)storage
90
              metadataProvider:(id<GDTCCTUploadMetadataProvider>)metadataProvider {
91
  self = [super init];
92
  if (self) {
93
    _uploaderQueue = queue;
94
    _target = target;
95
    _conditions = conditions;
96
    _uploadURL = uploadURL;
97
    _storage = storage;
98
    _metadataProvider = metadataProvider;
99
  }
100
  return self;
101
}
102
 
103
- (NSURLSession *)uploaderSessionCreateIfNeeded {
104
  if (_uploaderSession == nil) {
105
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
106
    _uploaderSession = [NSURLSession sessionWithConfiguration:config
107
                                                     delegate:self
108
                                                delegateQueue:nil];
109
  }
110
  return _uploaderSession;
111
}
112
 
113
- (void)uploadTarget:(GDTCORTarget)target withConditions:(GDTCORUploadConditions)conditions {
114
  __block GDTCORBackgroundIdentifier backgroundTaskID = GDTCORBackgroundIdentifierInvalid;
115
 
116
  dispatch_block_t backgroundTaskCompletion = ^{
117
    // End the background task if there was one.
118
    if (backgroundTaskID != GDTCORBackgroundIdentifierInvalid) {
119
      [[GDTCORApplication sharedApplication] endBackgroundTask:backgroundTaskID];
120
      backgroundTaskID = GDTCORBackgroundIdentifierInvalid;
121
    }
122
  };
123
 
124
  backgroundTaskID = [[GDTCORApplication sharedApplication]
125
      beginBackgroundTaskWithName:@"GDTCCTUploader-upload"
126
                expirationHandler:^{
127
                  if (backgroundTaskID != GDTCORBackgroundIdentifierInvalid) {
128
                    // Cancel the upload and complete delivery.
129
                    [self.currentTask cancel];
130
 
131
                    // End the background task.
132
                    backgroundTaskCompletion();
133
                  }
134
                }];
135
 
136
  id<GDTCORStoragePromiseProtocol> storage = self.storage;
137
 
138
  // 1. Check if the conditions for the target are suitable.
139
  [self isReadyToUploadTarget:target conditions:conditions]
140
      .thenOn(self.uploaderQueue,
141
              ^id(id result) {
142
                // 2. Remove previously attempted batches
143
                return [storage removeAllBatchesForTarget:target deleteEvents:NO];
144
              })
145
      .thenOn(self.uploaderQueue,
146
              ^FBLPromise<NSNumber *> *(id result) {
147
                // There may be a big amount of events stored, so creating a batch may be an
148
                // expensive operation.
149
 
150
                // 3. Do a lightweight check if there are any events for the target first to
151
                // finish early if there are no.
152
                return [storage hasEventsForTarget:target];
153
              })
154
      .validateOn(self.uploaderQueue,
155
                  ^BOOL(NSNumber *hasEvents) {
156
                    // Stop operation if there are no events to upload.
157
                    return hasEvents.boolValue;
158
                  })
159
      .thenOn(self.uploaderQueue,
160
              ^FBLPromise<GDTCORUploadBatch *> *(id result) {
161
                if (self.isCancelled) {
162
                  return nil;
163
                }
164
 
165
                // 4. Fetch events to upload.
166
                GDTCORStorageEventSelector *eventSelector = [self eventSelectorTarget:target
167
                                                                       withConditions:conditions];
168
                return [storage batchWithEventSelector:eventSelector
169
                                       batchExpiration:[NSDate dateWithTimeIntervalSinceNow:600]];
170
              })
171
      .validateOn(self.uploaderQueue,
172
                  ^BOOL(GDTCORUploadBatch *batch) {
173
                    // 5. Validate batch.
174
                    return batch.batchID != nil && batch.events.count > 0;
175
                  })
176
      .thenOn(self.uploaderQueue,
177
              ^FBLPromise *(GDTCORUploadBatch *batch) {
178
                // A non-empty batch has been created, consider it as an upload attempt.
179
                self.uploadAttempted = YES;
180
 
181
                // 6. Perform upload URL request.
182
                return [self sendURLRequestWithBatch:batch target:target storage:storage];
183
              })
184
      .thenOn(self.uploaderQueue,
185
              ^id(id result) {
186
                // 7. Finish operation.
187
                [self finishOperation];
188
                backgroundTaskCompletion();
189
                return nil;
190
              })
191
      .catchOn(self.uploaderQueue, ^(NSError *error) {
192
        // TODO: Maybe report the error to the client.
193
        [self finishOperation];
194
        backgroundTaskCompletion();
195
      });
196
}
197
 
198
#pragma mark - Upload implementation details
199
 
200
/** Sends URL request to upload the provided batch and handle the response. */
201
- (FBLPromise<NSNull *> *)sendURLRequestWithBatch:(GDTCORUploadBatch *)batch
202
                                           target:(GDTCORTarget)target
203
                                          storage:(id<GDTCORStoragePromiseProtocol>)storage {
204
  NSNumber *batchID = batch.batchID;
205
 
206
  // 1. Send URL request.
207
  return [self sendURLRequestWithBatch:batch target:target]
208
      .thenOn(
209
          self.uploaderQueue,
210
          ^FBLPromise<NSNull *> *(GULURLSessionDataResponse *response) {
211
            // 2. Parse response and update the next upload time if can.
212
            [self updateNextUploadTimeWithResponse:response forTarget:target];
213
 
214
            // 3. Cleanup batch.
215
 
216
            // Only retry if one of these codes is returned:
217
            // 429 - Too many requests;
218
            // 5xx - Server errors.
219
            NSInteger statusCode = response.HTTPResponse.statusCode;
220
            if (statusCode == 429 || (statusCode >= 500 && statusCode < 600)) {
221
              // Move the events back to the main storage to be uploaded on the next attempt.
222
              return [storage removeBatchWithID:batchID deleteEvents:NO];
223
            } else {
224
              if (statusCode >= 200 && statusCode <= 300) {
225
                GDTCORLogDebug(@"CCT: batch %@ delivered", batchID);
226
              } else {
227
                GDTCORLogDebug(
228
                    @"CCT: batch %@ was rejected by the server and will be deleted with all events",
229
                    batchID);
230
              }
231
 
232
              // The events are either delivered or unrecoverable broken, so remove the batch with
233
              // events.
234
              return [storage removeBatchWithID:batch.batchID deleteEvents:YES];
235
            }
236
          })
237
      .recoverOn(self.uploaderQueue, ^id(NSError *error) {
238
        // In the case of a network error move the events back to the main storage to be uploaded on
239
        // the next attempt.
240
        return [storage removeBatchWithID:batchID deleteEvents:NO];
241
      });
242
}
243
 
244
/** Composes and sends URL request. */
245
- (FBLPromise<GULURLSessionDataResponse *> *)sendURLRequestWithBatch:(GDTCORUploadBatch *)batch
246
                                                              target:(GDTCORTarget)target {
247
  return [FBLPromise
248
             onQueue:self.uploaderQueue
249
                  do:^NSURLRequest * {
250
                    // 1. Prepare URL request.
251
                    NSData *requestProtoData = [self constructRequestProtoWithEvents:batch.events];
252
                    NSData *gzippedData = [GDTCCTCompressionHelper gzippedData:requestProtoData];
253
                    BOOL usingGzipData =
254
                        gzippedData != nil && gzippedData.length < requestProtoData.length;
255
                    NSData *dataToSend = usingGzipData ? gzippedData : requestProtoData;
256
                    NSURLRequest *request = [self constructRequestWithURL:self.uploadURL
257
                                                                forTarget:target
258
                                                                     data:dataToSend];
259
                    GDTCORLogDebug(@"CTT: request containing %lu events for batch: %@ for target: "
260
                                   @"%ld created: %@",
261
                                   (unsigned long)batch.events.count, batch.batchID, (long)target,
262
                                   request);
263
                    return request;
264
                  }]
265
      .thenOn(self.uploaderQueue,
266
              ^FBLPromise<GULURLSessionDataResponse *> *(NSURLRequest *request) {
267
                // 2. Send URL request.
268
                return
269
                    [[self uploaderSessionCreateIfNeeded] gul_dataTaskPromiseWithRequest:request];
270
              })
271
      .thenOn(self.uploaderQueue,
272
              ^GULURLSessionDataResponse *(GULURLSessionDataResponse *response) {
273
                // Invalidate session to release the delegate (which is `self`) to break the retain
274
                // cycle.
275
                [self.uploaderSession finishTasksAndInvalidate];
276
                return response;
277
              })
278
      .recoverOn(self.uploaderQueue, ^id(NSError *error) {
279
        // Invalidate session to release the delegate (which is `self`) to break the retain cycle.
280
        [self.uploaderSession finishTasksAndInvalidate];
281
        // Re-throw the error.
282
        return error;
283
      });
284
}
285
 
286
/** Parses server response and update next upload time for the specified target based on it. */
287
- (void)updateNextUploadTimeWithResponse:(GULURLSessionDataResponse *)response
288
                               forTarget:(GDTCORTarget)target {
289
  GDTCORClock *futureUploadTime;
290
  if (response.HTTPBody) {
291
    NSError *decodingError;
292
    gdt_cct_LogResponse logResponse = GDTCCTDecodeLogResponse(response.HTTPBody, &decodingError);
293
    if (!decodingError && logResponse.has_next_request_wait_millis) {
294
      GDTCORLogDebug(@"CCT: The backend responded asking to not upload for %lld millis from now.",
295
                     logResponse.next_request_wait_millis);
296
      futureUploadTime =
297
          [GDTCORClock clockSnapshotInTheFuture:logResponse.next_request_wait_millis];
298
    } else if (decodingError) {
299
      GDTCORLogDebug(@"There was a response decoding error: %@", decodingError);
300
    }
301
    pb_release(gdt_cct_LogResponse_fields, &logResponse);
302
  }
303
 
304
  // If no futureUploadTime was parsed from the response body, then check
305
  // [Retry-After](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) header.
306
  if (!futureUploadTime) {
307
    NSString *retryAfterHeader = response.HTTPResponse.allHeaderFields[@"Retry-After"];
308
    if (retryAfterHeader.length > 0) {
309
      NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
310
      formatter.numberStyle = NSNumberFormatterDecimalStyle;
311
      NSNumber *retryAfterSeconds = [formatter numberFromString:retryAfterHeader];
312
      if (retryAfterSeconds != nil) {
313
        uint64_t retryAfterMillis = retryAfterSeconds.unsignedIntegerValue * 1000u;
314
        futureUploadTime = [GDTCORClock clockSnapshotInTheFuture:retryAfterMillis];
315
      }
316
    }
317
  }
318
 
319
  if (!futureUploadTime) {
320
    GDTCORLogDebug(@"%@", @"CCT: The backend response failed to parse, so the next request "
321
                          @"won't occur until 15 minutes from now");
322
    // 15 minutes from now.
323
    futureUploadTime = [GDTCORClock clockSnapshotInTheFuture:15 * 60 * 1000];
324
  }
325
 
326
  [self.metadataProvider setNextUploadTime:futureUploadTime forTarget:target];
327
}
328
 
329
#pragma mark - Private helper methods
330
 
331
/** @return A resolved promise if is ready and a rejected promise if not. */
332
- (FBLPromise<NSNull *> *)isReadyToUploadTarget:(GDTCORTarget)target
333
                                     conditions:(GDTCORUploadConditions)conditions {
334
  FBLPromise<NSNull *> *promise = [FBLPromise pendingPromise];
335
  if ([self readyToUploadTarget:target conditions:conditions]) {
336
    [promise fulfill:[NSNull null]];
337
  } else {
338
    NSString *reason =
339
        [NSString stringWithFormat:@"Target %ld is not ready to upload with condition: %ld",
340
                                   (long)target, (long)conditions];
341
    [promise reject:[self genericRejectedPromiseErrorWithReason:reason]];
342
  }
343
  return promise;
344
}
345
 
346
// TODO: Move to a separate class/extension/file when needed in other files.
347
/** Returns an error object with the specified failure reason. */
348
- (NSError *)genericRejectedPromiseErrorWithReason:(NSString *)reason {
349
  return [NSError errorWithDomain:@"GDTCCTUploader"
350
                             code:-1
351
                         userInfo:@{NSLocalizedFailureReasonErrorKey : reason}];
352
}
353
 
354
/** Returns if the specified target is ready to be uploaded based on the specified conditions. */
355
- (BOOL)readyToUploadTarget:(GDTCORTarget)target conditions:(GDTCORUploadConditions)conditions {
356
  // Not ready to upload with no network connection.
357
  // TODO: Reconsider using reachability to prevent an upload attempt.
358
  // See https://developer.apple.com/videos/play/wwdc2019/712/ (49:40) for more details.
359
  if (conditions & GDTCORUploadConditionNoNetwork) {
360
    GDTCORLogDebug(@"%@", @"CCT: Not ready to upload without a network connection.");
361
    return NO;
362
  }
363
 
364
  // Upload events with no additional conditions if high priority.
365
  if ((conditions & GDTCORUploadConditionHighPriority) == GDTCORUploadConditionHighPriority) {
366
    GDTCORLogDebug(@"%@", @"CCT: a high priority event is allowing an upload");
367
    return YES;
368
  }
369
 
370
  // Check next upload time for the target.
371
  BOOL isAfterNextUploadTime = YES;
372
  GDTCORClock *nextUploadTime = [self.metadataProvider nextUploadTimeForTarget:target];
373
  if (nextUploadTime) {
374
    isAfterNextUploadTime = [[GDTCORClock snapshot] isAfter:nextUploadTime];
375
  }
376
 
377
  if (isAfterNextUploadTime) {
378
    GDTCORLogDebug(@"CCT: can upload to target %ld because the request wait time has transpired",
379
                   (long)target);
380
  } else {
381
    GDTCORLogDebug(@"CCT: can't upload to target %ld because the backend asked to wait",
382
                   (long)target);
383
  }
384
 
385
  return isAfterNextUploadTime;
386
}
387
 
388
/** Constructs data given an upload package.
389
 *
390
 * @param events The events used to construct the request proto bytes.
391
 * @return Proto bytes representing a gdt_cct_LogRequest object.
392
 */
393
- (nonnull NSData *)constructRequestProtoWithEvents:(NSSet<GDTCOREvent *> *)events {
394
  // Segment the log events by log type.
395
  NSMutableDictionary<NSString *, NSMutableSet<GDTCOREvent *> *> *logMappingIDToLogSet =
396
      [[NSMutableDictionary alloc] init];
397
  [events enumerateObjectsUsingBlock:^(GDTCOREvent *_Nonnull event, BOOL *_Nonnull stop) {
398
    NSMutableSet *logSet = logMappingIDToLogSet[event.mappingID];
399
    logSet = logSet ? logSet : [[NSMutableSet alloc] init];
400
    [logSet addObject:event];
401
    logMappingIDToLogSet[event.mappingID] = logSet;
402
  }];
403
 
404
  gdt_cct_BatchedLogRequest batchedLogRequest =
405
      GDTCCTConstructBatchedLogRequest(logMappingIDToLogSet);
406
 
407
  NSData *data = GDTCCTEncodeBatchedLogRequest(&batchedLogRequest);
408
  pb_release(gdt_cct_BatchedLogRequest_fields, &batchedLogRequest);
409
  return data ? data : [[NSData alloc] init];
410
}
411
 
412
/** Constructs a request to the given URL and target with the specified request body data.
413
 *
414
 * @param target The target backend to send the request to.
415
 * @param data The request body data.
416
 * @return A new NSURLRequest ready to be sent to FLL.
417
 */
418
- (nullable NSURLRequest *)constructRequestWithURL:(NSURL *)URL
419
                                         forTarget:(GDTCORTarget)target
420
                                              data:(NSData *)data {
421
  if (data == nil || data.length == 0) {
422
    GDTCORLogDebug(@"There was no data to construct a request for target %ld.", (long)target);
423
    return nil;
424
  }
425
 
426
  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
427
  NSString *targetString;
428
  switch (target) {
429
    case kGDTCORTargetCCT:
430
      targetString = @"cct";
431
      break;
432
 
433
    case kGDTCORTargetFLL:
434
      targetString = @"fll";
435
      break;
436
 
437
    case kGDTCORTargetCSH:
438
      targetString = @"csh";
439
      break;
440
    case kGDTCORTargetINT:
441
      targetString = @"int";
442
      break;
443
 
444
    default:
445
      targetString = @"unknown";
446
      break;
447
  }
448
  NSString *userAgent =
449
      [NSString stringWithFormat:@"datatransport/%@ %@support/%@ apple/", kGDTCORVersion,
450
                                 targetString, kGDTCCTSupportSDKVersion];
451
 
452
  [request setValue:[self.metadataProvider APIKeyForTarget:target]
453
      forHTTPHeaderField:@"X-Goog-Api-Key"];
454
 
455
  if ([GDTCCTCompressionHelper isGzipped:data]) {
456
    [request setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"];
457
  }
458
  [request setValue:@"application/x-protobuf" forHTTPHeaderField:@"Content-Type"];
459
  [request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"];
460
  [request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
461
  request.HTTPMethod = @"POST";
462
  [request setHTTPBody:data];
463
  return request;
464
}
465
 
466
/** Creates and returns a storage event selector for the specified target and conditions. */
467
- (GDTCORStorageEventSelector *)eventSelectorTarget:(GDTCORTarget)target
468
                                     withConditions:(GDTCORUploadConditions)conditions {
469
  if ((conditions & GDTCORUploadConditionHighPriority) == GDTCORUploadConditionHighPriority) {
470
    return [GDTCORStorageEventSelector eventSelectorForTarget:target];
471
  }
472
  NSMutableSet<NSNumber *> *qosTiers = [[NSMutableSet alloc] init];
473
  if (conditions & GDTCORUploadConditionWifiData) {
474
    [qosTiers addObjectsFromArray:@[
475
      @(GDTCOREventQoSFast), @(GDTCOREventQoSWifiOnly), @(GDTCOREventQosDefault),
476
      @(GDTCOREventQoSTelemetry), @(GDTCOREventQoSUnknown)
477
    ]];
478
  }
479
  if (conditions & GDTCORUploadConditionMobileData) {
480
    [qosTiers addObjectsFromArray:@[ @(GDTCOREventQoSFast), @(GDTCOREventQosDefault) ]];
481
  }
482
 
483
  return [[GDTCORStorageEventSelector alloc] initWithTarget:target
484
                                                   eventIDs:nil
485
                                                 mappingIDs:nil
486
                                                   qosTiers:qosTiers];
487
}
488
 
489
#pragma mark - NSURLSessionDelegate
490
 
491
- (void)URLSession:(NSURLSession *)session
492
                          task:(NSURLSessionTask *)task
493
    willPerformHTTPRedirection:(NSHTTPURLResponse *)response
494
                    newRequest:(NSURLRequest *)request
495
             completionHandler:(void (^)(NSURLRequest *_Nullable))completionHandler {
496
  if (!completionHandler) {
497
    return;
498
  }
499
  if (response.statusCode == 302 || response.statusCode == 301) {
500
    NSURLRequest *newRequest = [self constructRequestWithURL:request.URL
501
                                                   forTarget:kGDTCORTargetCCT
502
                                                        data:task.originalRequest.HTTPBody];
503
    completionHandler(newRequest);
504
  } else {
505
    completionHandler(request);
506
  }
507
}
508
 
509
#pragma mark - NSOperation methods
510
 
511
@synthesize executing = _executing;
512
@synthesize finished = _finished;
513
 
514
- (BOOL)isFinished {
515
  @synchronized(self) {
516
    return _finished;
517
  }
518
}
519
 
520
- (BOOL)isExecuting {
521
  @synchronized(self) {
522
    return _executing;
523
  }
524
}
525
 
526
- (BOOL)isAsynchronous {
527
  return YES;
528
}
529
 
530
- (void)startOperation {
531
  @synchronized(self) {
532
    [self willChangeValueForKey:@"isExecuting"];
533
    [self willChangeValueForKey:@"isFinished"];
534
    self->_executing = YES;
535
    self->_finished = NO;
536
    [self didChangeValueForKey:@"isExecuting"];
537
    [self didChangeValueForKey:@"isFinished"];
538
  }
539
}
540
 
541
- (void)finishOperation {
542
  @synchronized(self) {
543
    [self willChangeValueForKey:@"isExecuting"];
544
    [self willChangeValueForKey:@"isFinished"];
545
    self->_executing = NO;
546
    self->_finished = YES;
547
    [self didChangeValueForKey:@"isExecuting"];
548
    [self didChangeValueForKey:@"isFinished"];
549
  }
550
}
551
 
552
- (void)start {
553
  [self startOperation];
554
 
555
  GDTCORLogDebug(@"Upload operation started: %@", self);
556
  [self uploadTarget:self.target withConditions:self.conditions];
557
}
558
 
559
- (void)cancel {
560
  @synchronized(self) {
561
    [super cancel];
562
 
563
    // If the operation hasn't been started we can set `isFinished = YES` straight away.
564
    if (!_executing) {
565
      _executing = NO;
566
      _finished = YES;
567
    }
568
  }
569
}
570
 
571
@end
572
 
573
NS_ASSUME_NONNULL_END