Rmtree command (#440)

* Added --rmtree command for recursive directory deletion. Generalized the read_dir function to invoke the callback both before and after processing the contents of a directory. Tidied up indentation in copy_file_callback.

* Added missing call to check_error.
diff --git a/src/ios-deploy/ios-deploy.m b/src/ios-deploy/ios-deploy.m
index 4c0e96b..4b5f2f5 100644
--- a/src/ios-deploy/ios-deploy.m
+++ b/src/ios-deploy/ios-deploy.m
@@ -1246,9 +1246,10 @@
     return bundle_id;
 }
 
+typedef enum { READ_DIR_FILE, READ_DIR_BEFORE_DIR, READ_DIR_AFTER_DIR } read_dir_cb_reason;
 
 void read_dir(AFCConnectionRef afc_conn_p, const char* dir,
-              void(*callback)(AFCConnectionRef conn, const char *dir, int file))
+              void(*callback)(AFCConnectionRef conn, const char *dir, read_dir_cb_reason reason))
 {
     char *dir_ent;
 
@@ -1271,13 +1272,7 @@
     AFCKeyValueClose(afc_dict_p);
 
     if (not_dir) {
-        NSLogOut(@"%@", [NSString stringWithUTF8String:dir]);
-    } else {
-        NSLogOut(@"%@/", [NSString stringWithUTF8String:dir]);
-    }
-
-    if (not_dir) {
-        if (callback) (*callback)(afc_conn_p, dir, not_dir);
+        if (callback) (*callback)(afc_conn_p, dir, READ_DIR_FILE);
         return;
     }
 
@@ -1287,9 +1282,13 @@
     if (err != 0) {
         // Couldn't open dir - was probably a file
         return;
-    } else {
-        if (callback) (*callback)(afc_conn_p, dir, not_dir);
     }
+    
+    // Call the callback on the directory before processing its
+    // contents. This is used by copy file callback, which needs to
+    // create the directory on the host before attempting to copy
+    // files into it.
+    if (callback) (*callback)(afc_conn_p, dir, READ_DIR_BEFORE_DIR);
 
     while(true) {
         err = AFCDirectoryRead(afc_conn_p, afc_dir_p, &dir_ent);
@@ -1310,6 +1309,11 @@
     }
 
     AFCDirectoryClose(afc_conn_p, afc_dir_p);
+    
+    // Call the callback on the directory after processing its
+    // contents. This is used by the rmtree callback because it needs
+    // to delete the directory's contents before the directory itself
+    if (callback) (*callback)(afc_conn_p, dir, READ_DIR_AFTER_DIR);
 }
 
 
@@ -1388,11 +1392,21 @@
     fclose(fd);
     return content;
 }
+
+void list_files_callback(AFCConnectionRef conn, const char *name, read_dir_cb_reason reason)
+{
+    if (reason == READ_DIR_FILE) {
+        NSLogOut(@"%@", [NSString stringWithUTF8String:name]);
+    } else if (reason == READ_DIR_BEFORE_DIR) {
+        NSLogOut(@"%@/", [NSString stringWithUTF8String:name]);
+    }
+}
+
 void list_files(AMDeviceRef device)
 {
     AFCConnectionRef afc_conn_p = start_house_arrest_service(device);
     assert(afc_conn_p);
-    read_dir(afc_conn_p, list_root?list_root:"/", NULL);
+    read_dir(afc_conn_p, list_root?list_root:"/", list_files_callback);
     check_error(AFCConnectionClose(afc_conn_p));
 }
 
@@ -1467,7 +1481,7 @@
     check_error(AMDeviceDisconnect(device));
 }
 
-void copy_file_callback(AFCConnectionRef afc_conn_p, const char *name,int file)
+void copy_file_callback(AFCConnectionRef afc_conn_p, const char *name, read_dir_cb_reason reason)
 {
     const char *local_name=name;
 
@@ -1475,39 +1489,44 @@
 
     if (*local_name=='\0') return;
 
-    if (file) {
-    afc_file_ref fref;
-    int err = AFCFileRefOpen(afc_conn_p,name,1,&fref);
+    if (reason == READ_DIR_FILE) {
+        NSLogOut(@"%@", [NSString stringWithUTF8String:name]);
+        afc_file_ref fref;
+        int err = AFCFileRefOpen(afc_conn_p,name,1,&fref);
 
-    if (err) {
-        fprintf(stderr,"AFCFileRefOpen(\"%s\") failed: %d\n",name,err);
-        return;
-    }
+        if (err) {
+            fprintf(stderr,"AFCFileRefOpen(\"%s\") failed: %d\n",name,err);
+            return;
+        }
 
-    FILE *fp = fopen(local_name,"w");
+        FILE *fp = fopen(local_name,"w");
 
-    if (fp==NULL) {
-        fprintf(stderr,"fopen(\"%s\",\"w\") failer: %s\n",local_name,strerror(errno));
+        if (fp==NULL) {
+            fprintf(stderr,"fopen(\"%s\",\"w\") failer: %s\n",local_name,strerror(errno));
+            AFCFileRefClose(afc_conn_p,fref);
+            return;
+        }
+
+        char buf[4096];
+        size_t sz=sizeof(buf);
+
+        while (AFCFileRefRead(afc_conn_p,fref,buf,&sz)==0 && sz) {
+            fwrite(buf,sz,1,fp);
+            sz = sizeof(buf);
+        }
+
         AFCFileRefClose(afc_conn_p,fref);
-        return;
-    }
+        fclose(fp);
 
-    char buf[4096];
-    size_t sz=sizeof(buf);
-
-    while (AFCFileRefRead(afc_conn_p,fref,buf,&sz)==0 && sz) {
-        fwrite(buf,sz,1,fp);
-        sz = sizeof(buf);
-    }
-
-    AFCFileRefClose(afc_conn_p,fref);
-    fclose(fp);
-    } else {
-    if (mkdir(local_name,0777) && errno!=EEXIST)
-        fprintf(stderr,"mkdir(\"%s\") failed: %s\n",local_name,strerror(errno));
+    } else if (reason == READ_DIR_BEFORE_DIR) {
+        NSLogOut(@"%@/", [NSString stringWithUTF8String:name]);
+        if (mkdir(local_name,0777) && errno!=EEXIST) {
+            fprintf(stderr,"mkdir(\"%s\") failed: %s\n",local_name,strerror(errno));
+        }
     }
 }
 
+
 void download_tree(AMDeviceRef device)
 {
     AFCConnectionRef afc_conn_p = start_house_arrest_service(device);
@@ -1547,7 +1566,6 @@
 {
     AFCConnectionRef afc_conn_p = start_house_arrest_service(device);
     assert(afc_conn_p);
-    //        read_dir(houseFd, NULL, "/", NULL);
 
     if (!target_filename)
     {
@@ -1578,8 +1596,6 @@
 
     afc_file_ref file_ref;
 
-    //        read_dir(houseFd, NULL, "/", NULL);
-
     size_t file_size;
     void* file_content = read_file_to_memory([sourcePath fileSystemRepresentation], &file_size);
 
@@ -1643,6 +1659,23 @@
     check_error(AFCConnectionClose(afc_conn_p));
 }
 
+// Handles the READ_DIR_AFTER_DIR callback so that we delete the contents of the
+// directory before the directory itself
+void rmtree_callback(AFCConnectionRef conn, const char *name, read_dir_cb_reason reason)
+{
+    if (reason == READ_DIR_FILE || reason == READ_DIR_AFTER_DIR) {
+        NSLogVerbose(@"Deleting %s", name);
+        check_error(AFCRemovePath(conn, name));
+    }
+}
+
+void rmtree(AMDeviceRef device) {
+    AFCConnectionRef afc_conn_p = start_house_arrest_service(device);
+    assert(afc_conn_p);
+    read_dir(afc_conn_p, target_filename, rmtree_callback);
+    check_error(AFCConnectionClose(afc_conn_p));
+}
+
 void uninstall_app(AMDeviceRef device) {
     CFRetain(device); // don't know if this is necessary?
 
@@ -1722,6 +1755,8 @@
             make_directory(device);
         } else if (strcmp("rm", command) == 0) {
             remove_path(device);
+        } else if (strcmp("rmtree", command) == 0) {
+            rmtree(device);
         } else if (strcmp("exists", command) == 0) {
             exit(app_exists(device));
         } else if (strcmp("uninstall_only", command) == 0) {
@@ -1968,6 +2003,7 @@
         @"  -2, --to <target pathname>   use together with up/download file/tree. specify target\n"
         @"  -D, --mkdir <dir>            make directory on device\n"
         @"  -R, --rm <path>              remove file or directory on device (directories must be empty)\n"
+        @"  -X, --rmtree <path>          remove directory and all contained files recursively on device\n"
         @"  -V, --version                print the executable version \n"
         @"  -e, --exists                 check if the app with given bundle_id is installed or not \n"
         @"  -B, --list_bundle_id         list bundle_id \n"
@@ -2020,6 +2056,7 @@
         { "to", required_argument, NULL, '2'},
         { "mkdir", required_argument, NULL, 'D'},
         { "rm", required_argument, NULL, 'R'},
+        { "rmtree",required_argument, NULL, 'X'},
         { "exists", no_argument, NULL, 'e'},
         { "list_bundle_id", no_argument, NULL, 'B'},
         { "no-wifi", no_argument, NULL, 'W'},
@@ -2033,7 +2070,7 @@
     };
     int ch;
 
-    while ((ch = getopt_long(argc, argv, "VmcdvunrILeD:R:i:b:a:t:p:1:2:o:l:w:9BWjNs:OE:CA:", longopts, NULL)) != -1)
+    while ((ch = getopt_long(argc, argv, "VmcdvunrILeD:R:X:i:b:a:t:p:1:2:o:l:w:9BWjNs:OE:CA:", longopts, NULL)) != -1)
     {
         switch (ch) {
         case 'm':
@@ -2128,6 +2165,11 @@
             target_filename = optarg;
             command = "rm";
             break;
+        case 'X':
+            command_only = true;
+            target_filename = optarg;
+            command = "rmtree";
+            break;
         case 'e':
             command_only = true;
             command = "exists";