summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDarius Makovsky <traveltissues@protonmail.com>2020-01-08 16:30:47 +0000
committerJürg Billeter <j@bitron.ch>2020-02-05 16:11:32 +0100
commitc4dafc8477f0787d622164d5c45fd9081af25a00 (patch)
tree797fc7afee13ef4634bf768ea8afdcec1cb7cd38
parent59b26c9697a23966ae72db7fed0e8af4258e4317 (diff)
downloadbuildstream-c4dafc8477f0787d622164d5c45fd9081af25a00.tar.gz
utils.py: Add file timestamp helpers
-rw-r--r--requirements/requirements.in1
-rw-r--r--requirements/requirements.txt1
-rw-r--r--src/buildstream/utils.py77
-rw-r--r--tests/internals/utils_move_atomic.py23
4 files changed, 101 insertions, 1 deletions
diff --git a/requirements/requirements.in b/requirements/requirements.in
index 50bb523da..ca38d710e 100644
--- a/requirements/requirements.in
+++ b/requirements/requirements.in
@@ -9,3 +9,4 @@ ruamel.yaml.clib >= 0.1.2
setuptools
pyroaring
ujson
+python-dateutil >= 2.7.0
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index 962090823..ab7a3a1f7 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -8,6 +8,7 @@ ruamel.yaml==0.16.5
setuptools==39.0.1
pyroaring==0.2.9
ujson==1.35
+python-dateutil==2.8.1
## The following requirements were added by pip freeze:
MarkupSafe==1.1.1
ruamel.yaml.clib==0.2.0
diff --git a/src/buildstream/utils.py b/src/buildstream/utils.py
index 88314b263..9593f3e75 100644
--- a/src/buildstream/utils.py
+++ b/src/buildstream/utils.py
@@ -33,10 +33,12 @@ from stat import S_ISDIR
import subprocess
import tempfile
import time
+import datetime
import itertools
from contextlib import contextmanager
from pathlib import Path
from typing import Callable, IO, Iterable, Iterator, Optional, Tuple, Union
+from dateutil import parser as dateutil_parser
import psutil
@@ -133,6 +135,81 @@ class FileListResult:
return ret
+def _make_timestamp(timepoint: float) -> str:
+ """Obtain the ISO 8601 timestamp represented by the time given in seconds.
+
+ Args:
+ timepoint (float): the time since the epoch in seconds
+
+ Returns:
+ (str): the timestamp specified by https://www.ietf.org/rfc/rfc3339.txt
+ with a UTC timezone code 'Z'.
+
+ """
+ assert isinstance(timepoint, float), "Time to render as timestamp must be a float: {}".format(str(timepoint))
+ try:
+ return datetime.datetime.utcfromtimestamp(timepoint).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
+ except (OverflowError, TypeError):
+ raise UtilError("Failed to make UTC timestamp from {}".format(timepoint))
+
+
+def _get_file_mtimestamp(fullpath: str) -> str:
+ """Obtain the ISO 8601 timestamp represented by the mtime of the
+ file at the given path."""
+ assert isinstance(fullpath, str), "Path to file must be a string: {}".format(str(fullpath))
+ try:
+ mtime = os.path.getmtime(fullpath)
+ except OSError:
+ raise UtilError("Failed to get mtime of file at {}".format(fullpath))
+ return _make_timestamp(mtime)
+
+
+def _parse_timestamp(timestamp: str) -> float:
+ """Parse an ISO 8601 timestamp as specified in
+ https://www.ietf.org/rfc/rfc3339.txt. Only timestamps with the UTC code
+ 'Z' or an offset are valid. For example: '2019-12-12T10:23:01.54Z' or
+ '2019-12-12T10:23:01.54+00:00'.
+
+ Args:
+ timestamp (str): the timestamp
+
+ Returns:
+ (float): The time in seconds since epoch represented by the
+ timestamp.
+
+ Raises:
+ UtilError: if extraction of seconds fails
+ """
+ assert isinstance(timestamp, str), "Timestamp to parse must be a string: {}".format(str(timestamp))
+ try:
+ errmsg = "Failed to parse given timestamp: " + timestamp
+ parsed_time = dateutil_parser.isoparse(timestamp)
+ if parsed_time.tzinfo:
+ return parsed_time.timestamp()
+ raise UtilError(errmsg)
+ except (ValueError, OverflowError, TypeError):
+ raise UtilError(errmsg)
+
+
+def _set_file_mtime(fullpath: str, seconds: Union[int, float]) -> None:
+ """Set the access and modification times of the file at the given path
+ to the given time. The time of the file will be set with nanosecond
+ resolution if supported.
+
+ Args:
+ fullpath (str): the string representing the path to the file
+ timestamp (int, float): the time in seconds since the UNIX epoch
+ """
+ assert isinstance(fullpath, str), "Path to file must be a string: {}".format(str(fullpath))
+ assert isinstance(seconds, (int, float)), "Mtime to set must be a float or integer: {}".format(str(seconds))
+ set_mtime = seconds * 10 ** 9
+ try:
+ os.utime(fullpath, times=None, ns=(int(set_mtime), int(set_mtime)))
+ except OSError:
+ errmsg = "Failed to set the times of the file at {} to {}".format(fullpath, str(seconds))
+ raise UtilError(errmsg)
+
+
def list_relative_paths(directory: str) -> Iterator[str]:
"""A generator for walking directory relative paths
diff --git a/tests/internals/utils_move_atomic.py b/tests/internals/utils_move_atomic.py
index cda020809..dd417cb66 100644
--- a/tests/internals/utils_move_atomic.py
+++ b/tests/internals/utils_move_atomic.py
@@ -3,7 +3,13 @@
import pytest
-from buildstream.utils import move_atomic, DirectoryExistsError
+from buildstream.utils import (
+ move_atomic,
+ DirectoryExistsError,
+ _get_file_mtimestamp,
+ _set_file_mtime,
+ _parse_timestamp,
+)
@pytest.fixture
@@ -89,3 +95,18 @@ def test_move_to_existing_non_empty_dir(src, tmp_path):
with pytest.raises(DirectoryExistsError):
move_atomic(src, dst)
+
+
+def test_move_to_empty_dir_set_mtime(src, tmp_path):
+ dst = tmp_path.joinpath("dst")
+ move_atomic(src, dst)
+ assert dst.joinpath("test").exists()
+ _dst = str(dst)
+ # set the mtime via stamp
+ timestamp1 = "2020-01-08T11:05:50.832123Z"
+ _set_file_mtime(_dst, _parse_timestamp(timestamp1))
+ assert timestamp1 == _get_file_mtimestamp(_dst)
+ # reset the mtime using an offset stamp
+ timestamp2 = "2010-02-12T12:05:50.832123+01:00"
+ _set_file_mtime(_dst, _parse_timestamp(timestamp2))
+ assert _get_file_mtimestamp(_dst) == "2010-02-12T11:05:50.832123Z"