Repository: Backblaze/B2_Command_Line_Tool

Issue:
Set `src_last_modified_millis` in `b2 upload_file`
When you use `sync` to upload files, it always sets `src_last_modified_millis`.  But uploading single files doesn't, which can cause later syncs to get confused.

I propose that `src_last_modified_millis` be set for every uploaded file.  If the user doesn't specify a value on the command line, we can take it from the file.

It would be good to check and make sure that the local clock isn't too different from the server's clock before uploading anything.  

Discussion:

Patch:
diff --git a/b2/console_tool.py b/b2/console_tool.py
index 0e7c7c7..bf15d7b 100644
--- a/b2/console_tool.py
+++ b/b2/console_tool.py
@@ -35,7 +35,7 @@ from .exception import (B2Error, BadFileInfo)
 from .file_version import (FileVersionInfo)
 from .parse_args import parse_arg_list
 from .progress import (make_progress_listener)
-from .raw_api import (test_raw_api)
+from .raw_api import (SRC_LAST_MODIFIED_MILLIS, test_raw_api)
 from .sync import parse_sync_folder, sync_folders
 from .utils import (current_time_millis, set_shutting_down)
 from .version import (VERSION)
@@ -859,6 +859,10 @@ class UploadFile(Command):
                 raise BadFileInfo(info)
             file_infos[parts[0]] = parts[1]
 
+        if SRC_LAST_MODIFIED_MILLIS not in file_infos:
+            file_infos[SRC_LAST_MODIFIED_MILLIS
+                      ] = str(int(os.path.getmtime(args.localFilePath) * 1000))
+
         max_workers = args.threads or 10
         self.api.set_thread_pool_size(max_workers)
 
diff --git a/b2/raw_api.py b/b2/raw_api.py
index ef34e11..bf6230f 100644
--- a/b2/raw_api.py
+++ b/b2/raw_api.py
@@ -26,6 +26,9 @@ from .download_dest import DownloadDestBytes
 from .exception import ChecksumMismatch, TruncatedOutput, UnexpectedCloudBehaviour
 from .utils import b2_url_encode, hex_sha1_of_stream
 
+# Standard names for file info entries
+SRC_LAST_MODIFIED_MILLIS = 'src_last_modified_millis'
+
 
 @six.add_metaclass(ABCMeta)
 class AbstractRawApi(object):
@@ -236,8 +239,8 @@ class B2RawApi(AbstractRawApi):
                     raise UnexpectedCloudBehaviour('Content-Range header was expected')
             file_info = dict((k[10:], info[k]) for k in info if k.startswith('x-bz-info-'))
 
-            if 'src_last_modified_millis' in file_info:
-                mod_time_millis = int(file_info['src_last_modified_millis'])
+            if SRC_LAST_MODIFIED_MILLIS in file_info:
+                mod_time_millis = int(file_info[SRC_LAST_MODIFIED_MILLIS])
             else:
                 mod_time_millis = int(info['x-bz-upload-timestamp'])
 
diff --git a/b2/sync/action.py b/b2/sync/action.py
index c79023e..b9e6acc 100644
--- a/b2/sync/action.py
+++ b/b2/sync/action.py
@@ -17,6 +17,7 @@ import six
 from ..download_dest import DownloadDestLocalFile
 from ..upload_source import UploadSourceLocalFile
 from ..utils import raise_if_shutting_down
+from ..raw_api import SRC_LAST_MODIFIED_MILLIS
 from .report import SyncFileReporter
 
 logger = logging.getLogger(__name__)
@@ -79,7 +80,7 @@ class B2UploadAction(AbstractAction):
         bucket.upload(
             UploadSourceLocalFile(self.local_full_path),
             self.b2_file_name,
-            file_info={'src_last_modified_millis': str(self.mod_time_millis)},
+            file_info={SRC_LAST_MODIFIED_MILLIS: str(self.mod_time_millis)},
             progress_listener=SyncFileReporter(reporter)
         )
 
diff --git a/b2/sync/folder.py b/b2/sync/folder.py
index 7309b86..137705a 100644
--- a/b2/sync/folder.py
+++ b/b2/sync/folder.py
@@ -16,6 +16,7 @@ import six
 
 from .exception import EnvironmentEncodingError
 from .file import File, FileVersion
+from ..raw_api import SRC_LAST_MODIFIED_MILLIS
 
 
 @six.add_metaclass(ABCMeta)
@@ -198,8 +199,8 @@ class B2Folder(AbstractFolder):
                 yield File(current_name, current_versions)
                 current_versions = []
             file_info = file_version_info.file_info
-            if 'src_last_modified_millis' in file_info:
-                mod_time_millis = int(file_info['src_last_modified_millis'])
+            if SRC_LAST_MODIFIED_MILLIS in file_info:
+                mod_time_millis = int(file_info[SRC_LAST_MODIFIED_MILLIS])
             else:
                 mod_time_millis = file_version_info.upload_timestamp
             assert file_version_info.size is not None


Test patch:
diff --git a/test/test_console_tool.py b/test/test_console_tool.py
index 8ac7ee6..66e0d75 100644
--- a/test/test_console_tool.py
+++ b/test/test_console_tool.py
@@ -210,6 +210,27 @@ class TestConsoleTool(TestBase):
                 expected_stdout, '', 0
             )
 
+            # Get file info
+            mod_time_str = str(int(os.path.getmtime(local_file1) * 1000))
+            expected_stdout = '''
+            {
+              "accountId": "my-account",
+              "action": "upload",
+              "bucketId": "bucket_0",
+              "contentLength": 11,
+              "contentSha1": "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
+              "contentType": "b2/x-auto",
+              "fileId": "9999",
+              "fileInfo": {
+                "src_last_modified_millis": "%s"
+              },
+              "fileName": "file1.txt",
+              "uploadTimestamp": 5000
+            }
+            ''' % (mod_time_str,)
+
+            self._run_command(['get_file_info', '9999'], expected_stdout, '', 0)
+
             # Download by name
             local_download1 = os.path.join(temp_dir, 'download1.txt')
             expected_stdout = '''
@@ -218,8 +239,9 @@ class TestConsoleTool(TestBase):
             File size:    11
             Content type: b2/x-auto
             Content sha1: 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
+            INFO src_last_modified_millis: %s
             checksum matches
-            '''
+            ''' % (mod_time_str,)
 
             self._run_command(
                 [
@@ -269,7 +291,9 @@ class TestConsoleTool(TestBase):
                   "contentSha1": "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
                   "contentType": "b2/x-auto",
                   "fileId": "9999",
-                  "fileInfo": {},
+                  "fileInfo": {
+                    "src_last_modified_millis": "%s"
+                  },
                   "fileName": "file1.txt",
                   "size": 11,
                   "uploadTimestamp": 5000
@@ -278,7 +302,7 @@ class TestConsoleTool(TestBase):
               "nextFileId": null,
               "nextFileName": null
             }
-            '''
+            ''' % (mod_time_str,)
 
             self._run_command(['list_file_versions', 'my-bucket'], expected_stdout, '', 0)
 
diff --git a/test_b2_command_line.py b/test_b2_command_line.py
index 8de27c4..2435226 100644
--- a/test_b2_command_line.py
+++ b/test_b2_command_line.py
@@ -324,6 +324,7 @@ def tearDown_envvar_test(envvar_name):
 def basic_test(b2_tool, bucket_name):
 
     file_to_upload = 'README.md'
+    file_mod_time_str = str(file_mod_time_millis(file_to_upload))
 
     hex_sha1 = hashlib.sha1(read_file(file_to_upload)).hexdigest()
 
@@ -398,7 +399,12 @@ def basic_test(b2_tool, bucket_name):
     b2_tool.should_succeed(['ls', bucket_name, 'b/'], r'^b/1\nb/2\n')
 
     file_info = b2_tool.should_succeed_json(['get_file_info', second_c_version['fileId']])
-    should_equal({'color': 'blue', 'foo': 'bar=baz'}, file_info['fileInfo'])
+    expected_info = {
+        'color': 'blue',
+        'foo': 'bar=baz',
+        'src_last_modified_millis': file_mod_time_str
+    }
+    should_equal(expected_info, file_info['fileInfo'])
 
     b2_tool.should_succeed(['delete_file_version', 'c', first_c_version['fileId']])
     b2_tool.should_succeed(['ls', bucket_name], r'^a\nb/\nc\nd\n')