source: trunk/macosx/Torrent.m @ 7598

Last change on this file since 7598 was 7598, checked in by livings124, 12 years ago

only increment the tier count for valid tiers

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