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)

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. 

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())

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-xdist pytest-mock pytest-asyncio pytest-bdd pytest-benchmark pytest-randomly responses mock hypothesis freezegun trustme requests-mock requests tomlkit
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==25.3.0
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
coverage==7.8.0
cryptography==44.0.2
-e git+https://github.com/Duke-GCB/DukeDSClient.git@e158990c3f68aef9ed4731aa446cea6d49be2950#egg=DukeDSClient
exceptiongroup==1.2.2
execnet==2.1.1
freezegun==1.5.1
future==0.16.0
gherkin-official==29.0.0
hypothesis==6.130.5
idna==3.10
importlib_metadata==8.6.1
iniconfig==2.1.0
Mako==1.3.9
MarkupSafe==3.0.2
mock==5.2.0
packaging==24.2
parse==1.20.2
parse_type==0.6.4
pluggy==1.5.0
py-cpuinfo==9.0.0
pycparser==2.22
pytest==8.3.5
pytest-asyncio==0.26.0
pytest-bdd==8.1.0
pytest-benchmark==5.1.0
pytest-cov==6.0.0
pytest-mock==3.14.0
pytest-randomly==3.16.0
pytest-xdist==3.6.1
python-dateutil==2.9.0.post0
PyYAML==3.12
requests==2.32.3
requests-mock==1.12.1
responses==0.25.7
six==1.17.0
sortedcontainers==2.4.0
tomli==2.2.1
tomlkit==0.13.2
trustme==1.2.1
typing_extensions==4.13.0
urllib3==2.3.0
zipp==3.21.0

name: DukeDSClient
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==25.3.0
      - certifi==2025.1.31
      - cffi==1.17.1
      - charset-normalizer==3.4.1
      - coverage==7.8.0
      - cryptography==44.0.2
      - exceptiongroup==1.2.2
      - execnet==2.1.1
      - freezegun==1.5.1
      - future==0.16.0
      - gherkin-official==29.0.0
      - hypothesis==6.130.5
      - idna==3.10
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - mako==1.3.9
      - markupsafe==3.0.2
      - mock==5.2.0
      - packaging==24.2
      - parse==1.20.2
      - parse-type==0.6.4
      - pluggy==1.5.0
      - py-cpuinfo==9.0.0
      - pycparser==2.22
      - pytest==8.3.5
      - pytest-asyncio==0.26.0
      - pytest-bdd==8.1.0
      - pytest-benchmark==5.1.0
      - pytest-cov==6.0.0
      - pytest-mock==3.14.0
      - pytest-randomly==3.16.0
      - pytest-xdist==3.6.1
      - python-dateutil==2.9.0.post0
      - pyyaml==3.12
      - requests==2.32.3
      - requests-mock==1.12.1
      - responses==0.25.7
      - six==1.17.0
      - sortedcontainers==2.4.0
      - tomli==2.2.1
      - tomlkit==0.13.2
      - trustme==1.2.1
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - zipp==3.21.0
prefix: /opt/conda/envs/DukeDSClient

ddsc/core/tests/test_upload.py::TestLocalOnlyCounter::test_total_items ddsc/core/tests/test_localstore.py::TestLocalFile::test_count_chunks_values
ddsc/core/tests/test_localstore.py::TestProjectContent::test_nested_folder_str ddsc/core/tests/test_localstore.py::TestProjectContent::test_big_folder_str
ddsc/core/tests/test_upload.py::TestUploadCommand::test_nothing_to_do ddsc/core/tests/test_upload.py::TestUploadCommand::test_two_files_to_upload ddsc/core/tests/test_localstore.py::TestProjectFolderFile::test_empty_folder_str ddsc/core/tests/test_localstore.py::TestProjectFolderFile::test_folder_one_child_str ddsc/core/tests/test_localstore.py::TestProjectFolderFile::test_folder_two_children_str ddsc/core/tests/test_localstore.py::TestProjectFolderFile::test_nested_folder_str ddsc/core/tests/test_localstore.py::TestProjectFolderFile::test_file_str ddsc/core/tests/test_localstore.py::TestProjectContent::test_folder_name_removes_slash ddsc/core/tests/test_localstore.py::TestProjectContent::test_empty_folder_str ddsc/core/tests/test_localstore.py::TestProjectContent::test_folder_up_and_back ddsc/core/tests/test_localstore.py::TestProjectContent::test_folder_dot_name ddsc/core/tests/test_localstore.py::TestProjectContent::test_exclude_dot_files ddsc/core/tests/test_localstore.py::TestProjectContent::test_top_level_file_str ddsc/core/tests/test_localstore.py::TestProjectContent::test_one_folder_str ddsc/core/tests/test_localstore.py::TestProjectContent::test_empty_folder_and_file_str ddsc/core/tests/test_localstore.py::TestProjectContent::test_empty_str ddsc/core/tests/test_localstore.py::TestProjectContent::test_folder_name_no_slash ddsc/core/tests/test_localstore.py::TestProjectContent::test_include_dot_files ddsc/core/tests/test_localstore.py::TestFileFilter::test_default_file_exclude_regex
ddsc/core/localstore.py
ddsc/core/localstore.py