Repository: Duke-GCB/DukeDSClient

Issue:
File Synchronization issue between portal and ddsclient
User feedback:

> As you may be aware, if a project has been modified via the web-portal, ddsclient seems to get confused. For example, If a file from a project has been removed via the web-portal, the ddsclient will ignore an upload request that would re-instantiate that file; it will report "Uploading 0 projects, 0 folders, 1 file.", but no files will be uploaded and the nothing will happen. 


Discussion:

Patch:
diff --git a/ddsc/core/localstore.py b/ddsc/core/localstore.py
index db0fa4b..9ced1a4 100644
--- a/ddsc/core/localstore.py
+++ b/ddsc/core/localstore.py
@@ -231,7 +231,14 @@ class LocalFile(object):
         self.remote_id = remote_id
 
     def count_chunks(self, bytes_per_chunk):
-        return math.ceil(float(self.size) / float(bytes_per_chunk))
+        """
+        Based on the size of the file determine how many chunks we will need to upload.
+        For empty files 1 chunk is returned (DukeDS requires an empty chunk for empty files).
+        :param bytes_per_chunk: int: how many bytes should chunks to spglit the file into
+        :return: int: number of chunks that will need to be sent
+        """
+        chunks = math.ceil(float(self.size) / float(bytes_per_chunk))
+        return max(chunks, 1)
 
     def __str__(self):
         return 'file:{}'.format(self.name)


Test patch:
diff --git a/ddsc/core/tests/test_localstore.py b/ddsc/core/tests/test_localstore.py
index ff182bf..b4bb9da 100644
--- a/ddsc/core/tests/test_localstore.py
+++ b/ddsc/core/tests/test_localstore.py
@@ -1,9 +1,9 @@
 import shutil
 import tarfile
 from unittest import TestCase
-
 from ddsc.core.localstore import LocalFile, LocalFolder, LocalProject, FileFilter
 from ddsc.config import FILE_EXCLUDE_REGEX_DEFAULT
+from mock import patch
 
 INCLUDE_ALL = ''
 
@@ -159,3 +159,20 @@ class TestFileFilter(TestCase):
         # exclude bad filenames
         for bad_filename in bad_files:
             self.assertEqual(include_file(bad_filename), False)
+
+
+class TestLocalFile(TestCase):
+    @patch('ddsc.core.localstore.os')
+    @patch('ddsc.core.localstore.PathData')
+    def test_count_chunks_values(self, mock_path_data, mock_os):
+        values = [
+            # file_size, bytes_per_chunk, expected
+            (200, 10, 20),
+            (200, 150, 2),
+            (3, 150, 1),
+            (0, 10, 1),  # Empty files must send 1 empty chunk to DukeDS
+        ]
+        f = LocalFile('fakefile.txt')
+        for file_size, bytes_per_chunk, expected in values:
+            f.size = file_size
+            self.assertEqual(expected, f.count_chunks(bytes_per_chunk))
diff --git a/ddsc/core/tests/test_upload.py b/ddsc/core/tests/test_upload.py
index d3a4099..6fcbac1 100644
--- a/ddsc/core/tests/test_upload.py
+++ b/ddsc/core/tests/test_upload.py
@@ -1,6 +1,7 @@
 from __future__ import absolute_import
 from unittest import TestCase
-from ddsc.core.upload import ProjectUpload
+from ddsc.core.upload import ProjectUpload, LocalOnlyCounter
+from ddsc.core.localstore import LocalFile
 from mock import MagicMock, patch
 
 
@@ -24,3 +25,19 @@ class TestUploadCommand(TestCase):
         self.assertIn("Files/Folders that need to be uploaded:", dry_run_report)
         self.assertIn("data.txt", dry_run_report)
         self.assertIn("data2.txt", dry_run_report)
+
+
+class TestLocalOnlyCounter(TestCase):
+    @patch('ddsc.core.localstore.os')
+    @patch('ddsc.core.localstore.PathData')
+    def test_total_items(self, mock_path_data, mock_os):
+        counter = LocalOnlyCounter(bytes_per_chunk=100)
+        self.assertEqual(0, counter.total_items())
+        f = LocalFile('fakefile.txt')
+        f.size = 0
+        counter.visit_file(f, None)
+        self.assertEqual(1, counter.total_items())
+        f = LocalFile('fakefile2.txt')
+        f.size = 200
+        counter.visit_file(f, None)
+        self.assertEqual(3, counter.total_items())