diff --git a/newsfragments/5693.bugfix.rst b/newsfragments/5693.bugfix.rst new file mode 100644 index 00000000000..c337ea0c814 --- /dev/null +++ b/newsfragments/5693.bugfix.rst @@ -0,0 +1 @@ +Fix file creation/modification times as shown in the file explorer on Windows diff --git a/parsec/core/mountpoint/winfsp_operations.py b/parsec/core/mountpoint/winfsp_operations.py index 71099526a4c..f1f7d7be8c5 100644 --- a/parsec/core/mountpoint/winfsp_operations.py +++ b/parsec/core/mountpoint/winfsp_operations.py @@ -3,7 +3,7 @@ import json from contextlib import contextmanager -from datetime import datetime +from datetime import datetime, timezone from functools import partial, wraps from pathlib import PurePath from typing import Any, Callable, Iterator, List, Tuple, TypeVar, Union, cast @@ -220,8 +220,8 @@ def stat_to_file_attributes(stat: dict[str, str]) -> FILE_ATTRIBUTE: def stat_to_winfsp_attributes(stat: dict[str, Any]) -> dict[str, Any]: - created = dt_to_filetime(datetime.fromtimestamp(stat["created"].timestamp())) - updated = dt_to_filetime(datetime.fromtimestamp(stat["updated"].timestamp())) + created = dt_to_filetime(datetime.fromtimestamp(stat["created"].timestamp(), tz=timezone.utc)) + updated = dt_to_filetime(datetime.fromtimestamp(stat["updated"].timestamp(), tz=timezone.utc)) attributes = { "creation_time": created, "last_access_time": updated, diff --git a/tests/core/mountpoint/test_base.py b/tests/core/mountpoint/test_base.py index 538a38ac84a..f6b0c8e0feb 100644 --- a/tests/core/mountpoint/test_base.py +++ b/tests/core/mountpoint/test_base.py @@ -13,7 +13,7 @@ from parsec._parsec import CoreEvent, DateTime from parsec.api.data import EntryID, EntryName from parsec.core import logged_core_factory -from parsec.core.fs import FsPath +from parsec.core.fs import FsPath, UserFS from parsec.core.mountpoint import ( MountpointAlreadyMounted, MountpointConfigurationError, @@ -816,3 +816,48 @@ def check_mountpoint_paths(standard_mountpoint_path, personal_mountpoint_path): await mountpoint_manager.mount_workspace(wid_standard), await mountpoint_manager.mount_workspace(wid_personal), ) + + +@pytest.mark.trio +@pytest.mark.mountpoint +async def test_file_creation_and_modification_time( + base_mountpoint, + alice_user_fs: UserFS, + event_bus, + running_backend, +): + # Populate a bit the fs first... + + wid = await alice_user_fs.workspace_create(EntryName("w")) + workspace = alice_user_fs.get_workspace(wid) + + before_creation = alice_user_fs.device.time_provider.now().timestamp() + await workspace.write_bytes("/foo.txt", b"Hello world !") + after_creation = alice_user_fs.device.time_provider.now().timestamp() + + # Now we can start fuse + + async with mountpoint_manager_factory( + alice_user_fs, event_bus, base_mountpoint + ) as mountpoint_manager: + path = trio.Path(await mountpoint_manager.mount_workspace(wid)) / "foo.txt" + stat = await path.stat() + + assert before_creation <= stat.st_ctime <= stat.st_mtime <= stat.st_atime <= after_creation + + before_modification = alice_user_fs.device.time_provider.now().timestamp() + assert await path.read_text() == "Hello world !" + await path.write_text("New content") + assert await path.read_text() == "New content" + after_modification = alice_user_fs.device.time_provider.now().timestamp() + + stat = await path.stat() + assert ( + before_creation + <= stat.st_ctime + <= after_creation + <= before_modification + <= stat.st_mtime + <= stat.st_atime + <= after_modification + )