source: trunk/macosx/Torrent.m @ 7069

Last change on this file since 7069 was 7069, checked in by charles, 10 years ago

more fucking around with the speed measurements.

  • Property svn:keywords set to Date Rev Author Id
File size: 63.8 KB
Line 
1/******************************************************************************
2 * $Id: Torrent.m 7069 2008-11-08 02:49:04Z charles $
3 *
4 * Copyright (c) 2006-2008 Transmission authors and contributors
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a
7 * copy of this software and associated documentation files (the "Software"),
8 * to deal in the Software without restriction, including without limitation
9 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 * and/or sell copies of the Software, and to permit persons to whom the
11 * Software is furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 * DEALINGS IN THE SOFTWARE.
23 *****************************************************************************/
24
25#import "Torrent.h"
26#import "GroupsController.h"
27#import "FileListNode.h"
28#import "NSApplicationAdditions.h"
29#import "NSStringAdditions.h"
30#import "metainfo.h"
31#import "utils.h" //tr_httpIsValidURL
32
33@interface Torrent (Private)
34
35- (id) initWithHash: (NSString *) hashString path: (NSString *) path torrentStruct: (tr_torrent *) torrentStruct lib: (tr_handle *) lib
36        publicTorrent: (NSNumber *) publicTorrent
37        downloadFolder: (NSString *) downloadFolder
38        useIncompleteFolder: (NSNumber *) useIncompleteFolder incompleteFolder: (NSString *) incompleteFolder
39        ratioSetting: (NSNumber *) ratioSetting ratioLimit: (NSNumber *) ratioLimit
40        waitToStart: (NSNumber *) waitToStart
41        orderValue: (NSNumber *) orderValue groupValue: (NSNumber *) groupValue addedTrackers: (NSNumber *) addedTrackers;
42
43- (BOOL) shouldUseIncompleteFolderForName: (NSString *) name;
44- (void) updateDownloadFolder;
45
46- (void) createFileList;
47- (void) insertPath: (NSMutableArray *) components forParent: (FileListNode *) parent fileSize: (uint64_t) size index: (NSInteger) index;
48
49- (void) completenessChange: (NSNumber *) status;
50
51- (void) quickPause;
52- (void) endQuickPause;
53
54- (NSString *) etaString: (NSInteger) eta;
55
56- (void) updateAllTrackers: (NSMutableArray *) trackers;
57
58- (void) trashFile: (NSString *) path;
59
60- (void) setTimeMachineExclude: (BOOL) exclude forPath: (NSString *) path;
61
62@end
63
64void completenessChangeCallback(tr_torrent * torrent, tr_completeness status, void * torrentData)
65{
66    [(Torrent *)torrentData performSelectorOnMainThread: @selector(completenessChange:)
67                withObject: [[NSNumber alloc] initWithInt: status] waitUntilDone: NO];
68}
69
70@implementation Torrent
71
72- (id) initWithPath: (NSString *) path location: (NSString *) location deleteTorrentFile: (torrentFileState) torrentDelete
73        lib: (tr_handle *) lib
74{
75    self = [self initWithHash: nil path: path torrentStruct: NULL lib: lib
76            publicTorrent: torrentDelete != TORRENT_FILE_DEFAULT ? [NSNumber numberWithBool: torrentDelete == TORRENT_FILE_SAVE] : nil
77            downloadFolder: location
78            useIncompleteFolder: nil incompleteFolder: nil
79            ratioSetting: nil ratioLimit: nil
80            waitToStart: nil orderValue: nil groupValue: nil addedTrackers: nil];
81   
82    if (self)
83    {
84        //if the public and private torrent files are the same, then there is no public torrent
85        if ([[self torrentLocation] isEqualToString: path])
86        {
87            fPublicTorrent = NO;
88            [fPublicTorrentLocation release];
89            fPublicTorrentLocation = nil;
90        }
91        else if (!fPublicTorrent)
92            [self trashFile: path];
93        else;
94    }
95    return self;
96}
97
98- (id) initWithTorrentStruct: (tr_torrent *) torrentStruct location: (NSString *) location lib: (tr_handle *) lib
99{
100    self = [self initWithHash: nil path: nil torrentStruct: torrentStruct lib: lib
101            publicTorrent: [NSNumber numberWithBool: NO]
102            downloadFolder: location
103            useIncompleteFolder: nil incompleteFolder: nil
104            ratioSetting: nil ratioLimit: nil
105            waitToStart: nil orderValue: nil groupValue: nil addedTrackers: nil];
106   
107    return self;
108}
109
110- (id) initWithHistory: (NSDictionary *) history lib: (tr_handle *) lib
111{
112    self = [self initWithHash: [history objectForKey: @"TorrentHash"]
113                path: [history objectForKey: @"TorrentPath"] torrentStruct: NULL lib: lib
114                publicTorrent: [history objectForKey: @"PublicCopy"]
115                downloadFolder: [history objectForKey: @"DownloadFolder"]
116                useIncompleteFolder: [history objectForKey: @"UseIncompleteFolder"]
117                incompleteFolder: [history objectForKey: @"IncompleteFolder"]
118                ratioSetting: [history objectForKey: @"RatioSetting"]
119                ratioLimit: [history objectForKey: @"RatioLimit"]
120                waitToStart: [history objectForKey: @"WaitToStart"]
121                orderValue: [history objectForKey: @"OrderValue"]
122                groupValue: [history objectForKey: @"GroupValue"]
123                addedTrackers: [history objectForKey: @"AddedTrackers"]];
124   
125    if (self)
126    {
127        //start transfer
128        NSNumber * active;
129        if ((active = [history objectForKey: @"Active"]) && [active boolValue])
130        {
131            fStat = tr_torrentStat(fHandle);
132            [self startTransfer];
133        }
134       
135        //upgrading from versions < 1.30: get old added, activity, and done dates
136        NSDate * date;
137        if ((date = [history objectForKey: @"Date"]))
138            tr_torrentSetAddedDate(fHandle, [date timeIntervalSince1970]);
139        if ((date = [history objectForKey: @"DateActivity"]))
140            tr_torrentSetActivityDate(fHandle, [date timeIntervalSince1970]);
141        if ((date = [history objectForKey: @"DateCompleted"]))
142            tr_torrentSetDoneDate(fHandle, [date timeIntervalSince1970]);
143    }
144    return self;
145}
146
147- (NSDictionary *) history
148{
149    NSMutableDictionary * history = [NSMutableDictionary dictionaryWithObjectsAndKeys:
150                    [NSNumber numberWithBool: fPublicTorrent], @"PublicCopy",
151                    [self hashString], @"TorrentHash",
152                    fDownloadFolder, @"DownloadFolder",
153                    [NSNumber numberWithBool: fUseIncompleteFolder], @"UseIncompleteFolder",
154                    [NSNumber numberWithBool: [self isActive]], @"Active",
155                    [NSNumber numberWithInt: fRatioSetting], @"RatioSetting",
156                    [NSNumber numberWithFloat: fRatioLimit], @"RatioLimit",
157                    [NSNumber numberWithBool: fWaitToStart], @"WaitToStart",
158                    [NSNumber numberWithInt: fOrderValue], @"OrderValue",
159                    [NSNumber numberWithInt: fGroupValue], @"GroupValue",
160                    [NSNumber numberWithBool: fAddedTrackers], @"AddedTrackers", nil];
161   
162    if (fIncompleteFolder)
163        [history setObject: fIncompleteFolder forKey: @"IncompleteFolder"];
164
165    if (fPublicTorrent)
166        [history setObject: [self publicTorrentLocation] forKey: @"TorrentPath"];
167       
168    return history;
169}
170
171- (void) dealloc
172{
173    [[NSNotificationCenter defaultCenter] removeObserver: self];
174   
175    if (fFileStat)
176        tr_torrentFilesFree(fFileStat, [self fileCount]);
177   
178    [fPreviousFinishedIndexes release];
179    [fPreviousFinishedIndexesDate release];
180   
181    [fNameString release];
182    [fHashString release];
183   
184    [fDownloadFolder release];
185    [fIncompleteFolder release];
186   
187    [fPublicTorrentLocation release];
188   
189    [fIcon release];
190   
191    [fFileList release];
192   
193    [fQuickPauseDict release];
194   
195    [super dealloc];
196}
197
198- (NSString *) description
199{
200    return [@"Torrent: " stringByAppendingString: [self name]];
201}
202
203- (void) closeRemoveTorrent
204{
205    //allow the file to be index by Time Machine
206    [self setTimeMachineExclude: NO forPath: [[self downloadFolder] stringByAppendingPathComponent: [self name]]];
207   
208    tr_torrentRemove(fHandle);
209}
210
211- (void) changeIncompleteDownloadFolder: (NSString *) folder
212{
213    fUseIncompleteFolder = folder != nil;
214   
215    [fIncompleteFolder release];
216    fIncompleteFolder = fUseIncompleteFolder ? [folder retain] : nil;
217   
218    [self updateDownloadFolder];
219}
220
221- (void) changeDownloadFolder: (NSString *) folder
222{
223    [fDownloadFolder release];
224    fDownloadFolder = [folder retain];
225   
226    [self updateDownloadFolder];
227}
228
229- (NSString *) downloadFolder
230{
231    return [NSString stringWithUTF8String: tr_torrentGetDownloadDir(fHandle)];
232}
233
234- (void) getAvailability: (int8_t *) tab size: (NSInteger) size
235{
236    tr_torrentAvailability(fHandle, tab, size);
237}
238
239- (void) getAmountFinished: (float *) tab size: (NSInteger) size
240{
241    tr_torrentAmountFinished(fHandle, tab, size);
242}
243
244- (NSIndexSet *) previousFinishedPieces
245{
246    //if the torrent hasn't been seen in a bit, and therefore hasn't been refreshed, return nil
247    if (fPreviousFinishedIndexesDate && [fPreviousFinishedIndexesDate timeIntervalSinceNow] > -2.0)
248        return fPreviousFinishedIndexes;
249    else
250        return nil;
251}
252
253-(void) setPreviousFinishedPieces: (NSIndexSet *) indexes
254{
255    [fPreviousFinishedIndexes release];
256    fPreviousFinishedIndexes = [indexes retain];
257   
258    [fPreviousFinishedIndexesDate release];
259    fPreviousFinishedIndexesDate = indexes != nil ? [[NSDate alloc] init] : nil;
260}
261
262- (void) update
263{
264    //get previous status values before update
265    BOOL wasChecking = NO, wasError = NO, wasStalled = NO;
266    if (fStat != NULL)
267    {
268        wasChecking = [self isChecking];
269        wasError = [self isError];
270        wasStalled = fStalled;
271    }
272   
273    fStat = tr_torrentStat(fHandle);
274   
275    //check to stop for ratio
276    CGFloat stopRatio;
277    if ([self isSeeding] && (stopRatio = [self actualStopRatio]) != INVALID && [self ratio] >= stopRatio)
278    {
279        [self setRatioSetting: NSOffState];
280        [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentStoppedForRatio" object: self];
281       
282        [self stopTransfer];
283        fStat = tr_torrentStat(fHandle);
284       
285        fFinishedSeeding = YES;
286    }
287   
288    //check if stalled (stored because based on time and needs to check if it was previously stalled)
289    fStalled = [self isActive] && [fDefaults boolForKey: @"CheckStalled"]
290                && [self stalledMinutes] > [fDefaults integerForKey: @"StalledMinutes"];
291   
292    //update queue for checking (from downloading to seeding), stalled, or error
293    if ((wasChecking && ![self isChecking]) || (wasStalled != fStalled) || (!wasError && [self isError] && [self isActive]))
294        [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateQueue" object: self];
295}
296
297- (void) startTransfer
298{
299    fWaitToStart = NO;
300    fFinishedSeeding = NO;
301   
302    if (![self isActive] && [self alertForFolderAvailable] && [self alertForRemainingDiskSpace])
303    {
304        tr_torrentStart(fHandle);
305        [self update];
306    }
307}
308
309- (void) stopTransfer
310{
311    fWaitToStart = NO;
312   
313    if ([self isActive])
314    {
315        tr_torrentStop(fHandle);
316        [self update];
317       
318        [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateQueue" object: self];
319    }
320}
321
322- (void) sleep
323{
324    if ((fResumeOnWake = [self isActive]))
325        tr_torrentStop(fHandle);
326}
327
328- (void) wakeUp
329{
330    if (fResumeOnWake)
331        tr_torrentStart(fHandle);
332}
333
334- (void) manualAnnounce
335{
336    tr_torrentManualUpdate(fHandle);
337}
338
339- (BOOL) canManualAnnounce
340{
341    return tr_torrentCanManualUpdate(fHandle);
342}
343
344- (void) resetCache
345{
346    tr_torrentVerify(fHandle);
347    [self update];
348}
349
350- (CGFloat) ratio
351{
352    return fStat->ratio;
353}
354
355- (NSInteger) ratioSetting
356{
357    return fRatioSetting;
358}
359
360- (void) setRatioSetting: (NSInteger) setting
361{
362    fRatioSetting = setting;
363}
364
365- (CGFloat) ratioLimit
366{
367    return fRatioLimit;
368}
369
370- (void) setRatioLimit: (CGFloat) limit
371{
372    if (limit >= 0)
373        fRatioLimit = limit;
374}
375
376- (CGFloat) actualStopRatio
377{
378    if (fRatioSetting == NSOnState)
379        return fRatioLimit;
380    else if (fRatioSetting == NSMixedState && [fDefaults boolForKey: @"RatioCheck"])
381        return [fDefaults floatForKey: @"RatioLimit"];
382    else
383        return INVALID;
384}
385
386- (CGFloat) progressStopRatio
387{
388    CGFloat stopRatio, ratio;
389    if ((stopRatio = [self actualStopRatio]) == INVALID || (ratio = [self ratio]) >= stopRatio)
390        return 1.0;
391    else if (stopRatio > 0.0)
392        return ratio / stopRatio;
393    else
394        return 0.0;
395}
396
397- (tr_speedlimit) speedMode: (BOOL) upload
398{
399    return tr_torrentGetSpeedMode(fHandle, upload ? TR_UP : TR_DOWN);
400}
401
402- (void) setSpeedMode: (tr_speedlimit) mode upload: (BOOL) upload
403{
404    tr_torrentSetSpeedMode(fHandle, upload ? TR_UP : TR_DOWN, mode);
405}
406
407- (NSInteger) speedLimit: (BOOL) upload
408{
409    return tr_torrentGetSpeedLimit(fHandle, upload ? TR_UP : TR_DOWN);
410}
411
412- (void) setSpeedLimit: (NSInteger) limit upload: (BOOL) upload
413{
414    tr_torrentSetSpeedLimit(fHandle, upload ? TR_UP : TR_DOWN, limit);
415}
416
417- (void) setMaxPeerConnect: (uint16_t) count
418{
419    NSAssert(count > 0, @"max peer count must be greater than 0");
420   
421    tr_torrentSetPeerLimit(fHandle, count);
422}
423
424- (uint16_t) maxPeerConnect
425{
426    return tr_torrentGetPeerLimit(fHandle);
427}
428
429- (void) setWaitToStart: (BOOL) wait
430{
431    fWaitToStart = wait;
432}
433
434- (BOOL) waitingToStart
435{
436    return fWaitToStart;
437}
438
439- (void) revealData
440{
441    [[NSWorkspace sharedWorkspace] selectFile: [self dataLocation] inFileViewerRootedAtPath: nil];
442}
443
444- (void) revealPublicTorrent
445{
446    if (fPublicTorrent)
447        [[NSWorkspace sharedWorkspace] selectFile: fPublicTorrentLocation inFileViewerRootedAtPath: nil];
448}
449
450- (void) trashData
451{
452    [self trashFile: [self dataLocation]];
453}
454
455- (void) trashTorrent
456{
457    if (fPublicTorrent)
458    {
459        [self trashFile: fPublicTorrentLocation];
460        [fPublicTorrentLocation release];
461        fPublicTorrentLocation = nil;
462       
463        fPublicTorrent = NO;
464    }
465}
466
467- (void) moveTorrentDataFileTo: (NSString *) folder
468{
469    NSString * oldFolder = [self downloadFolder];
470    if (![oldFolder isEqualToString: folder] || ![fDownloadFolder isEqualToString: folder])
471    {
472        //check if moving inside itself
473        NSArray * oldComponents = [oldFolder pathComponents],
474                * newComponents = [folder pathComponents];
475        NSInteger count;
476       
477        if ((count = [oldComponents count]) < [newComponents count]
478                && [[newComponents objectAtIndex: count] isEqualToString: [self name]]
479                && [oldComponents isEqualToArray:
480                        [newComponents objectsAtIndexes: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, count)]]])
481        {
482            NSAlert * alert = [[NSAlert alloc] init];
483            [alert setMessageText: NSLocalizedString(@"A folder cannot be moved to inside itself.",
484                                                        "Move inside itself alert -> title")];
485            [alert setInformativeText: [NSString stringWithFormat:
486                            NSLocalizedString(@"The move operation of \"%@\" cannot be done.",
487                                                "Move inside itself alert -> message"), [self name]]];
488            [alert addButtonWithTitle: NSLocalizedString(@"OK", "Move inside itself alert -> button")];
489           
490            [alert runModal];
491            [alert release];
492           
493            return;
494        }
495       
496        [self quickPause];
497       
498        //allow if file can be moved or does not exist
499        if ([[NSFileManager defaultManager] movePath: [oldFolder stringByAppendingPathComponent: [self name]]
500                            toPath: [folder stringByAppendingPathComponent: [self name]] handler: nil]
501            || ![[NSFileManager defaultManager] fileExistsAtPath: [oldFolder stringByAppendingPathComponent: [self name]]])
502        {
503            //get rid of both incomplete folder and old download folder, even if move failed
504            fUseIncompleteFolder = NO;
505            if (fIncompleteFolder)
506            {
507                [fIncompleteFolder release];
508                fIncompleteFolder = nil;
509            }
510            [self changeDownloadFolder: folder];
511           
512            [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateStats" object: nil];
513           
514            [self endQuickPause];
515        }
516        else
517        {
518            [self endQuickPause];
519       
520            NSAlert * alert = [[NSAlert alloc] init];
521            [alert setMessageText: NSLocalizedString(@"There was an error moving the data file.", "Move error alert -> title")];
522            [alert setInformativeText: [NSString stringWithFormat:
523                            NSLocalizedString(@"The move operation of \"%@\" cannot be done.",
524                                                "Move error alert -> message"), [self name]]];
525            [alert addButtonWithTitle: NSLocalizedString(@"OK", "Move error alert -> button")];
526           
527            [alert runModal];
528            [alert release];
529        }
530    }
531}
532
533- (void) copyTorrentFileTo: (NSString *) path
534{
535    [[NSFileManager defaultManager] copyPath: [self torrentLocation] toPath: path handler: nil];
536}
537
538- (BOOL) alertForRemainingDiskSpace
539{
540    if ([self allDownloaded] || ![fDefaults boolForKey: @"WarningRemainingSpace"])
541        return YES;
542   
543    NSFileManager * fileManager = [NSFileManager defaultManager];
544    NSString * downloadFolder = [self downloadFolder];
545   
546    NSString * volumeName;
547    if ((volumeName = [[fileManager componentsToDisplayForPath: downloadFolder] objectAtIndex: 0]))
548    {
549        BOOL onLeopard = [NSApp isOnLeopardOrBetter];
550       
551        NSDictionary * systemAttributes = onLeopard ? [fileManager attributesOfFileSystemForPath: downloadFolder error: NULL]
552                                            : [fileManager fileSystemAttributesAtPath: downloadFolder];
553        uint64_t remainingSpace = [[systemAttributes objectForKey: NSFileSystemFreeSize] unsignedLongLongValue];
554       
555        //if the remaining space is greater than the size left, then there is enough space regardless of preallocation
556        if (remainingSpace < [self sizeLeft] && remainingSpace < tr_torrentGetBytesLeftToAllocate(fHandle))
557        {
558            NSAlert * alert = [[NSAlert alloc] init];
559            [alert setMessageText: [NSString stringWithFormat:
560                                    NSLocalizedString(@"Not enough remaining disk space to download \"%@\" completely.",
561                                        "Torrent disk space alert -> title"), [self name]]];
562            [alert setInformativeText: [NSString stringWithFormat: NSLocalizedString(@"The transfer will be paused."
563                                        " Clear up space on %@ or deselect files in the torrent inspector to continue.",
564                                        "Torrent disk space alert -> message"), volumeName]];
565            [alert addButtonWithTitle: NSLocalizedString(@"OK", "Torrent disk space alert -> button")];
566            [alert addButtonWithTitle: NSLocalizedString(@"Download Anyway", "Torrent disk space alert -> button")];
567           
568            if (onLeopard)
569            {
570                [alert setShowsSuppressionButton: YES];
571                [[alert suppressionButton] setTitle: NSLocalizedString(@"Do not check disk space again",
572                                                        "Torrent disk space alert -> button")];
573            }
574            else
575                [alert addButtonWithTitle: NSLocalizedString(@"Always Download", "Torrent disk space alert -> button")];
576
577            NSInteger result = [alert runModal];
578            if ((onLeopard ? [[alert suppressionButton] state] == NSOnState : result == NSAlertThirdButtonReturn))
579                [fDefaults setBool: NO forKey: @"WarningRemainingSpace"];
580            [alert release];
581           
582            return result != NSAlertFirstButtonReturn;
583        }
584    }
585    return YES;
586}
587
588- (BOOL) alertForFolderAvailable
589{
590    #warning check for change from incomplete to download folder first
591    if (access(tr_torrentGetDownloadDir(fHandle), 0))
592    {
593        NSAlert * alert = [[NSAlert alloc] init];
594        [alert setMessageText: [NSString stringWithFormat:
595                                NSLocalizedString(@"The folder for downloading \"%@\" cannot be used.",
596                                    "Folder cannot be used alert -> title"), [self name]]];
597        [alert setInformativeText: [NSString stringWithFormat:
598                        NSLocalizedString(@"\"%@\" cannot be used. The transfer will be paused.",
599                                            "Folder cannot be used alert -> message"), [self downloadFolder]]];
600        [alert addButtonWithTitle: NSLocalizedString(@"OK", "Folder cannot be used alert -> button")];
601        [alert addButtonWithTitle: [NSLocalizedString(@"Choose New Location",
602                                    "Folder cannot be used alert -> location button") stringByAppendingEllipsis]];
603       
604        if ([alert runModal] != NSAlertFirstButtonReturn)
605        {
606            NSOpenPanel * panel = [NSOpenPanel openPanel];
607           
608            [panel setPrompt: NSLocalizedString(@"Select", "Folder cannot be used alert -> prompt")];
609            [panel setAllowsMultipleSelection: NO];
610            [panel setCanChooseFiles: NO];
611            [panel setCanChooseDirectories: YES];
612            [panel setCanCreateDirectories: YES];
613
614            [panel setMessage: [NSString stringWithFormat: NSLocalizedString(@"Select the download folder for \"%@\"",
615                                "Folder cannot be used alert -> select destination folder"), [self name]]];
616           
617            [[NSNotificationCenter defaultCenter] postNotificationName: @"MakeWindowKey" object: nil];
618            [panel beginSheetForDirectory: nil file: nil types: nil modalForWindow: [NSApp keyWindow] modalDelegate: self
619                    didEndSelector: @selector(destinationChoiceClosed:returnCode:contextInfo:) contextInfo: nil];
620        }
621       
622        [alert release];
623       
624        return NO;
625    }
626    return YES;
627}
628
629- (void) destinationChoiceClosed: (NSOpenPanel *) openPanel returnCode: (NSInteger) code contextInfo: (void *) context
630{
631    if (code != NSOKButton)
632        return;
633   
634    [self changeDownloadFolder: [[openPanel filenames] objectAtIndex: 0]];
635   
636    [self startTransfer];
637    [self update];
638   
639    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateStats" object: nil];
640}
641
642- (BOOL) alertForMoveFolderAvailable
643{
644    if (access([fDownloadFolder UTF8String], 0))
645    {
646        NSAlert * alert = [[NSAlert alloc] init];
647        [alert setMessageText: [NSString stringWithFormat:
648                                NSLocalizedString(@"The folder for moving the completed \"%@\" cannot be used.",
649                                    "Move folder cannot be used alert -> title"), [self name]]];
650        [alert setInformativeText: [NSString stringWithFormat:
651                                NSLocalizedString(@"\"%@\" cannot be used. The file will remain in its current location.",
652                                    "Move folder cannot be used alert -> message"), fDownloadFolder]];
653        [alert addButtonWithTitle: NSLocalizedString(@"OK", "Move folder cannot be used alert -> button")];
654       
655        [alert runModal];
656        [alert release];
657       
658        return NO;
659    }
660   
661    return YES;
662}
663
664- (NSImage *) icon
665{
666    if (!fIcon)
667    {
668        fIcon = [[[NSWorkspace sharedWorkspace] iconForFileType: [self isFolder] ? NSFileTypeForHFSTypeCode('fldr')
669                                                : [[self name] pathExtension]] retain];
670        [fIcon setFlipped: YES];
671    }
672    return fIcon;
673}
674
675- (NSString *) name
676{
677    return fNameString;
678}
679
680- (BOOL) isFolder
681{
682    return fInfo->isMultifile;
683}
684
685- (uint64_t) size
686{
687    return fInfo->totalSize;
688}
689
690- (uint64_t) sizeLeft
691{
692    return fStat->leftUntilDone;
693}
694
695- (NSString *) trackerAddressAnnounce
696{
697    return fStat->announceURL ? [NSString stringWithUTF8String: fStat->announceURL] : nil;
698}
699
700- (NSDate *) lastAnnounceTime
701{
702    NSInteger date = fStat->lastAnnounceTime;
703    return date > 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil;
704}
705
706- (NSInteger) nextAnnounceTime
707{
708    NSInteger date = fStat->nextAnnounceTime;
709    NSTimeInterval difference;
710    switch (date)
711    {
712        case 0:
713            return STAT_TIME_NONE;
714        case 1:
715            return STAT_TIME_NOW;
716        default:
717            difference = [[NSDate dateWithTimeIntervalSince1970: date] timeIntervalSinceNow];
718            return difference > 0 ? (NSInteger)difference : STAT_TIME_NONE;
719    }
720}
721
722- (NSString *) announceResponse
723{
724    return [NSString stringWithUTF8String: fStat->announceResponse];
725}
726
727- (NSString *) trackerAddressScrape
728{
729    return fStat->scrapeURL ? [NSString stringWithUTF8String: fStat->scrapeURL] : nil;
730}
731
732- (NSDate *) lastScrapeTime
733{
734    NSInteger date = fStat->lastScrapeTime;
735    return date > 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil;
736}
737
738- (NSInteger) nextScrapeTime
739{
740    NSInteger date = fStat->nextScrapeTime;
741    NSTimeInterval difference;
742    switch (date)
743    {
744        case 0:
745            return STAT_TIME_NONE;
746        case 1:
747            return STAT_TIME_NOW;
748        default:
749            difference = [[NSDate dateWithTimeIntervalSince1970: date] timeIntervalSinceNow];
750            return difference > 0 ? (NSInteger)difference : STAT_TIME_NONE;
751    }
752}
753
754- (NSString *) scrapeResponse
755{
756    return [NSString stringWithUTF8String: fStat->scrapeResponse];
757}
758
759- (NSMutableArray *) allTrackers: (BOOL) separators
760{
761    NSInteger count = fInfo->trackerCount, capacity = count;
762    if (separators)
763        capacity += fInfo->trackers[count-1].tier + 1;
764    NSMutableArray * allTrackers = [NSMutableArray arrayWithCapacity: capacity];
765   
766    for (NSInteger i = 0, tier = -1; i < count; i++)
767    {
768        if (separators && tier != fInfo->trackers[i].tier)
769        {
770            tier = fInfo->trackers[i].tier;
771            [allTrackers addObject: [NSNumber numberWithInt: fAddedTrackers ? tier : tier + 1]];
772        }
773       
774        [allTrackers addObject: [NSString stringWithUTF8String: fInfo->trackers[i].announce]];
775    }
776   
777    return allTrackers;
778}
779
780- (BOOL) updateAllTrackersForAdd: (NSMutableArray *) trackers
781{
782    //find added tracker at end of first tier
783    NSInteger i;
784    for (i = 1; i < [trackers count]; i++)
785        if ([[trackers objectAtIndex: i] isKindOfClass: [NSNumber class]])
786            break;
787    i--;
788   
789    NSString * tracker = [trackers objectAtIndex: i];
790   
791    tracker = [tracker stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];
792   
793    if ([tracker rangeOfString: @"://"].location == NSNotFound)
794    {
795        tracker = [@"http://" stringByAppendingString: tracker];
796        [trackers replaceObjectAtIndex: i withObject: tracker];
797    }
798   
799    if (!tr_httpIsValidURL([tracker UTF8String]))
800        return NO;
801   
802    [self updateAllTrackers: trackers];
803   
804    fAddedTrackers = YES;
805    return YES;
806}
807
808- (void) updateAllTrackersForRemove: (NSMutableArray *) trackers
809{
810    //check if no user-added groups
811    if ([[trackers objectAtIndex: 0] intValue] != 0)
812        fAddedTrackers = NO;
813   
814    [self updateAllTrackers: trackers];
815}
816
817- (BOOL) hasAddedTrackers
818{
819    return fAddedTrackers;
820}
821
822- (NSString *) comment
823{
824    return [NSString stringWithUTF8String: fInfo->comment];
825}
826
827- (NSString *) creator
828{
829    return [NSString stringWithUTF8String: fInfo->creator];
830}
831
832- (NSDate *) dateCreated
833{
834    NSInteger date = fInfo->dateCreated;
835    return date > 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil;
836}
837
838- (NSInteger) pieceSize
839{
840    return fInfo->pieceSize;
841}
842
843- (NSInteger) pieceCount
844{
845    return fInfo->pieceCount;
846}
847
848- (NSString *) hashString
849{
850    return fHashString;
851}
852
853- (BOOL) privateTorrent
854{
855    return fInfo->isPrivate;
856}
857
858- (NSString *) torrentLocation
859{
860    return [NSString stringWithUTF8String: fInfo->torrent];
861}
862
863- (NSString *) publicTorrentLocation
864{
865    return fPublicTorrentLocation;
866}
867
868- (NSString *) dataLocation
869{
870    return [[self downloadFolder] stringByAppendingPathComponent: [self name]];
871}
872
873- (BOOL) publicTorrent
874{
875    return fPublicTorrent;
876}
877
878- (CGFloat) progress
879{
880    return fStat->percentComplete;
881}
882
883- (CGFloat) progressDone
884{
885    return fStat->percentDone;
886}
887
888- (CGFloat) progressLeft
889{
890    return (CGFloat)[self sizeLeft] / [self size];
891}
892
893- (CGFloat) checkingProgress
894{
895    return fStat->recheckProgress;
896}
897
898- (NSInteger) eta
899{
900    return fStat->eta;
901}
902
903- (NSInteger) etaRatio
904{
905    if (![self isSeeding])
906        return TR_ETA_UNKNOWN;
907   
908    CGFloat uploadRate = [self uploadRate];
909    if (uploadRate < 0.1)
910        return TR_ETA_UNKNOWN;
911   
912    CGFloat stopRatio = [self actualStopRatio], ratio = [self ratio];
913    if (stopRatio == INVALID || ratio >= stopRatio)
914        return TR_ETA_UNKNOWN;
915   
916    CGFloat haveDownloaded = (CGFloat)([self downloadedTotal] > 0 ? [self downloadedTotal] : [self haveVerified]);
917    CGFloat needUploaded = haveDownloaded * (stopRatio - ratio);
918    return needUploaded / uploadRate / 1024.0;
919}
920
921- (CGFloat) notAvailableDesired
922{
923    return 1.0 - (CGFloat)fStat->desiredAvailable / [self sizeLeft];
924}
925
926- (BOOL) isActive
927{
928    return fStat->activity != TR_STATUS_STOPPED;
929}
930
931- (BOOL) isSeeding
932{
933    return fStat->activity == TR_STATUS_SEED;
934}
935
936- (BOOL) isChecking
937{
938    return fStat->activity == TR_STATUS_CHECK || fStat->activity == TR_STATUS_CHECK_WAIT;
939}
940
941- (BOOL) isCheckingWaiting
942{
943    return fStat->activity == TR_STATUS_CHECK_WAIT;
944}
945
946- (BOOL) allDownloaded
947{
948    return [self progressDone] >= 1.0;
949}
950
951- (BOOL) isComplete
952{
953    return [self progress] >= 1.0;
954}
955
956- (BOOL) isError
957{
958    return fStat->error != TR_OK;
959}
960
961- (NSString *) errorMessage
962{
963    if (![self isError])
964        return @"";
965   
966    NSString * error;
967    if (!(error = [NSString stringWithUTF8String: fStat->errorString])
968        && !(error = [NSString stringWithCString: fStat->errorString encoding: NSISOLatin1StringEncoding]))
969        error = [NSString stringWithFormat: @"(%@)", NSLocalizedString(@"unreadable error", "Torrent -> error string unreadable")];
970   
971    return error;
972}
973
974- (NSArray *) peers
975{
976    int totalPeers;
977    tr_peer_stat * peers = tr_torrentPeers(fHandle, &totalPeers);
978   
979    NSMutableArray * peerDicts = [NSMutableArray arrayWithCapacity: totalPeers];
980   
981    for (int i = 0; i < totalPeers; i++)
982    {
983        tr_peer_stat * peer = &peers[i];
984        NSMutableDictionary * dict = [NSMutableDictionary dictionaryWithCapacity: 9];
985       
986        [dict setObject: [NSNumber numberWithInt: peer->from] forKey: @"From"];
987        [dict setObject: [NSString stringWithUTF8String: peer->addr] forKey: @"IP"];
988        [dict setObject: [NSNumber numberWithInt: peer->port] forKey: @"Port"];
989        [dict setObject: [NSNumber numberWithFloat: peer->progress] forKey: @"Progress"];
990        [dict setObject: [NSNumber numberWithBool: peer->isEncrypted] forKey: @"Encryption"];
991        [dict setObject: [NSString stringWithUTF8String: peer->client] forKey: @"Client"];
992        [dict setObject: [NSString stringWithUTF8String: peer->flagStr] forKey: @"Flags"];
993       
994        if (peer->isUploadingTo)
995            [dict setObject: [NSNumber numberWithFloat: peer->rateToPeer] forKey: @"UL To Rate"];
996        if (peer->isDownloadingFrom)
997            [dict setObject: [NSNumber numberWithFloat: peer->rateToClient] forKey: @"DL From Rate"];
998       
999        [peerDicts addObject: dict];
1000    }
1001   
1002    tr_torrentPeersFree(peers, totalPeers);
1003   
1004    return peerDicts;
1005}
1006
1007- (NSUInteger) webSeedCount
1008{
1009    return fInfo->webseedCount;
1010}
1011
1012- (NSArray *) webSeeds
1013{
1014    const NSInteger webSeedCount = fInfo->webseedCount;
1015    NSMutableArray * webSeeds = [NSMutableArray arrayWithCapacity: webSeedCount];
1016   
1017    float * dlSpeeds = tr_torrentWebSpeeds(fHandle);
1018   
1019    for (NSInteger i = 0; i < webSeedCount; i++)
1020    {
1021        NSMutableDictionary * dict = [NSMutableDictionary dictionaryWithCapacity: 2];
1022       
1023        [dict setObject: [NSString stringWithUTF8String: fInfo->webseeds[i]] forKey: @"Address"];
1024       
1025        if (dlSpeeds[i] != -1.0)
1026            [dict setObject: [NSNumber numberWithFloat: dlSpeeds[i]] forKey: @"DL From Rate"];
1027       
1028        [webSeeds addObject: dict];
1029    }
1030   
1031    tr_free(dlSpeeds);
1032   
1033    return webSeeds;
1034}
1035
1036- (NSString *) progressString
1037{
1038    NSString * string;
1039   
1040    if (![self allDownloaded])
1041    {
1042        CGFloat progress;
1043        if ([self isFolder] && [fDefaults boolForKey: @"DisplayStatusProgressSelected"])
1044        {
1045            string = [NSString stringWithFormat: NSLocalizedString(@"%@ of %@ selected", "Torrent -> progress string"),
1046                        [NSString stringForFileSize: [self haveTotal]], [NSString stringForFileSize: [self totalSizeSelected]]];
1047            progress = 100.0 * [self progressDone];
1048        }
1049        else
1050        {
1051            string = [NSString stringWithFormat: NSLocalizedString(@"%@ of %@", "Torrent -> progress string"),
1052                        [NSString stringForFileSize: [self haveTotal]], [NSString stringForFileSize: [self size]]];
1053            progress = 100.0 * [self progress];
1054        }
1055       
1056        string = [NSString localizedStringWithFormat: @"%@ (%.2f%%)", string, progress];
1057    }
1058    else
1059    {
1060        NSString * downloadString;
1061        if (![self isComplete]) //only multifile possible
1062        {
1063            if ([fDefaults boolForKey: @"DisplayStatusProgressSelected"])
1064                downloadString = [NSString stringWithFormat: NSLocalizedString(@"%@ selected", "Torrent -> progress string"),
1065                                    [NSString stringForFileSize: [self haveTotal]]];
1066            else
1067            {
1068                downloadString = [NSString stringWithFormat: NSLocalizedString(@"%@ of %@", "Torrent -> progress string"),
1069                                    [NSString stringForFileSize: [self haveTotal]], [NSString stringForFileSize: [self size]]];
1070               
1071                downloadString = [NSString localizedStringWithFormat: @"%@ (%.2f%%)", downloadString, 100.0 * [self progress]];
1072            }
1073        }
1074        else
1075            downloadString = [NSString stringForFileSize: [self size]];
1076       
1077        NSString * uploadString = [NSString stringWithFormat: NSLocalizedString(@"uploaded %@ (Ratio: %@)",
1078                                    "Torrent -> progress string"), [NSString stringForFileSize: [self uploadedTotal]],
1079                                    [NSString stringForRatio: [self ratio]]];
1080       
1081        string = [downloadString stringByAppendingFormat: @", %@", uploadString];
1082    }
1083   
1084    //add time when downloading
1085    if (fStat->activity == TR_STATUS_DOWNLOAD || ([self isSeeding]
1086        && (fRatioSetting == NSOnState || (fRatioSetting == NSMixedState && [fDefaults boolForKey: @"RatioCheck"]))))
1087    {
1088        NSInteger eta = fStat->activity == TR_STATUS_DOWNLOAD ? [self eta] : [self etaRatio];
1089        string = [string stringByAppendingFormat: @" - %@", [self etaString: eta]];
1090    }
1091   
1092    return string;
1093}
1094
1095- (NSString *) statusString
1096{
1097    NSString * string;
1098   
1099    if ([self isError])
1100    {
1101        string = NSLocalizedString(@"Error", "Torrent -> status string");
1102        NSString * errorString = [self errorMessage];
1103        if (errorString && ![errorString isEqualToString: @""])
1104            string = [string stringByAppendingFormat: @": %@", errorString];
1105    }
1106    else
1107    {
1108        switch (fStat->activity)
1109        {
1110            case TR_STATUS_STOPPED:
1111                if (fWaitToStart)
1112                {
1113                    string = ![self allDownloaded]
1114                            ? [NSLocalizedString(@"Waiting to download", "Torrent -> status string") stringByAppendingEllipsis]
1115                            : [NSLocalizedString(@"Waiting to seed", "Torrent -> status string") stringByAppendingEllipsis];
1116                }
1117                else if (fFinishedSeeding)
1118                    string = NSLocalizedString(@"Seeding complete", "Torrent -> status string");
1119                else
1120                    string = NSLocalizedString(@"Paused", "Torrent -> status string");
1121                break;
1122
1123            case TR_STATUS_CHECK_WAIT:
1124                string = [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis];
1125                break;
1126
1127            case TR_STATUS_CHECK:
1128                string = [NSString localizedStringWithFormat: NSLocalizedString(@"Checking existing data (%.2f%%)",
1129                                        "Torrent -> status string"), 100.0 * [self checkingProgress]];
1130                break;
1131
1132            case TR_STATUS_DOWNLOAD:
1133                if ([self totalPeersConnected] != 1)
1134                    string = [NSString stringWithFormat: NSLocalizedString(@"Downloading from %d of %d peers",
1135                                                    "Torrent -> status string"), [self peersSendingToUs], [self totalPeersConnected]];
1136                else
1137                    string = [NSString stringWithFormat: NSLocalizedString(@"Downloading from %d of 1 peer",
1138                                                    "Torrent -> status string"), [self peersSendingToUs]];
1139               
1140                NSInteger webSeedCount = fStat->webseedsSendingToUs;
1141                if (webSeedCount > 0)
1142                {
1143                    NSString * webSeedString;
1144                    if (webSeedCount == 1)
1145                        webSeedString = NSLocalizedString(@"web seed", "Torrent -> status string");
1146                    else
1147                        webSeedString = [NSString stringWithFormat: NSLocalizedString(@"%d web seeds", "Torrent -> status string"),
1148                                                                    webSeedCount];
1149                   
1150                    string = [string stringByAppendingFormat: @" + %@", webSeedString];
1151                }
1152               
1153                break;
1154
1155            case TR_STATUS_SEED:
1156                if ([self totalPeersConnected] != 1)
1157                    string = [NSString stringWithFormat: NSLocalizedString(@"Seeding to %d of %d peers", "Torrent -> status string"),
1158                                                    [self peersGettingFromUs], [self totalPeersConnected]];
1159                else
1160                    string = [NSString stringWithFormat: NSLocalizedString(@"Seeding to %d of 1 peer", "Torrent -> status string"),
1161                                                    [self peersGettingFromUs]];
1162        }
1163       
1164        if (fStalled)
1165            string = [NSLocalizedString(@"Stalled", "Torrent -> status string") stringByAppendingFormat: @", %@", string];
1166    }
1167   
1168    //append even if error
1169    if ([self isActive] && ![self isChecking])
1170    {
1171        if (fStat->activity == TR_STATUS_DOWNLOAD)
1172            string = [string stringByAppendingFormat: @" - %@: %@, %@: %@",
1173                        NSLocalizedString(@"DL", "Torrent -> status string"), [NSString stringForSpeed: [self downloadRate]],
1174                        NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]];
1175        else
1176            string = [string stringByAppendingFormat: @" - %@: %@",
1177                        NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]];
1178    }
1179   
1180    return string;
1181}
1182
1183- (NSString *) shortStatusString
1184{
1185    NSString * string;
1186   
1187    switch (fStat->activity)
1188    {
1189        case TR_STATUS_STOPPED:
1190            if (fWaitToStart)
1191            {
1192                string = ![self allDownloaded]
1193                        ? [NSLocalizedString(@"Waiting to download", "Torrent -> status string") stringByAppendingEllipsis]
1194                        : [NSLocalizedString(@"Waiting to seed", "Torrent -> status string") stringByAppendingEllipsis];
1195            }
1196            else if (fFinishedSeeding)
1197                string = NSLocalizedString(@"Seeding complete", "Torrent -> status string");
1198            else
1199                string = NSLocalizedString(@"Paused", "Torrent -> status string");
1200            break;
1201
1202        case TR_STATUS_CHECK_WAIT:
1203            string = [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis];
1204            break;
1205
1206        case TR_STATUS_CHECK:
1207            string = [NSString localizedStringWithFormat: NSLocalizedString(@"Checking existing data (%.2f%%)",
1208                                    "Torrent -> status string"), 100.0 * [self checkingProgress]];
1209            break;
1210       
1211        case TR_STATUS_DOWNLOAD:
1212            string = [NSString stringWithFormat: @"%@: %@, %@: %@",
1213                            NSLocalizedString(@"DL", "Torrent -> status string"), [NSString stringForSpeed: [self downloadRate]],
1214                            NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]];
1215            break;
1216       
1217        case TR_STATUS_SEED:
1218            string = [NSString stringWithFormat: @"%@: %@, %@: %@",
1219                            NSLocalizedString(@"Ratio", "Torrent -> status string"), [NSString stringForRatio: [self ratio]],
1220                            NSLocalizedString(@"UL", "Torrent -> status string"), [NSString stringForSpeed: [self uploadRate]]];
1221    }
1222   
1223    return string;
1224}
1225
1226- (NSString *) remainingTimeString
1227{
1228    if (![self isActive] || ([self isSeeding]
1229        && !(fRatioSetting == NSOnState || (fRatioSetting == NSMixedState && [fDefaults boolForKey: @"RatioCheck"]))))
1230        return [self shortStatusString];
1231   
1232    return [self etaString: [self isSeeding] ? [self etaRatio] : [self eta]];
1233}
1234
1235- (NSString *) stateString
1236{
1237    switch (fStat->activity)
1238    {
1239        case TR_STATUS_STOPPED:
1240            return NSLocalizedString(@"Paused", "Torrent -> status string");
1241
1242        case TR_STATUS_CHECK:
1243            return [NSString localizedStringWithFormat: NSLocalizedString(@"Checking existing data (%.2f%%)",
1244                                    "Torrent -> status string"), 100.0 * [self checkingProgress]];
1245       
1246        case TR_STATUS_CHECK_WAIT:
1247            return [NSLocalizedString(@"Waiting to check existing data", "Torrent -> status string") stringByAppendingEllipsis];
1248
1249        case TR_STATUS_DOWNLOAD:
1250            return NSLocalizedString(@"Downloading", "Torrent -> status string");
1251
1252        case TR_STATUS_SEED:
1253            return NSLocalizedString(@"Seeding", "Torrent -> status string");
1254    }
1255}
1256
1257- (NSInteger) seeders
1258{
1259    return fStat->seeders;
1260}
1261
1262- (NSInteger) leechers
1263{
1264    return fStat->leechers;
1265}
1266
1267- (NSInteger) completedFromTracker
1268{
1269    return fStat->timesCompleted;
1270}
1271
1272- (NSInteger) totalPeersConnected
1273{
1274    return fStat->peersConnected;
1275}
1276
1277- (NSInteger) totalPeersTracker
1278{
1279    return fStat->peersFrom[TR_PEER_FROM_TRACKER];
1280}
1281
1282- (NSInteger) totalPeersIncoming
1283{
1284    return fStat->peersFrom[TR_PEER_FROM_INCOMING];
1285}
1286
1287- (NSInteger) totalPeersCache
1288{
1289    return fStat->peersFrom[TR_PEER_FROM_CACHE];
1290}
1291
1292- (NSInteger) totalPeersPex
1293{
1294    return fStat->peersFrom[TR_PEER_FROM_PEX];
1295}
1296
1297- (NSInteger) totalPeersKnown
1298{
1299    return fStat->peersKnown;
1300}
1301
1302- (NSInteger) peersSendingToUs
1303{
1304    return fStat->peersSendingToUs;
1305}
1306
1307- (NSInteger) peersGettingFromUs
1308{
1309    return fStat->peersGettingFromUs;
1310}
1311
1312- (CGFloat) downloadRate
1313{
1314    return fStat->pieceDownloadSpeed;
1315}
1316
1317- (CGFloat) uploadRate
1318{
1319    return fStat->pieceUploadSpeed;
1320}
1321
1322- (CGFloat) totalRate
1323{
1324    return [self downloadRate] + [self uploadRate];
1325}
1326
1327- (uint64_t) haveVerified
1328{
1329    return fStat->haveValid;
1330}
1331
1332- (uint64_t) haveTotal
1333{
1334    return [self haveVerified] + fStat->haveUnchecked;
1335}
1336
1337- (uint64_t) totalSizeSelected
1338{
1339    return fStat->sizeWhenDone;
1340}
1341
1342- (uint64_t) downloadedTotal
1343{
1344    return fStat->downloadedEver;
1345}
1346
1347- (uint64_t) uploadedTotal
1348{
1349    return fStat->uploadedEver;
1350}
1351
1352- (uint64_t) failedHash
1353{
1354    return fStat->corruptEver;
1355}
1356
1357- (CGFloat) swarmSpeed
1358{
1359    return fStat->swarmSpeed;
1360}
1361
1362- (NSInteger) orderValue
1363{
1364    return fOrderValue;
1365}
1366
1367- (void) setOrderValue: (NSInteger) orderValue
1368{
1369    fOrderValue = orderValue;
1370}
1371
1372- (NSInteger) groupValue
1373{
1374    return fGroupValue;
1375}
1376
1377- (void) setGroupValue: (NSInteger) goupValue
1378{
1379    fGroupValue = goupValue;
1380}
1381
1382- (NSInteger) groupOrderValue
1383{
1384    return [[GroupsController groups] rowValueForIndex: fGroupValue];
1385}
1386
1387- (void) checkGroupValueForRemoval: (NSNotification *) notification
1388{
1389    if (fGroupValue != -1 && [[[notification userInfo] objectForKey: @"Indexes"] containsIndex: fGroupValue])
1390        fGroupValue = -1;
1391}
1392
1393- (NSArray *) fileList
1394{
1395    return fFileList;
1396}
1397
1398- (NSInteger) fileCount
1399{
1400    return fInfo->fileCount;
1401}
1402
1403- (void) updateFileStat
1404{
1405    if (fFileStat)
1406        tr_torrentFilesFree(fFileStat, [self fileCount]);
1407   
1408    fFileStat = tr_torrentFiles(fHandle, NULL);
1409}
1410
1411- (CGFloat) fileProgress: (FileListNode *) node
1412{
1413    if ([self isComplete])
1414        return 1.0;
1415   
1416    if (!fFileStat)
1417        [self updateFileStat];
1418   
1419    NSIndexSet * indexSet = [node indexes];
1420   
1421    if ([indexSet count] == 1)
1422        return fFileStat[[indexSet firstIndex]].progress;
1423   
1424    uint64_t have = 0;
1425    for (NSInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1426        have += fFileStat[index].bytesCompleted;
1427   
1428    NSAssert([node size], @"director in torrent file has size 0");
1429    return (CGFloat)have / [node size];
1430}
1431
1432- (BOOL) canChangeDownloadCheckForFile: (NSInteger) index
1433{
1434    if (!fFileStat)
1435        [self updateFileStat];
1436   
1437    return [self fileCount] > 1 && fFileStat[index].progress < 1.0;
1438}
1439
1440- (BOOL) canChangeDownloadCheckForFiles: (NSIndexSet *) indexSet
1441{
1442    if ([self fileCount] <= 1 || [self isComplete])
1443        return NO;
1444   
1445    if (!fFileStat)
1446        [self updateFileStat];
1447   
1448    for (NSInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1449        if (fFileStat[index].progress < 1.0)
1450            return YES;
1451    return NO;
1452}
1453
1454- (NSInteger) checkForFiles: (NSIndexSet *) indexSet
1455{
1456    BOOL onState = NO, offState = NO;
1457    for (NSInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1458    {
1459        if (tr_torrentGetFileDL(fHandle, index) || ![self canChangeDownloadCheckForFile: index])
1460            onState = YES;
1461        else
1462            offState = YES;
1463       
1464        if (onState && offState)
1465            return NSMixedState;
1466    }
1467    return onState ? NSOnState : NSOffState;
1468}
1469
1470- (void) setFileCheckState: (NSInteger) state forIndexes: (NSIndexSet *) indexSet
1471{
1472    NSUInteger count = [indexSet count];
1473    tr_file_index_t * files = malloc(count * sizeof(tr_file_index_t));
1474    for (NSUInteger index = [indexSet firstIndex], i = 0; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index], i++)
1475        files[i] = index;
1476   
1477    tr_torrentSetFileDLs(fHandle, files, count, state != NSOffState);
1478    free(files);
1479   
1480    [self update];
1481    [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentFileCheckChange" object: self];
1482}
1483
1484- (void) setFilePriority: (NSInteger) priority forIndexes: (NSIndexSet *) indexSet
1485{
1486    const NSUInteger count = [indexSet count];
1487    tr_file_index_t * files = malloc(count * sizeof(tr_file_index_t));
1488    for (NSUInteger index = [indexSet firstIndex], i = 0; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index], i++)
1489        files[i] = index;
1490   
1491    tr_torrentSetFilePriorities(fHandle, files, count, priority);
1492    free(files);
1493}
1494
1495- (BOOL) hasFilePriority: (NSInteger) priority forIndexes: (NSIndexSet *) indexSet
1496{
1497    for (NSInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1498        if (priority == tr_torrentGetFilePriority(fHandle, index) && [self canChangeDownloadCheckForFile: index])
1499            return YES;
1500    return NO;
1501}
1502
1503- (NSSet *) filePrioritiesForIndexes: (NSIndexSet *) indexSet
1504{
1505    BOOL low = NO, normal = NO, high = NO;
1506    NSMutableSet * priorities = [NSMutableSet setWithCapacity: 3];
1507   
1508    for (NSInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index])
1509    {
1510        if (![self canChangeDownloadCheckForFile: index])
1511            continue;
1512       
1513        NSInteger priority = tr_torrentGetFilePriority(fHandle, index);
1514        if (priority == TR_PRI_LOW)
1515        {
1516            if (low)
1517                continue;
1518            low = YES;
1519        }
1520        else if (priority == TR_PRI_HIGH)
1521        {
1522            if (high)
1523                continue;
1524            high = YES;
1525        }
1526        else
1527        {
1528            if (normal)
1529                continue;
1530            normal = YES;
1531        }
1532       
1533        [priorities addObject: [NSNumber numberWithInt: priority]];
1534        if (low && normal && high)
1535            break;
1536    }
1537    return priorities;
1538}
1539
1540- (NSDate *) dateAdded
1541{
1542    time_t date = fStat->addedDate;
1543    return [NSDate dateWithTimeIntervalSince1970: date];
1544}
1545
1546- (NSDate *) dateCompleted
1547{
1548    time_t date = fStat->doneDate;
1549    return date != 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil;
1550}
1551
1552- (NSDate *) dateActivity
1553{
1554    time_t date = fStat->activityDate;
1555    return date != 0 ? [NSDate dateWithTimeIntervalSince1970: date] : nil;
1556}
1557
1558- (NSDate *) dateActivityOrAdd
1559{
1560    NSDate * date = [self dateActivity];
1561    return date ? date : [self dateAdded];
1562}
1563
1564- (NSInteger) stalledMinutes
1565{
1566    time_t start = fStat->startDate;
1567    if (start == 0)
1568        return -1;
1569   
1570    NSDate * started = [NSDate dateWithTimeIntervalSince1970: start],
1571            * activity = [self dateActivity];
1572   
1573    NSDate * laterDate = activity ? [started laterDate: activity] : started;
1574    return -1 * [laterDate timeIntervalSinceNow] / 60;
1575}
1576
1577- (BOOL) isStalled
1578{
1579    return fStalled;
1580}
1581
1582- (NSInteger) stateSortKey
1583{
1584    if (![self isActive]) //paused
1585        return 0;
1586    else if ([self isSeeding]) //seeding
1587        return 1;
1588    else //downloading
1589        return 2;
1590}
1591
1592- (tr_torrent *) torrentStruct
1593{
1594    return fHandle;
1595}
1596
1597@end
1598
1599@implementation Torrent (Private)
1600
1601//if a hash is given, attempt to load that; otherwise, attempt to open file at path
1602- (id) initWithHash: (NSString *) hashString path: (NSString *) path torrentStruct: (tr_torrent *) torrentStruct lib: (tr_handle *) lib
1603        publicTorrent: (NSNumber *) publicTorrent
1604        downloadFolder: (NSString *) downloadFolder
1605        useIncompleteFolder: (NSNumber *) useIncompleteFolder incompleteFolder: (NSString *) incompleteFolder
1606        ratioSetting: (NSNumber *) ratioSetting ratioLimit: (NSNumber *) ratioLimit
1607        waitToStart: (NSNumber *) waitToStart
1608        orderValue: (NSNumber *) orderValue groupValue: (NSNumber *) groupValue addedTrackers: (NSNumber *) addedTrackers
1609{
1610    if (!(self = [super init]))
1611        return nil;
1612   
1613    fDefaults = [NSUserDefaults standardUserDefaults];
1614
1615    fPublicTorrent = path && (publicTorrent ? [publicTorrent boolValue] : ![fDefaults boolForKey: @"DeleteOriginalTorrent"]);
1616    if (fPublicTorrent)
1617        fPublicTorrentLocation = [path retain];
1618   
1619    fDownloadFolder = downloadFolder ? downloadFolder : [fDefaults stringForKey: @"DownloadFolder"];
1620    fDownloadFolder = [[fDownloadFolder stringByExpandingTildeInPath] retain];
1621   
1622    fUseIncompleteFolder = useIncompleteFolder ? [useIncompleteFolder boolValue]
1623                                : [fDefaults boolForKey: @"UseIncompleteDownloadFolder"];
1624    if (fUseIncompleteFolder)
1625    {
1626        fIncompleteFolder = incompleteFolder ? incompleteFolder : [fDefaults stringForKey: @"IncompleteDownloadFolder"];
1627        fIncompleteFolder = [[fIncompleteFolder stringByExpandingTildeInPath] retain];
1628    }
1629   
1630    if (torrentStruct)
1631    {
1632        fHandle = torrentStruct;
1633        fInfo = tr_torrentInfo(fHandle);
1634       
1635        NSString * currentDownloadFolder = [self shouldUseIncompleteFolderForName: [NSString stringWithUTF8String: fInfo->name]]
1636                                                ? fIncompleteFolder : fDownloadFolder;
1637        tr_torrentSetDownloadDir(fHandle, [currentDownloadFolder UTF8String]);
1638    }
1639    else
1640    {
1641        //set libtransmission settings for initialization
1642        tr_ctor * ctor = tr_ctorNew(lib);
1643        tr_ctorSetPaused(ctor, TR_FORCE, YES);
1644        tr_ctorSetPeerLimit(ctor, TR_FALLBACK, [fDefaults integerForKey: @"PeersTorrent"]);
1645       
1646        tr_info info;
1647        if (hashString)
1648        {
1649            tr_ctorSetMetainfoFromHash(ctor, [hashString UTF8String]);
1650            if (tr_torrentParse(lib, ctor, &info) == TR_OK)
1651            {
1652                NSString * currentDownloadFolder = [self shouldUseIncompleteFolderForName: [NSString stringWithUTF8String: info.name]]
1653                                                    ? fIncompleteFolder : fDownloadFolder;
1654                tr_ctorSetDownloadDir(ctor, TR_FORCE, [currentDownloadFolder UTF8String]);
1655               
1656                fHandle = tr_torrentNew(lib, ctor, NULL);
1657            }
1658            tr_metainfoFree(&info);
1659        }
1660        if (!fHandle && path)
1661        {
1662            tr_ctorSetMetainfoFromFile(ctor, [path UTF8String]);
1663            if (tr_torrentParse(lib, ctor, &info) == TR_OK)
1664            {
1665                NSString * currentDownloadFolder = [self shouldUseIncompleteFolderForName: [NSString stringWithUTF8String: info.name]]
1666                                                    ? fIncompleteFolder : fDownloadFolder;
1667                tr_ctorSetDownloadDir(ctor, TR_FORCE, [currentDownloadFolder UTF8String]);
1668               
1669                fHandle = tr_torrentNew(lib, ctor, NULL);
1670            }
1671            tr_metainfoFree(&info);
1672        }
1673       
1674        tr_ctorFree(ctor);
1675       
1676        if (!fHandle)
1677        {
1678            [self release];
1679            return nil;
1680        }
1681       
1682        fInfo = tr_torrentInfo(fHandle);
1683    }
1684   
1685    tr_torrentSetCompletenessCallback(fHandle, completenessChangeCallback, self);
1686   
1687    fNameString = [[NSString alloc] initWithUTF8String: fInfo->name];
1688    fHashString = [[NSString alloc] initWithUTF8String: fInfo->hashString];
1689       
1690    fRatioSetting = ratioSetting ? [ratioSetting intValue] : NSMixedState;
1691    fRatioLimit = ratioLimit ? [ratioLimit floatValue] : [fDefaults floatForKey: @"RatioLimit"];
1692    fFinishedSeeding = NO;
1693   
1694    fWaitToStart = waitToStart && [waitToStart boolValue];
1695    fResumeOnWake = NO;
1696   
1697    fOrderValue = orderValue ? [orderValue intValue] : tr_sessionCountTorrents(lib) - 1;
1698    fGroupValue = groupValue ? [groupValue intValue] : -1;
1699   
1700    fAddedTrackers = addedTrackers ? [addedTrackers boolValue] : NO;
1701   
1702    [self createFileList];
1703   
1704    [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(checkGroupValueForRemoval:)
1705        name: @"GroupValueRemoved" object: nil];
1706   
1707    [self update];
1708   
1709    //mark incomplete files to be ignored by Time Machine
1710    [self setTimeMachineExclude: ![self allDownloaded] forPath: [[self downloadFolder] stringByAppendingPathComponent: [self name]]];
1711   
1712    return self;
1713}
1714
1715- (void) createFileList
1716{
1717    if ([self isFolder])
1718    {
1719        NSInteger count = [self fileCount];
1720        NSMutableArray * fileList = [[NSMutableArray alloc] initWithCapacity: count];
1721       
1722        for (NSInteger i = 0; i < count; i++)
1723        {
1724            tr_file * file = &fInfo->files[i];
1725           
1726            NSMutableArray * pathComponents = [[[NSString stringWithUTF8String: file->name] pathComponents] mutableCopy];
1727            NSString * path = [pathComponents objectAtIndex: 0];
1728            NSString * name = [pathComponents objectAtIndex: 1];
1729            [pathComponents removeObjectsAtIndexes: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, 2)]];
1730           
1731            if ([pathComponents count] > 0)
1732            {
1733                //determine if folder node already exists
1734                NSEnumerator * enumerator = [fileList objectEnumerator];
1735                FileListNode * node;
1736                while ((node = [enumerator nextObject]))
1737                    if ([[node name] isEqualToString: name] && [node isFolder])
1738                        break;
1739               
1740                if (!node)
1741                {
1742                    node = [[FileListNode alloc] initWithFolderName: name path: path];
1743                    [fileList addObject: node];
1744                    [node release];
1745                }
1746               
1747                [node insertIndex: i withSize: file->length];
1748                [self insertPath: pathComponents forParent: node fileSize: file->length index: i];
1749            }
1750            else
1751            {
1752                FileListNode * node = [[FileListNode alloc] initWithFileName: name path: path size: file->length index: i];
1753                [fileList addObject: node];
1754                [node release];
1755            }
1756           
1757            [pathComponents release];
1758        }
1759       
1760        fFileList = [[NSArray alloc] initWithArray: fileList];
1761        [fileList release];
1762    }
1763    else
1764    {
1765        FileListNode * node = [[FileListNode alloc] initWithFileName: [self name] path: @"" size: [self size] index: 0];
1766        fFileList = [[NSArray arrayWithObject: node] retain];
1767        [node release];
1768    }
1769}
1770
1771- (void) insertPath: (NSMutableArray *) components forParent: (FileListNode *) parent fileSize: (uint64_t) size index: (NSInteger) index
1772{
1773    NSString * name = [components objectAtIndex: 0];
1774    BOOL isFolder = [components count] > 1;
1775   
1776    FileListNode * node = nil;
1777    if (isFolder)
1778    {
1779        NSEnumerator * enumerator = [[parent children] objectEnumerator];
1780        while ((node = [enumerator nextObject]))
1781            if ([[node name] isEqualToString: name] && [node isFolder])
1782                break;
1783    }
1784   
1785    //create new folder or file if it doesn't already exist
1786    if (!node)
1787    {
1788        if (isFolder)
1789            node = [[FileListNode alloc] initWithFolderName: name path: [parent fullPath]];
1790        else
1791            node = [[FileListNode alloc] initWithFileName: name path: [parent fullPath] size: size index: index];
1792       
1793        [parent insertChild: node];
1794        [node release];
1795    }
1796   
1797    if (isFolder)
1798    {
1799        [node insertIndex: index withSize: size];
1800       
1801        [components removeObjectAtIndex: 0];
1802        [self insertPath: components forParent: node fileSize: size index: index];
1803    }
1804}
1805
1806- (BOOL) shouldUseIncompleteFolderForName: (NSString *) name
1807{
1808    return fUseIncompleteFolder &&
1809        ![[NSFileManager defaultManager] fileExistsAtPath: [fDownloadFolder stringByAppendingPathComponent: name]];
1810}
1811
1812- (void) updateDownloadFolder
1813{
1814    //remove old Time Machine location
1815    [self setTimeMachineExclude: NO forPath: [[self downloadFolder] stringByAppendingPathComponent: [self name]]];
1816   
1817    NSString * folder = [self shouldUseIncompleteFolderForName: [self name]] ? fIncompleteFolder : fDownloadFolder;
1818    tr_torrentSetDownloadDir(fHandle, [folder UTF8String]);
1819   
1820    [self setTimeMachineExclude: ![self allDownloaded] forPath: [folder stringByAppendingPathComponent: [self name]]];
1821}
1822
1823//status has been retained
1824- (void) completenessChange: (NSNumber *) status
1825{
1826    fStat = tr_torrentStat(fHandle); //don't call update yet to avoid auto-stop
1827   
1828    BOOL canMove;
1829    switch ([status intValue])
1830    {
1831        case TR_CP_DONE:
1832        case TR_CP_COMPLETE:
1833            canMove = YES;
1834           
1835            //move file from incomplete folder to download folder
1836            if (fUseIncompleteFolder && ![[self downloadFolder] isEqualToString: fDownloadFolder]
1837                && (canMove = [self alertForMoveFolderAvailable]))
1838            {
1839                [self quickPause];
1840               
1841                if ([[NSFileManager defaultManager] movePath: [[self downloadFolder] stringByAppendingPathComponent: [self name]]
1842                                        toPath: [fDownloadFolder stringByAppendingPathComponent: [self name]] handler: nil])
1843                    [self updateDownloadFolder];
1844                else
1845                    canMove = NO;
1846               
1847                [self endQuickPause];
1848            }
1849           
1850            if (!canMove)
1851            {
1852                fUseIncompleteFolder = NO;
1853               
1854                [fDownloadFolder release];
1855                fDownloadFolder = fIncompleteFolder;
1856                fIncompleteFolder = nil;
1857            }
1858           
1859            //allow to be backed up by Time Machine
1860            [self setTimeMachineExclude: NO forPath: [[self downloadFolder] stringByAppendingPathComponent: [self name]]];
1861           
1862            [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentFinishedDownloading" object: self];
1863            break;
1864       
1865        case TR_CP_INCOMPLETE:
1866            //do not allow to be backed up by Time Machine
1867            [self setTimeMachineExclude: YES forPath: [[self downloadFolder] stringByAppendingPathComponent: [self name]]];
1868           
1869            [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentRestartedDownloading" object: self];
1870            break;
1871    }
1872    [status release];
1873   
1874    [self update];
1875}
1876
1877- (void) quickPause
1878{
1879    if (fQuickPauseDict)
1880        return;
1881
1882    fQuickPauseDict = [[NSDictionary alloc] initWithObjectsAndKeys:
1883                    [NSNumber numberWithInt: [self speedMode: YES]], @"UploadSpeedMode",
1884                    [NSNumber numberWithInt: [self speedLimit: YES]], @"UploadSpeedLimit",
1885                    [NSNumber numberWithInt: [self speedMode: NO]], @"DownloadSpeedMode",
1886                    [NSNumber numberWithInt: [self speedLimit: NO]], @"DownloadSpeedLimit", nil];
1887   
1888    [self setSpeedMode: TR_SPEEDLIMIT_SINGLE upload: YES];
1889    [self setSpeedLimit: 0 upload: YES];
1890    [self setSpeedMode: TR_SPEEDLIMIT_SINGLE upload: NO];
1891    [self setSpeedLimit: 0 upload: NO];
1892}
1893
1894- (void) endQuickPause
1895{
1896    if (!fQuickPauseDict)
1897        return;
1898   
1899    [self setSpeedMode: [[fQuickPauseDict objectForKey: @"UploadSpeedMode"] intValue] upload: YES];
1900    [self setSpeedLimit: [[fQuickPauseDict objectForKey: @"UploadSpeedLimit"] intValue] upload: YES];
1901    [self setSpeedMode: [[fQuickPauseDict objectForKey: @"DownloadSpeedMode"] intValue] upload: NO];
1902    [self setSpeedLimit: [[fQuickPauseDict objectForKey: @"DownloadSpeedLimit"] intValue] upload: NO];
1903   
1904    [fQuickPauseDict release];
1905    fQuickPauseDict = nil;
1906}
1907
1908- (NSString *) etaString: (NSInteger) eta
1909{
1910    switch (eta)
1911    {
1912        case TR_ETA_NOT_AVAIL:
1913        case TR_ETA_UNKNOWN:
1914            return NSLocalizedString(@"remaining time unknown", "Torrent -> eta string");
1915        default:
1916            return [NSString stringWithFormat: NSLocalizedString(@"%@ remaining", "Torrent -> eta string"),
1917                        [NSString timeString: eta showSeconds: YES maxFields: 2]];
1918    }
1919}
1920
1921- (void) updateAllTrackers: (NSMutableArray *) trackers
1922{
1923    //get count
1924    NSInteger count = 0;
1925    NSEnumerator * enumerator = [trackers objectEnumerator];
1926    id object;
1927    while ((object = [enumerator nextObject]))
1928        if (![object isKindOfClass: [NSNumber class]])
1929            count++;
1930   
1931    //recreate the tracker structure
1932    tr_tracker_info * trackerStructs = tr_new(tr_tracker_info, count);
1933    NSInteger tier = 0, i = 0;
1934    enumerator = [trackers objectEnumerator];
1935    while ((object = [enumerator nextObject]))
1936    {
1937        if (![object isKindOfClass: [NSNumber class]])
1938        {
1939            trackerStructs[i].tier = tier;
1940            trackerStructs[i].announce = (char *)[object UTF8String];
1941            i++;
1942        }
1943        else
1944            tier++;
1945    }
1946   
1947    tr_torrentSetAnnounceList(fHandle, trackerStructs, count);
1948    tr_free(trackerStructs);
1949}
1950
1951- (void) trashFile: (NSString *) path
1952{
1953    //attempt to move to trash
1954    if (![[NSWorkspace sharedWorkspace] performFileOperation: NSWorkspaceRecycleOperation
1955        source: [path stringByDeletingLastPathComponent] destination: @""
1956        files: [NSArray arrayWithObject: [path lastPathComponent]] tag: nil])
1957    {
1958        //if cannot trash, just delete it (will work if it's on a remote volume)
1959        if ([NSApp isOnLeopardOrBetter])
1960        {
1961            NSError * error;
1962            if (![[NSFileManager defaultManager] removeItemAtPath: path error: &error])
1963                NSLog(@"Could not trash %@: %@", path, [error localizedDescription]);
1964        }
1965        else
1966        {
1967            if (![[NSFileManager defaultManager] removeFileAtPath: path handler: nil])
1968                NSLog(@"Could not trash %@", path);
1969        }
1970    }
1971}
1972
1973- (void) setTimeMachineExclude: (BOOL) exclude forPath: (NSString *) path
1974{
1975    if ([NSApp isOnLeopardOrBetter])
1976        CSBackupSetItemExcluded((CFURLRef)[NSURL fileURLWithPath: path], exclude, true);
1977}
1978
1979@end
Note: See TracBrowser for help on using the repository browser.