Yesterday I wrote DroPub – a simple but powerful little OS X application which transparently handles file transfers “from the desktop”.
Even though it has a lot of features, have been tested, updates itself and so on, I only spent about two days on the whole project – for me, this is the essence of Cocoa.
DroPub is heavily based on NSOperation
s and uses a hierarchy model for structuring operations. NSOperation
hierarchies are powerful means for writing most types of “service” applications. The code can easily be followed by a Cocoa programmer and the operating system frameworks and libraries can give really good performance.
In almost all cases one process-global NSOperationQueue is enough and makes things much easier for you. In prefix.pch we declare the instance and allocate it in main.m:
NSOperationQueue *g_opq;
int main(int argc, const char *argv[]) {
g_opq = [[NSOperationQueue alloc] init];
NSApplicationMain(argc, argv);
return 0;
}
Note that the code snippets presented here are condensed versions of the actual code, for illustrative purposes
For each folder which is watched in DroPub, there is one NSOperation
called DPSupervisor
. Whenever the main “thread” detects that a folder should be watched (might have been added to the configuration or changed location), it calls startSupervising:
which starts a new DPSupervisor
:
DPSupervisor *sv = [[DPSupervisor alloc] initWithApp:self conf:conf];
sv.delegate = self;
[g_opq addOperation:sv];
Note here how g_opq
refers to the global NSOperationQueue
mentioned earlier. The conf
argument is simply the per-watched folder configuration containing path, remote host, and so on.
The DPSupervisor
s main
method then looks at the designated folder for new files to appear:
- (void)main {
while ( !self.isCancelled && conf ) {
// [sets up a NSDirectoryEnumerator here]
while (filename = [dirEnum nextObject]) {
// [continues if the file matches certain criteria (isn't hidden etc)]
if (![filesInTransit containsObject:path])
[self sendFile:path name:filename];
}
sleep(1);
}
}
The supervisor uses a NSMutableSet
(filesInTransit
in the code above) to keep track of which files are currently in transit. Here the question of robustness might occur — yes, this is actually a very robust construction. Since the nature of the application is to atomically transfer (to a temporary location then mv once completed successfully) files, so if a operation crashes or if the whole app crashes (oh noes!) the file will simply be transferred again a few seconds later or when the application is restarted. The only danger here is if we mess with our NSMutableSet
of files in transit, then the worst case scenario is probably corrupt files, so let’s not mess with it.
Next step is dispatching yet another NSOperation
, subordinate to the DPSupervisor
, which in DroPub is called DPSendFileOp
. This is done in DPSupervisor
s sendFile:name:
- (void)sendFile:(NSString *)path name:(NSString *)name {
// [make sure we can get an exclusive lock on the file here,
// otherwise try again later]
[filesInTransit addObject:path];
DPSendFileOp *op = [[DPSendFileOp alloc] initWithPath:path name:name conf:conf];
op.delegate = self;
[g_opq addOperation:op];
}
As you might have figured out, DPSendFileOp
takes care of the actual transmission and reports back to it’s parent (technically its delegate) DPSupervisor
.
The main
method of DPSendFileOp
is too comprehensive for pasting here in this article but this is a summary of what it does:
DSFileExistenceMonitor
).".dpupload_"
.DSFileExistenceMonitor
is cancelled.mv
is done over standard SSH.If the file is removed while being transferred DSFileExistenceMonitor
will notify DPSendFileOp
which will interrupt (by signalling) SCP and then notify it’s delegate through fileTransmission:didAbortForPath:
. On the other hand, if the transfer is successful the DPSendFileOp
s delegate is notified through fileTransmission:didSucceedForPath:remoteURI:
in which case the parent DPSupervisor
will move the successfully uploaded file to Trash.
Since we use a hierarchy of operations – running in parallel, thus we can not use the dependency system of NSOperation
– we need to take care of cancelling child operations. We do this by keeping an NSMutableArray
in each parent in which we store references to any subordinate tasks which have started and not yet exited. When the parents cancel
method is invoked, we simply propagate the message to our children:
- (void)cancel {
for (NSOperation *op in childOperations)
[op cancel];
[super cancel];
}
I’m not going to talk much about the user interface in this article, but to sum it up DroPub uses bindings, key-value coding and key-value observation to communicate with the “background” parts.
The interface was built almost completely in Interface Builder.