diff --git a/gns3server/compute/project.py b/gns3server/compute/project.py
index 713f221c5..3e7b3cad3 100644
--- a/gns3server/compute/project.py
+++ b/gns3server/compute/project.py
@@ -480,11 +480,13 @@ class Project:
stat_info = await wait_run_in_executor(lambda e=entry: e.stat())
is_dir = await wait_run_in_executor(lambda e=entry: e.is_dir())
if is_dir:
+ created_at: str | None = None
+ modified_at: str | None = None
try:
created_at = datetime.datetime.fromtimestamp(stat_info.st_ctime).isoformat()
modified_at = datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat()
except (OSError, OverflowError, ValueError):
- created_at = modified_at = ""
+ created_at = modified_at = None
files.append(
{
"path": rel_path,
@@ -506,7 +508,7 @@ class Project:
created_at = datetime.datetime.fromtimestamp(stat_info.st_ctime).isoformat()
modified_at = datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat()
except (OSError, OverflowError, ValueError):
- created_at = modified_at = ""
+ created_at = modified_at = None
files.append(
{
"path": rel_path,
@@ -537,7 +539,7 @@ class Project:
created_at = datetime.datetime.fromtimestamp(stat_info.st_ctime).isoformat()
modified_at = datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat()
except (OSError, OverflowError, ValueError):
- created_at = modified_at = ""
+ created_at = modified_at = None
files.append(
{
"path": os.path.relpath(dir_full_path, base_path),
@@ -567,7 +569,7 @@ class Project:
modified_at = datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat()
except (OSError, OverflowError, ValueError) as e:
log.warning(f"Invalid timestamp for '{rel_path}': {e}")
- created_at = modified_at = ""
+ created_at = modified_at = None
files.append(
{
"path": rel_path,
diff --git a/gns3server/schemas/controller/projects.py b/gns3server/schemas/controller/projects.py
index 6354b7696..1b84b5586 100644
--- a/gns3server/schemas/controller/projects.py
+++ b/gns3server/schemas/controller/projects.py
@@ -15,6 +15,7 @@
# along with this program. If not, see .
+from datetime import datetime
from enum import Enum
from typing import List, Optional
from uuid import UUID
@@ -111,8 +112,8 @@ class NodeFile(BaseModel):
path: str = Field(..., description="File name")
size: int = Field(..., description="File size in bytes")
- created_at: str = Field(..., description="File creation time (ISO 8601)")
- modified_at: str = Field(..., description="File modification time (ISO 8601)")
+ created_at: Optional[datetime] = Field(None, description="File creation time (ISO 8601)")
+ modified_at: Optional[datetime] = Field(None, description="File modification time (ISO 8601)")
file_type: str = Field(..., description="File type determined by the file command")
diff --git a/gns3server/schemas/controller/snapshots.py b/gns3server/schemas/controller/snapshots.py
index bf46f2bbe..d86fd21aa 100644
--- a/gns3server/schemas/controller/snapshots.py
+++ b/gns3server/schemas/controller/snapshots.py
@@ -15,10 +15,11 @@
# along with this program. If not, see .
+from datetime import datetime, timezone
from typing import Optional
from uuid import UUID
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, Field, model_validator
class SnapshotBase(BaseModel):
@@ -44,4 +45,14 @@ class Snapshot(SnapshotBase):
name: str = Field(..., description="Name of the snapshot")
filename: str = Field(..., description="Filename of the snapshot")
description: str = Field(..., description="Description of the snapshot")
- created_at: int = Field(..., description="Date of the snapshot (UTC timestamp)")
+ created_at: int = Field(
+ ..., description="Date of the snapshot (UTC timestamp). Deprecated: use created", deprecated=True
+ )
+ created: datetime = Field(..., description="Date of the snapshot (ISO 8601, UTC)")
+
+ @model_validator(mode="before")
+ @classmethod
+ def _derive_created(cls, data):
+ if isinstance(data, dict) and "created" not in data and data.get("created_at") is not None:
+ data = {**data, "created": datetime.fromtimestamp(data["created_at"], tz=timezone.utc)}
+ return data
diff --git a/tests/api/routes/controller/test_snapshots.py b/tests/api/routes/controller/test_snapshots.py
index 73f6e1ca7..aeff6d9f9 100644
--- a/tests/api/routes/controller/test_snapshots.py
+++ b/tests/api/routes/controller/test_snapshots.py
@@ -17,6 +17,7 @@
import os
import uuid
+from datetime import datetime, timezone
import pytest
import pytest_asyncio
@@ -54,6 +55,9 @@ class TestSnapshotRoutes:
response = await client.get(app.url_path_for("get_snapshots", project_id=project.id))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) == 1
+ data = response.json()[0]
+ assert isinstance(data["created_at"], int)
+ assert datetime.fromisoformat(data["created"]) == datetime.fromtimestamp(data["created_at"], tz=timezone.utc)
async def test_delete_snapshot(
self, app: FastAPI, client: AsyncClient, project: Project, snapshot: Snapshot
@@ -80,3 +84,15 @@ class TestSnapshotRoutes:
response = await client.post(app.url_path_for("create_snapshot", project_id=project.id), json={"name": "snap1"})
assert response.status_code == status.HTTP_201_CREATED
assert len(os.listdir(os.path.join(project.path, "snapshots"))) == 2
+ data = response.json()
+ assert isinstance(data["created_at"], int)
+ assert datetime.fromisoformat(data["created"]) == datetime.fromtimestamp(data["created_at"], tz=timezone.utc)
+
+ async def test_snapshot_schema_timestamps(self, app: FastAPI) -> None:
+
+ schemas = app.openapi()["components"]["schemas"]
+ assert schemas["Snapshot"]["properties"]["created"]["format"] == "date-time"
+ assert schemas["Snapshot"]["properties"]["created_at"]["type"] == "integer"
+ assert schemas["Snapshot"]["properties"]["created_at"]["deprecated"] is True
+ for field in ("created_at", "modified_at"):
+ assert {"type": "string", "format": "date-time"} in schemas["NodeFile"]["properties"][field]["anyOf"]