8
8
import posixpath
9
9
import sys
10
10
import traceback
11
- from datetime import timedelta
11
+ from datetime import datetime , timedelta , timezone
12
12
from fnmatch import fnmatch
13
13
from io import StringIO
14
14
from pathlib import Path
59
59
60
60
from .util import files as file_utils
61
61
from .util .files import (
62
+ MetadataTimeField ,
62
63
adjust_regex ,
63
64
ensure_mode_int ,
64
65
get_timestamp ,
@@ -813,6 +814,56 @@ def get(
813
814
host .noop ("file {0} has already been downloaded" .format (dest ))
814
815
815
816
817
+ def _canonicalize_timespec (field : MetadataTimeField , local_file , timespec ):
818
+ if isinstance (timespec , datetime ):
819
+ if not timespec .tzinfo :
820
+ # specify remote host timezone
821
+ timespec_with_tz = timespec .replace (tzinfo = host .get_fact (Date ).tzinfo )
822
+ return timespec_with_tz
823
+ else :
824
+ return timespec
825
+ elif isinstance (timespec , bool ) and timespec :
826
+ lf_ts = (
827
+ os .stat (local_file ).st_atime
828
+ if field is MetadataTimeField .ATIME
829
+ else os .stat (local_file ).st_mtime
830
+ )
831
+ return datetime .fromtimestamp (lf_ts , tz = timezone .utc )
832
+ else :
833
+ try :
834
+ isodatetime = datetime .fromisoformat (timespec )
835
+ if not isodatetime .tzinfo :
836
+ return isodatetime .replace (tzinfo = host .get_fact (Date ).tzinfo )
837
+ else :
838
+ return isodatetime
839
+ except ValueError :
840
+ try :
841
+ timestamp = float (timespec )
842
+ return datetime .fromtimestamp (timestamp , tz = timezone .utc )
843
+ except ValueError :
844
+ # verify there is a remote file matching path in timesrc
845
+ ref_file = host .get_fact (File , path = timespec )
846
+ if ref_file :
847
+ if field is MetadataTimeField .ATIME :
848
+ assert ref_file ["atime" ] is not None
849
+ return ref_file ["atime" ].replace (tzinfo = timezone .utc )
850
+ else :
851
+ assert ref_file ["mtime" ] is not None
852
+ return ref_file ["mtime" ].replace (tzinfo = timezone .utc )
853
+ else :
854
+ ValueError ("Bad argument for `timesspec`: {0}" .format (timespec ))
855
+
856
+
857
+ # returns True for a visible difference in the second field between the datetime values
858
+ # in the ref's TZ
859
+ def _times_differ_in_s (ref , cand ):
860
+ assert ref .tzinfo and cand .tzinfo
861
+ cand_in_ref_tz = cand .astimezone (ref .tzinfo )
862
+ return (abs ((cand_in_ref_tz - ref ).total_seconds ()) >= 1.0 ) or (
863
+ ref .second != cand_in_ref_tz .second
864
+ )
865
+
866
+
816
867
@operation ()
817
868
def put (
818
869
src : str | IO [Any ],
@@ -824,6 +875,8 @@ def put(
824
875
create_remote_dir = True ,
825
876
force = False ,
826
877
assume_exists = False ,
878
+ atime : datetime | float | int | str | bool | None = None ,
879
+ mtime : datetime | float | int | str | bool | None = None ,
827
880
):
828
881
"""
829
882
Upload a local file, or file-like object, to the remote system.
@@ -837,6 +890,8 @@ def put(
837
890
+ create_remote_dir: create the remote directory if it doesn't exist
838
891
+ force: always upload the file, even if the remote copy matches
839
892
+ assume_exists: whether to assume the local file exists
893
+ + atime: value of atime the file should have, use ``True`` to match the local file
894
+ + mtime: value of mtime the file should have, use ``True`` to match the local file
840
895
841
896
``dest``:
842
897
If this is a directory that already exists on the remote side, the local
@@ -853,7 +908,21 @@ def put(
853
908
user & group as passed to ``files.put``. The mode will *not* be copied over,
854
909
if this is required call ``files.directory`` separately.
855
910
856
- Note:
911
+ ``atime`` and ``mtime``:
912
+ When set to values other than ``False`` or ``None``, the respective metadata
913
+ fields on the remote file will updated accordingly. Timestamp values are
914
+ considered equivalent if the difference is less than one second and they have
915
+ the identical number in the seconds field. If set to ``True`` the local
916
+ file is the source of the value. Otherwise, these values can be provided as
917
+ ``datetime`` objects, POSIX timestamps, or strings that can be parsed into
918
+ either of these date and time specifications. They can also be reference file
919
+ paths on the remote host, as with the ``-r`` argument to ``touch``. If a
920
+ ``datetime`` argument has no ``tzinfo`` value (i.e., it is naive), it is
921
+ assumed to be in the remote host's local timezone. There is no shortcut for
922
+ setting both ``atime` and ``mtime`` values with a single time specification,
923
+ unlike the native ``touch`` command.
924
+
925
+ Notes:
857
926
This operation is not suitable for large files as it may involve copying
858
927
the file before uploading it.
859
928
@@ -862,6 +931,12 @@ def put(
862
931
behave as if the remote file does not match the specified permissions and
863
932
requires a change.
864
933
934
+ If the ``atime`` argument is set for a given file, unless the remote
935
+ filesystem is mounted ``noatime`` or ``relatime``, multiple runs of this
936
+ operation will trigger the change detection for that file, since the act of
937
+ reading and checksumming the file will cause the host OS to update the file's
938
+ ``atime``.
939
+
865
940
**Examples:**
866
941
867
942
.. code:: python
@@ -937,7 +1012,22 @@ def put(
937
1012
if mode :
938
1013
yield file_utils .chmod (dest , mode )
939
1014
940
- # File exists, check sum and check user/group/mode if supplied
1015
+ # do mtime before atime to ensure atime setting isn't undone by mtime setting
1016
+ if mtime :
1017
+ yield file_utils .touch (
1018
+ dest ,
1019
+ MetadataTimeField .MTIME ,
1020
+ _canonicalize_timespec (MetadataTimeField .MTIME , src , mtime ),
1021
+ )
1022
+
1023
+ if atime :
1024
+ yield file_utils .touch (
1025
+ dest ,
1026
+ MetadataTimeField .ATIME ,
1027
+ _canonicalize_timespec (MetadataTimeField .ATIME , src , atime ),
1028
+ )
1029
+
1030
+ # File exists, check sum and check user/group/mode/atime/mtime if supplied
941
1031
else :
942
1032
if not _file_equal (local_sum_path , dest ):
943
1033
yield FileUploadCommand (
@@ -952,6 +1042,20 @@ def put(
952
1042
if mode :
953
1043
yield file_utils .chmod (dest , mode )
954
1044
1045
+ if mtime :
1046
+ yield file_utils .touch (
1047
+ dest ,
1048
+ MetadataTimeField .MTIME ,
1049
+ _canonicalize_timespec (MetadataTimeField .MTIME , src , mtime ),
1050
+ )
1051
+
1052
+ if atime :
1053
+ yield file_utils .touch (
1054
+ dest ,
1055
+ MetadataTimeField .ATIME ,
1056
+ _canonicalize_timespec (MetadataTimeField .ATIME , src , atime ),
1057
+ )
1058
+
955
1059
else :
956
1060
changed = False
957
1061
@@ -965,6 +1069,26 @@ def put(
965
1069
yield file_utils .chown (dest , user , group )
966
1070
changed = True
967
1071
1072
+ # Check mtime
1073
+ if mtime :
1074
+ canonical_mtime = _canonicalize_timespec (MetadataTimeField .MTIME , src , mtime )
1075
+ assert remote_file ["mtime" ] is not None
1076
+ if _times_differ_in_s (
1077
+ canonical_mtime , remote_file ["mtime" ].replace (tzinfo = timezone .utc )
1078
+ ):
1079
+ yield file_utils .touch (dest , MetadataTimeField .MTIME , canonical_mtime )
1080
+ changed = True
1081
+
1082
+ # Check atime
1083
+ if atime :
1084
+ canonical_atime = _canonicalize_timespec (MetadataTimeField .ATIME , src , atime )
1085
+ assert remote_file ["atime" ] is not None
1086
+ if _times_differ_in_s (
1087
+ canonical_atime , remote_file ["atime" ].replace (tzinfo = timezone .utc )
1088
+ ):
1089
+ yield file_utils .touch (dest , MetadataTimeField .ATIME , canonical_atime )
1090
+ changed = True
1091
+
968
1092
if not changed :
969
1093
host .noop ("file {0} is already uploaded" .format (dest ))
970
1094
0 commit comments